package cash.z.ecc.android.sdk.demoapp.demos.listutxos import android.content.Context import android.os.Bundle import android.view.LayoutInflater import android.view.View import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.recyclerview.widget.LinearLayoutManager import cash.z.ecc.android.sdk.SdkSynchronizer 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.databinding.FragmentListUtxosBinding import cash.z.ecc.android.sdk.demoapp.ext.requireApplicationContext import cash.z.ecc.android.sdk.demoapp.type.fromResources import cash.z.ecc.android.sdk.demoapp.util.mainActivity import cash.z.ecc.android.sdk.internal.twig import cash.z.ecc.android.sdk.model.Account import cash.z.ecc.android.sdk.model.BlockHeight import cash.z.ecc.android.sdk.model.TransactionOverview import cash.z.ecc.android.sdk.model.ZcashNetwork import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlin.math.max /** * =============================================================================================== * NOTE: this is still a WIP because t-addrs are not officially supported by the SDK yet * =============================================================================================== * * * List all transactions related to the given seed, since the given birthday. This begins by * downloading any missing blocks and then validating and scanning their contents. Once scan is * complete, the transactions are available in the database and can be accessed by any SQL tool. * By default, the SDK uses a PagedTransactionRepository to provide transaction contents from the * database in a paged format that works natively with RecyclerViews. */ @Suppress("TooManyFunctions") class ListUtxosFragment : BaseDemoFragment() { private lateinit var adapter: UtxoAdapter private val address: String = "t1RwbKka1CnktvAJ1cSqdn7c6PXWG4tZqgd" private var status: Synchronizer.Status? = null private val isSynced get() = status == Synchronizer.Status.SYNCED override fun inflateBinding(layoutInflater: LayoutInflater): FragmentListUtxosBinding = FragmentListUtxosBinding.inflate(layoutInflater) private fun initUi() { binding.inputAddress.setText(address) binding.inputRangeStart.setText( ZcashNetwork.fromResources(requireApplicationContext()).saplingActivationHeight.toString() ) binding.inputRangeEnd.setText(getUxtoEndHeight(requireApplicationContext()).value.toString()) binding.buttonLoad.setOnClickListener { mainActivity()?.hideKeyboard() downloadTransactions() } initTransactionUi() } private fun downloadTransactions() { sharedViewModel.synchronizerFlow.value?.let { synchronizer -> binding.textStatus.text = "loading..." binding.textStatus.post { val network = ZcashNetwork.fromResources(requireApplicationContext()) binding.textStatus.requestFocus() val addressToUse = binding.inputAddress.text.toString() val startToUse = max( binding.inputRangeStart.text.toString().toLongOrNull() ?: network.saplingActivationHeight.value, network.saplingActivationHeight.value ) val endToUse = binding.inputRangeEnd.text.toString().toLongOrNull() ?: getUxtoEndHeight(requireApplicationContext()).value var allStart = now twig("loading transactions in range $startToUse..$endToUse") val txids = lightWalletService?.getTAddressTransactions( addressToUse, BlockHeight.new(network, startToUse)..BlockHeight.new(network, endToUse) ) var delta = now - allStart updateStatus("found ${txids?.size} transactions in ${delta}ms.", false) txids?.map { // Disabled during migration to newer SDK version; this appears to have been // leveraging non-public APIs in the SDK so perhaps should be removed // it.data.apply { // try { // runBlocking { initializer.rustBackend.decryptAndStoreTransaction(toByteArray()) } // } catch (t: Throwable) { // twig("failed to decrypt and store transaction due to: $t") // } // } }?.let { _ -> // Disabled during migration to newer SDK version; this appears to have been // leveraging non-public APIs in the SDK so perhaps should be removed // val parseStart = now // val tList = LocalRpcTypes.TransactionDataList.newBuilder().addAllData(txData).build() // val parsedTransactions = initializer.rustBackend.parseTransactionDataList(tList) // delta = now - parseStart // updateStatus("parsed txs in ${delta}ms.") } (synchronizer as SdkSynchronizer).refreshTransactions() delta = now - allStart updateStatus("Total time ${delta}ms.") lifecycleScope.launch { withContext(Dispatchers.IO) { finalCount = synchronizer.getTransactionCount() withContext(Dispatchers.Main) { @Suppress("MagicNumber") delay(100) updateStatus("Also found ${finalCount - initialCount} shielded txs") } } } } } } private val now get() = System.currentTimeMillis() private fun updateStatus(message: String, append: Boolean = true) { if (append) { binding.textStatus.text = "${binding.textStatus.text} $message" } else { binding.textStatus.text = message } twig(message) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) initUi() monitorStatus() } var initialCount: Int = 0 var finalCount: Int = 0 private fun initTransactionUi() { binding.recyclerTransactions.layoutManager = LinearLayoutManager(activity, LinearLayoutManager.VERTICAL, false) adapter = UtxoAdapter() binding.recyclerTransactions.adapter = adapter } @OptIn(ExperimentalCoroutinesApi::class) private fun monitorStatus() { viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { launch { sharedViewModel.synchronizerFlow .filterNotNull() .flatMapLatest { it.status } .collect { onStatus(it) } } launch { sharedViewModel.synchronizerFlow .filterNotNull() .flatMapLatest { it.progress } .collect { onProgress(it) } } launch { sharedViewModel.synchronizerFlow .filterNotNull() .flatMapLatest { it.processorInfo } .collect { onProcessorInfoUpdated(it) } } launch { sharedViewModel.synchronizerFlow .filterNotNull() .flatMapLatest { it.clearedTransactions } .collect { onTransactionsUpdated(it) } } launch { sharedViewModel.synchronizerFlow .filterNotNull() .collect { binding.inputAddress.setText( it.getTransparentAddress(Account.DEFAULT) ) } } } } } private fun onProcessorInfoUpdated(info: CompactBlockProcessor.ProcessorInfo) { if (info.isScanning) binding.textStatus.text = "Scanning blocks...${info.scanProgress}%" } @Suppress("MagicNumber") private fun onProgress(i: Int) { if (i < 100) binding.textStatus.text = "Downloading blocks...$i%" } private fun onStatus(status: Synchronizer.Status) { this.status = status binding.textStatus.text = "Status: $status" if (isSynced) onSyncComplete() } private fun onSyncComplete() { binding.textStatus.visibility = View.INVISIBLE } private fun onTransactionsUpdated(transactions: List) { twig("got a new paged list of transactions of size ${transactions.size}") adapter.submitList(transactions) } override fun onActionButtonClicked() { lifecycleScope.launch { withContext(Dispatchers.IO) { sharedViewModel.synchronizerFlow.value?.let { synchronizer -> twig("current count: ${(synchronizer as SdkSynchronizer).getTransactionCount()}") twig("refreshing transactions") synchronizer.refreshTransactions() twig("current count: ${synchronizer.getTransactionCount()}") } } } } @Suppress("MagicNumber") private fun getUxtoEndHeight(context: Context): BlockHeight { return BlockHeight.new(ZcashNetwork.fromResources(context), 968085L) } }