package cash.z.ecc.android.sdk.demoapp.demos.getbalance import android.os.Bundle import android.view.LayoutInflater import android.view.Menu import android.view.View import androidx.lifecycle.lifecycleScope import cash.z.ecc.android.bip39.Mnemonics import cash.z.ecc.android.bip39.toSeed import cash.z.ecc.android.sdk.Synchronizer import cash.z.ecc.android.sdk.block.CompactBlockProcessor import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment import cash.z.ecc.android.sdk.demoapp.R import cash.z.ecc.android.sdk.demoapp.databinding.FragmentGetBalanceBinding import cash.z.ecc.android.sdk.demoapp.ext.requireApplicationContext import cash.z.ecc.android.sdk.demoapp.util.SyncBlockchainBenchmarkTrace import cash.z.ecc.android.sdk.demoapp.util.fromResources import cash.z.ecc.android.sdk.ext.BenchmarkingExt import cash.z.ecc.android.sdk.ext.ZcashSdk import cash.z.ecc.android.sdk.ext.collectWith import cash.z.ecc.android.sdk.ext.convertZatoshiToZecString import cash.z.ecc.android.sdk.fixture.BlockRangeFixture import cash.z.ecc.android.sdk.internal.twig import cash.z.ecc.android.sdk.model.Account import cash.z.ecc.android.sdk.model.LightWalletEndpoint import cash.z.ecc.android.sdk.model.WalletBalance import cash.z.ecc.android.sdk.model.Zatoshi import cash.z.ecc.android.sdk.model.ZcashNetwork import cash.z.ecc.android.sdk.model.defaultForNetwork import cash.z.ecc.android.sdk.tool.DerivationTool import kotlinx.coroutines.launch /** * Displays the available balance && total balance associated with the seed defined by the default config. * comments. */ @Suppress("TooManyFunctions") class GetBalanceFragment : BaseDemoFragment() { private lateinit var synchronizer: Synchronizer override fun inflateBinding(layoutInflater: LayoutInflater): FragmentGetBalanceBinding = FragmentGetBalanceBinding.inflate(layoutInflater) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) reportTraceEvent(SyncBlockchainBenchmarkTrace.Event.BALANCE_SCREEN_START) setHasOptionsMenu(true) setup() } override fun onPrepareOptionsMenu(menu: Menu) { super.onPrepareOptionsMenu(menu) // We rather hide options menu actions while actively using the Synchronizer menu.setGroupVisible(R.id.main_menu_group, false) } override fun onDestroy() { super.onDestroy() reportTraceEvent(SyncBlockchainBenchmarkTrace.Event.BALANCE_SCREEN_END) } private fun setup() { // defaults to the value of `DemoConfig.seedWords` but can also be set by the user val seedPhrase = sharedViewModel.seedPhrase.value // Use a BIP-39 library to convert a seed phrase into a byte array. Most wallets already // have the seed stored val seed = Mnemonics.MnemonicCode(seedPhrase).toSeed() val network = ZcashNetwork.fromResources(requireApplicationContext()) synchronizer = Synchronizer.newBlocking( requireApplicationContext(), network, lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network), seed = seed, birthday = if (BenchmarkingExt.isBenchmarking()) { BlockRangeFixture.new().start } else { sharedViewModel.birthdayHeight.value } ) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val seedPhrase = sharedViewModel.seedPhrase.value val seed = Mnemonics.MnemonicCode(seedPhrase).toSeed() val network = ZcashNetwork.fromResources(requireApplicationContext()) binding.shield.apply { setOnClickListener { lifecycleScope.launch { synchronizer.shieldFunds(DerivationTool.deriveUnifiedSpendingKey(seed, network, Account.DEFAULT)) } } } } override fun onResume() { super.onResume() // the lifecycleScope is used to dispose of the synchronize when the fragment dies synchronizer.start(lifecycleScope) monitorChanges() } private fun monitorChanges() { synchronizer.status.collectWith(lifecycleScope, ::onStatus) synchronizer.progress.collectWith(lifecycleScope, ::onProgress) synchronizer.processorInfo.collectWith(lifecycleScope, ::onProcessorInfoUpdated) synchronizer.orchardBalances.collectWith(lifecycleScope, ::onOrchardBalance) synchronizer.saplingBalances.collectWith(lifecycleScope, ::onSaplingBalance) synchronizer.transparentBalances.collectWith(lifecycleScope, ::onTransparentBalance) } private fun onOrchardBalance( orchardBalance: WalletBalance? ) { binding.orchardBalance.apply { text = orchardBalance.humanString() } } private fun onSaplingBalance( saplingBalance: WalletBalance? ) { binding.saplingBalance.apply { text = saplingBalance.humanString() } } private fun onTransparentBalance( transparentBalance: WalletBalance? ) { binding.transparentBalance.apply { text = transparentBalance.humanString() } binding.shield.apply { // TODO [#776]: Support variable fees // TODO [#776]: https://github.com/zcash/zcash-android-wallet-sdk/issues/776 visibility = if ((transparentBalance?.available ?: Zatoshi(0)) > ZcashSdk.MINERS_FEE) { View.VISIBLE } else { View.GONE } } } private fun onStatus(status: Synchronizer.Status) { twig("Synchronizer status: $status") // report benchmark event val traceEvents = when (status) { Synchronizer.Status.DOWNLOADING -> { listOf( SyncBlockchainBenchmarkTrace.Event.BLOCKCHAIN_SYNC_START, SyncBlockchainBenchmarkTrace.Event.DOWNLOAD_START ) } Synchronizer.Status.VALIDATING -> { listOf( SyncBlockchainBenchmarkTrace.Event.DOWNLOAD_END, SyncBlockchainBenchmarkTrace.Event.VALIDATION_START ) } Synchronizer.Status.SCANNING -> { listOf( SyncBlockchainBenchmarkTrace.Event.VALIDATION_END, SyncBlockchainBenchmarkTrace.Event.SCAN_START ) } Synchronizer.Status.SYNCED -> { listOf( SyncBlockchainBenchmarkTrace.Event.SCAN_END, SyncBlockchainBenchmarkTrace.Event.BLOCKCHAIN_SYNC_END ) } else -> null } traceEvents?.forEach { reportTraceEvent(it) } binding.textStatus.text = "Status: $status" onOrchardBalance(synchronizer.orchardBalances.value) onSaplingBalance(synchronizer.saplingBalances.value) onTransparentBalance(synchronizer.transparentBalances.value) } @Suppress("MagicNumber") private fun onProgress(i: Int) { if (i < 100) { binding.textStatus.text = "Downloading blocks...$i%" } } private fun onProcessorInfoUpdated(info: CompactBlockProcessor.ProcessorInfo) { if (info.isScanning) binding.textStatus.text = "Scanning blocks...${info.scanProgress}%" } } @Suppress("MagicNumber") private fun WalletBalance?.humanString() = if (null == this) { "Calculating balance" } else { """ Pending balance: ${pending.convertZatoshiToZecString(12)} Available balance: ${available.convertZatoshiToZecString(12)} Total balance: ${total.convertZatoshiToZecString(12)} """.trimIndent() }