Merge branch 'main' into merge-detekt-warning-fixes

This commit is contained in:
Jack Grigg 2022-08-23 21:28:01 +01:00
commit 0b80bff23d
61 changed files with 687 additions and 723 deletions

View File

@ -1,5 +1,7 @@
<component name="ProjectCodeStyleConfiguration"> <component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173"> <code_scheme name="Project" version="173">
<option name="RIGHT_MARGIN" value="120" />
<option name="WRAP_WHEN_TYPING_REACHES_RIGHT_MARGIN" value="true" />
<JetCodeStyleSettings> <JetCodeStyleSettings>
<option name="PACKAGES_TO_USE_STAR_IMPORTS"> <option name="PACKAGES_TO_USE_STAR_IMPORTS">
<value> <value>

View File

@ -30,7 +30,6 @@ tasks {
exclude("**/jvmTest/**") exclude("**/jvmTest/**")
exclude("**/androidTest/**") exclude("**/androidTest/**")
config.setFrom(files("${rootProject.projectDir}/tools/detekt.yml")) config.setFrom(files("${rootProject.projectDir}/tools/detekt.yml"))
baseline.set(file("$rootDir/tools/detekt-baseline.xml"))
buildUponDefaultConfig = true buildUponDefaultConfig = true
} }

View File

@ -20,12 +20,14 @@ import androidx.viewbinding.ViewBinding
import cash.z.ecc.android.sdk.demoapp.util.fromResources import cash.z.ecc.android.sdk.demoapp.util.fromResources
import cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService import cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService
import cash.z.ecc.android.sdk.internal.service.LightWalletService import cash.z.ecc.android.sdk.internal.service.LightWalletService
import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.model.LightWalletEndpoint import cash.z.ecc.android.sdk.model.LightWalletEndpoint
import cash.z.ecc.android.sdk.model.ZcashNetwork import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.model.defaultForNetwork import cash.z.ecc.android.sdk.model.defaultForNetwork
import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.navigation.NavigationView import com.google.android.material.navigation.NavigationView
@Suppress("TooManyFunctions")
class MainActivity : class MainActivity :
AppCompatActivity(), AppCompatActivity(),
ClipboardManager.OnPrimaryClipChangedListener, ClipboardManager.OnPrimaryClipChangedListener,
@ -155,15 +157,19 @@ class MainActivity :
/* DrawerListener implementation */ /* DrawerListener implementation */
override fun onDrawerStateChanged(newState: Int) { override fun onDrawerStateChanged(newState: Int) {
twig("Drawer state changed to: $newState.")
} }
override fun onDrawerSlide(drawerView: View, slideOffset: Float) { override fun onDrawerSlide(drawerView: View, slideOffset: Float) {
twig("Drawer slides with offset: $slideOffset.")
} }
override fun onDrawerClosed(drawerView: View) { override fun onDrawerClosed(drawerView: View) {
twig("Drawer closed.")
} }
override fun onDrawerOpened(drawerView: View) { override fun onDrawerOpened(drawerView: View) {
twig("Drawer opened.")
hideKeyboard() hideKeyboard()
} }
} }

View File

@ -2,6 +2,7 @@ package cash.z.ecc.android.sdk.demoapp
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import cash.z.ecc.android.bip39.Mnemonics import cash.z.ecc.android.bip39.Mnemonics
import cash.z.ecc.android.sdk.internal.twig
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@ -10,7 +11,7 @@ import kotlinx.coroutines.flow.StateFlow
*/ */
class SharedViewModel : ViewModel() { class SharedViewModel : ViewModel() {
private val _seedPhrase = MutableStateFlow(DemoConstants.initialSeedWords) private val _seedPhrase = MutableStateFlow(DemoConstants.INITIAL_SEED_WORDS)
// publicly, this is read-only // publicly, this is read-only
val seedPhrase: StateFlow<String> get() = _seedPhrase val seedPhrase: StateFlow<String> get() = _seedPhrase
@ -25,10 +26,21 @@ class SharedViewModel : ViewModel() {
} }
private fun isValidSeedPhrase(phrase: String?): Boolean { private fun isValidSeedPhrase(phrase: String?): Boolean {
if (phrase.isNullOrEmpty()) return false if (phrase.isNullOrEmpty()) {
return false
}
return try { return try {
Mnemonics.MnemonicCode(phrase).validate() Mnemonics.MnemonicCode(phrase).validate()
true true
} catch (t: Throwable) { false } } catch (e: Mnemonics.WordCountException) {
twig("Seed phrase validation failed with WordCountException: ${e.message}, cause: ${e.cause}")
false
} catch (e: Mnemonics.InvalidWordException) {
twig("Seed phrase validation failed with InvalidWordException: ${e.message}, cause: ${e.cause}")
false
} catch (e: Mnemonics.ChecksumException) {
twig("Seed phrase validation failed with ChecksumException: ${e.message}, cause: ${e.cause}")
false
}
} }
} }

View File

@ -56,7 +56,8 @@ class GetAddressFragment : BaseDemoFragment<FragmentGetAddressBinding>() {
} }
} }
// TODO: show an example with the synchronizer // TODO [#677]: Show an example with the synchronizer
// TODO [#677]: https://github.com/zcash/zcash-android-wallet-sdk/issues/677
// //
// Android Lifecycle overrides // Android Lifecycle overrides

View File

@ -83,6 +83,7 @@ class GetBalanceFragment : BaseDemoFragment<FragmentGetBalanceBinding>() {
synchronizer.saplingBalances.filterNotNull().collectWith(lifecycleScope, ::onBalance) synchronizer.saplingBalances.filterNotNull().collectWith(lifecycleScope, ::onBalance)
} }
@Suppress("MagicNumber")
private fun onBalance(balance: WalletBalance) { private fun onBalance(balance: WalletBalance) {
binding.textBalance.text = """ binding.textBalance.text = """
Available balance: ${balance.available.convertZatoshiToZecString(12)} Available balance: ${balance.available.convertZatoshiToZecString(12)}
@ -100,6 +101,7 @@ class GetBalanceFragment : BaseDemoFragment<FragmentGetBalanceBinding>() {
} }
} }
@Suppress("MagicNumber")
private fun onProgress(i: Int) { private fun onProgress(i: Int) {
if (i < 100) { if (i < 100) {
binding.textStatus.text = "Downloading blocks...$i%" binding.textStatus.text = "Downloading blocks...$i%"

View File

@ -43,10 +43,15 @@ class GetBlockFragment : BaseDemoFragment<FragmentGetBlockBinding>() {
} }
@Suppress("UNUSED_PARAMETER") @Suppress("UNUSED_PARAMETER")
private fun onApply(_unused: View? = null) { private fun onApply(unused: View? = null) {
val network = ZcashNetwork.fromResources(requireApplicationContext()) val network = ZcashNetwork.fromResources(requireApplicationContext())
val newHeight = min(binding.textBlockHeight.text.toString().toLongOrNull() ?: network.saplingActivationHeight.value, network.saplingActivationHeight.value) val newHeight = min(
binding.textBlockHeight.text.toString().toLongOrNull()
?: network.saplingActivationHeight.value,
network.saplingActivationHeight.value
)
@Suppress("TooGenericExceptionCaught")
try { try {
setBlockHeight(BlockHeight.new(network, newHeight)) setBlockHeight(BlockHeight.new(network, newHeight))
} catch (t: Throwable) { } catch (t: Throwable) {

View File

@ -51,7 +51,7 @@ class GetBlockRangeFragment : BaseDemoFragment<FragmentGetBlockRangeBinding>() {
val inCount = sumOf { block -> block.vtxList.sumOf { it.spendsCount } } val inCount = sumOf { block -> block.vtxList.sumOf { it.spendsCount } }
val processTime = System.currentTimeMillis() - start - fetchDelta val processTime = System.currentTimeMillis() - start - fetchDelta
@Suppress("MaxLineLength") @Suppress("MaxLineLength", "MagicNumber")
""" """
<b>total blocks:</b> ${count.withCommas()} <b>total blocks:</b> ${count.withCommas()}
<br/><b>fetch time:</b> ${if (fetchDelta > 1000) "%.2f sec".format(fetchDelta / 1000.0) else "%d ms".format(fetchDelta)} <br/><b>fetch time:</b> ${if (fetchDelta > 1000) "%.2f sec".format(fetchDelta / 1000.0) else "%d ms".format(fetchDelta)}
@ -75,11 +75,20 @@ class GetBlockRangeFragment : BaseDemoFragment<FragmentGetBlockRangeBinding>() {
} }
@Suppress("UNUSED_PARAMETER") @Suppress("UNUSED_PARAMETER")
private fun onApply(_unused: View) { private fun onApply(unused: View) {
val network = ZcashNetwork.fromResources(requireApplicationContext()) val network = ZcashNetwork.fromResources(requireApplicationContext())
val start = max(binding.textStartHeight.text.toString().toLongOrNull() ?: network.saplingActivationHeight.value, network.saplingActivationHeight.value) val start = max(
val end = max(binding.textEndHeight.text.toString().toLongOrNull() ?: network.saplingActivationHeight.value, network.saplingActivationHeight.value) binding.textStartHeight.text.toString().toLongOrNull()
?: network.saplingActivationHeight.value,
network.saplingActivationHeight.value
)
val end = max(
binding.textEndHeight.text.toString().toLongOrNull()
?: network.saplingActivationHeight.value,
network.saplingActivationHeight.value
)
if (start <= end) { if (start <= end) {
@Suppress("TooGenericExceptionCaught")
try { try {
with(binding.buttonApply) { with(binding.buttonApply) {
isEnabled = false isEnabled = false

View File

@ -42,6 +42,7 @@ class GetPrivateKeyFragment : BaseDemoFragment<FragmentGetPrivateKeyBinding>() {
// derive the keys from the seed: // derive the keys from the seed:
// demonstrate deriving spending keys for five accounts but only take the first one // demonstrate deriving spending keys for five accounts but only take the first one
lifecycleScope.launchWhenStarted { lifecycleScope.launchWhenStarted {
@Suppress("MagicNumber")
val spendingKey = DerivationTool.deriveSpendingKeys( val spendingKey = DerivationTool.deriveSpendingKeys(
seed, seed,
ZcashNetwork.fromResources(requireApplicationContext()), ZcashNetwork.fromResources(requireApplicationContext()),

View File

@ -5,7 +5,6 @@ import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentHomeBinding import cash.z.ecc.android.sdk.demoapp.databinding.FragmentHomeBinding
@ -18,10 +17,9 @@ import kotlinx.coroutines.launch
* data just for sanity. The goal is for each demo to be self-contained so that the behavior is * data just for sanity. The goal is for each demo to be self-contained so that the behavior is
* repeatable and independent of pre-existing state. * repeatable and independent of pre-existing state.
*/ */
@Suppress("TooManyFunctions")
class HomeFragment : BaseDemoFragment<FragmentHomeBinding>() { class HomeFragment : BaseDemoFragment<FragmentHomeBinding>() {
private val homeViewModel: HomeViewModel by viewModels()
override fun inflateBinding(layoutInflater: LayoutInflater) = override fun inflateBinding(layoutInflater: LayoutInflater) =
FragmentHomeBinding.inflate(layoutInflater) FragmentHomeBinding.inflate(layoutInflater)

View File

@ -31,6 +31,7 @@ import kotlinx.coroutines.runBlocking
* By default, the SDK uses a PagedTransactionRepository to provide transaction contents from the * By default, the SDK uses a PagedTransactionRepository to provide transaction contents from the
* database in a paged format that works natively with RecyclerViews. * database in a paged format that works natively with RecyclerViews.
*/ */
@Suppress("TooManyFunctions")
class ListTransactionsFragment : BaseDemoFragment<FragmentListTransactionsBinding>() { class ListTransactionsFragment : BaseDemoFragment<FragmentListTransactionsBinding>() {
private lateinit var initializer: Initializer private lateinit var initializer: Initializer
private lateinit var synchronizer: Synchronizer private lateinit var synchronizer: Synchronizer
@ -97,6 +98,7 @@ class ListTransactionsFragment : BaseDemoFragment<FragmentListTransactionsBindin
if (info.isScanning) binding.textInfo.text = "Scanning blocks...${info.scanProgress}%" if (info.isScanning) binding.textInfo.text = "Scanning blocks...${info.scanProgress}%"
} }
@Suppress("MagicNumber")
private fun onProgress(i: Int) { private fun onProgress(i: Int) {
if (i < 100) binding.textInfo.text = "Downloading blocks...$i%" if (i < 100) binding.textInfo.text = "Downloading blocks...$i%"
} }

View File

@ -22,6 +22,7 @@ class TransactionViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
private val icon = itemView.findViewById<ImageView>(R.id.image_transaction_type) private val icon = itemView.findViewById<ImageView>(R.id.image_transaction_type)
private val formatter = SimpleDateFormat("M/d h:mma", Locale.getDefault()) private val formatter = SimpleDateFormat("M/d h:mma", Locale.getDefault())
@Suppress("MagicNumber")
fun bindTo(transaction: ConfirmedTransaction?) { fun bindTo(transaction: ConfirmedTransaction?) {
val isInbound = transaction?.toAddress.isNullOrEmpty() val isInbound = transaction?.toAddress.isNullOrEmpty()
amountText.text = transaction?.valueInZatoshi.convertZatoshiToZecString() amountText.text = transaction?.valueInZatoshi.convertZatoshiToZecString()
@ -32,7 +33,9 @@ class TransactionViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
icon.rotation = if (isInbound) 0f else 180f icon.rotation = if (isInbound) 0f else 180f
icon.rotation = if (isInbound) 0f else 180f icon.rotation = if (isInbound) 0f else 180f
icon.setColorFilter(ContextCompat.getColor(itemView.context, if (isInbound) R.color.tx_inbound else R.color.tx_outbound)) icon.setColorFilter(
ContextCompat.getColor(itemView.context, if (isInbound) R.color.tx_inbound else R.color.tx_outbound)
)
} }
private fun getMemoString(transaction: ConfirmedTransaction?): String { private fun getMemoString(transaction: ConfirmedTransaction?): String {

View File

@ -44,6 +44,7 @@ import kotlin.math.max
* By default, the SDK uses a PagedTransactionRepository to provide transaction contents from the * By default, the SDK uses a PagedTransactionRepository to provide transaction contents from the
* database in a paged format that works natively with RecyclerViews. * database in a paged format that works natively with RecyclerViews.
*/ */
@Suppress("TooManyFunctions")
class ListUtxosFragment : BaseDemoFragment<FragmentListUtxosBinding>() { class ListUtxosFragment : BaseDemoFragment<FragmentListUtxosBinding>() {
private lateinit var seed: ByteArray private lateinit var seed: ByteArray
private lateinit var initializer: Initializer private lateinit var initializer: Initializer
@ -88,7 +89,9 @@ class ListUtxosFragment : BaseDemoFragment<FragmentListUtxosBinding>() {
private fun initUi() { private fun initUi() {
binding.inputAddress.setText(address) binding.inputAddress.setText(address)
binding.inputRangeStart.setText(ZcashNetwork.fromResources(requireApplicationContext()).saplingActivationHeight.toString()) binding.inputRangeStart.setText(
ZcashNetwork.fromResources(requireApplicationContext()).saplingActivationHeight.toString()
)
binding.inputRangeEnd.setText(getUxtoEndHeight(requireApplicationContext()).value.toString()) binding.inputRangeEnd.setText(getUxtoEndHeight(requireApplicationContext()).value.toString())
binding.buttonLoad.setOnClickListener { binding.buttonLoad.setOnClickListener {
@ -148,6 +151,7 @@ class ListUtxosFragment : BaseDemoFragment<FragmentListUtxosBinding>() {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
finalCount = (synchronizer as SdkSynchronizer).getTransactionCount() finalCount = (synchronizer as SdkSynchronizer).getTransactionCount()
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
@Suppress("MagicNumber")
delay(100) delay(100)
updateStatus("Also found ${finalCount - initialCount} shielded txs") updateStatus("Also found ${finalCount - initialCount} shielded txs")
} }
@ -188,7 +192,9 @@ class ListUtxosFragment : BaseDemoFragment<FragmentListUtxosBinding>() {
var initialCount: Int = 0 var initialCount: Int = 0
var finalCount: Int = 0 var finalCount: Int = 0
fun resetInBackground() {
@Suppress("TooGenericExceptionCaught")
private fun resetInBackground() {
try { try {
lifecycleScope.launch { lifecycleScope.launch {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
@ -242,6 +248,7 @@ class ListUtxosFragment : BaseDemoFragment<FragmentListUtxosBinding>() {
if (info.isScanning) binding.textStatus.text = "Scanning blocks...${info.scanProgress}%" if (info.isScanning) binding.textStatus.text = "Scanning blocks...${info.scanProgress}%"
} }
@Suppress("MagicNumber")
private fun onProgress(i: Int) { private fun onProgress(i: Int) {
if (i < 100) binding.textStatus.text = "Downloading blocks...$i%" if (i < 100) binding.textStatus.text = "Downloading blocks...$i%"
} }

View File

@ -19,6 +19,7 @@ class UtxoViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val timeText = itemView.findViewById<TextView>(R.id.text_transaction_timestamp) private val timeText = itemView.findViewById<TextView>(R.id.text_transaction_timestamp)
private val formatter = SimpleDateFormat("M/d h:mma", Locale.getDefault()) private val formatter = SimpleDateFormat("M/d h:mma", Locale.getDefault())
@Suppress("MagicNumber")
fun bindTo(transaction: ConfirmedTransaction?) { fun bindTo(transaction: ConfirmedTransaction?) {
amountText.text = transaction?.valueInZatoshi.convertZatoshiToZecString() amountText.text = transaction?.valueInZatoshi.convertZatoshiToZecString()
timeText.text = timeText.text =

View File

@ -45,6 +45,7 @@ import kotlinx.coroutines.runBlocking
* PendingTransaction objects which represent the active state of the transaction that was sent. * PendingTransaction objects which represent the active state of the transaction that was sent.
* Any time the state of that transaction changes, a new instance will be emitted. * Any time the state of that transaction changes, a new instance will be emitted.
*/ */
@Suppress("TooManyFunctions")
class SendFragment : BaseDemoFragment<FragmentSendBinding>() { class SendFragment : BaseDemoFragment<FragmentSendBinding>() {
private lateinit var synchronizer: Synchronizer private lateinit var synchronizer: Synchronizer
@ -81,7 +82,9 @@ class SendFragment : BaseDemoFragment<FragmentSendBinding>() {
}.let { initializer -> }.let { initializer ->
synchronizer = Synchronizer.newBlocking(initializer) synchronizer = Synchronizer.newBlocking(initializer)
} }
spendingKey = runBlocking { DerivationTool.deriveSpendingKeys(seed, ZcashNetwork.fromResources(requireApplicationContext())).first() } spendingKey = runBlocking {
DerivationTool.deriveSpendingKeys(seed, ZcashNetwork.fromResources(requireApplicationContext())).first()
}
} }
// //
@ -111,10 +114,10 @@ class SendFragment : BaseDemoFragment<FragmentSendBinding>() {
private fun initSendUi() { private fun initSendUi() {
amountInput = binding.inputAmount.apply { amountInput = binding.inputAmount.apply {
setText(DemoConstants.sendAmount.toZecString()) setText(DemoConstants.SEND_AMOUNT.toZecString())
} }
addressInput = binding.inputAddress.apply { addressInput = binding.inputAddress.apply {
setText(DemoConstants.toAddress) setText(DemoConstants.TO_ADDRESS)
} }
binding.buttonSend.setOnClickListener(::onSend) binding.buttonSend.setOnClickListener(::onSend)
} }
@ -140,6 +143,7 @@ class SendFragment : BaseDemoFragment<FragmentSendBinding>() {
} }
} }
@Suppress("MagicNumber")
private fun onProgress(i: Int) { private fun onProgress(i: Int) {
if (i < 100) { if (i < 100) {
binding.textStatus.text = "Downloading blocks...$i%" binding.textStatus.text = "Downloading blocks...$i%"
@ -153,6 +157,7 @@ class SendFragment : BaseDemoFragment<FragmentSendBinding>() {
if (info.isScanning) binding.textStatus.text = "Scanning blocks...${info.scanProgress}%" if (info.isScanning) binding.textStatus.text = "Scanning blocks...${info.scanProgress}%"
} }
@Suppress("MagicNumber")
private fun onBalance(balance: WalletBalance?) { private fun onBalance(balance: WalletBalance?) {
this.balance = balance this.balance = balance
if (!isSyncing) { if (!isSyncing) {
@ -177,14 +182,17 @@ class SendFragment : BaseDemoFragment<FragmentSendBinding>() {
mainActivity()?.hideKeyboard() mainActivity()?.hideKeyboard()
} }
@Suppress("ComplexMethod")
private fun onPendingTxUpdated(pendingTransaction: PendingTransaction?) { private fun onPendingTxUpdated(pendingTransaction: PendingTransaction?) {
val id = pendingTransaction?.id ?: -1 val id = pendingTransaction?.id ?: -1
val message = when { val message = when {
pendingTransaction == null -> "Transaction not found" pendingTransaction == null -> "Transaction not found"
pendingTransaction.isMined() -> "Transaction Mined (id: $id)!\n\nSEND COMPLETE".also { isSending = false } pendingTransaction.isMined() -> "Transaction Mined (id: $id)!\n\nSEND COMPLETE".also { isSending = false }
pendingTransaction.isSubmitSuccess() -> "Successfully submitted transaction!\nAwaiting confirmation..." pendingTransaction.isSubmitSuccess() -> "Successfully submitted transaction!\nAwaiting confirmation..."
pendingTransaction.isFailedEncoding() -> "ERROR: failed to encode transaction! (id: $id)".also { isSending = false } pendingTransaction.isFailedEncoding() ->
pendingTransaction.isFailedSubmit() -> "ERROR: failed to submit transaction! (id: $id)".also { isSending = false } "ERROR: failed to encode transaction! (id: $id)".also { isSending = false }
pendingTransaction.isFailedSubmit() ->
"ERROR: failed to submit transaction! (id: $id)".also { isSending = false }
pendingTransaction.isCreated() -> "Transaction creation complete! (id: $id)" pendingTransaction.isCreated() -> "Transaction creation complete! (id: $id)"
pendingTransaction.isCreating() -> "Creating transaction!".also { onResetInfo() } pendingTransaction.isCreating() -> "Creating transaction!".also { onResetInfo() }
else -> "Transaction updated!".also { twig("Unhandled TX state: $pendingTransaction") } else -> "Transaction updated!".also { twig("Unhandled TX state: $pendingTransaction") }

View File

@ -20,6 +20,7 @@ fun Number?.withCommas() = this?.let { "%,d".format(it) } ?: "Unknown"
/** /**
* Convert date time in seconds to relative time like (4 days ago). * Convert date time in seconds to relative time like (4 days ago).
*/ */
@Suppress("MagicNumber")
fun Int?.toRelativeTime(context: Context) = fun Int?.toRelativeTime(context: Context) =
this?.let { timeInSeconds -> this?.let { timeInSeconds ->
DateUtils.getRelativeDateTimeString( DateUtils.getRelativeDateTimeString(

View File

@ -10,9 +10,9 @@ import java.util.Locale
fun ZcashNetwork.Companion.fromResources(context: Context): ZcashNetwork { fun ZcashNetwork.Companion.fromResources(context: Context): ZcashNetwork {
val networkNameFromResources = context.getString(R.string.network_name).lowercase(Locale.ROOT) val networkNameFromResources = context.getString(R.string.network_name).lowercase(Locale.ROOT)
return if (networkNameFromResources == Testnet.networkName) { return if (networkNameFromResources == Testnet.networkName) {
ZcashNetwork.Testnet Testnet
} else if (networkNameFromResources.lowercase(Locale.ROOT) == Mainnet.networkName) { } else if (networkNameFromResources.lowercase(Locale.ROOT) == Mainnet.networkName) {
ZcashNetwork.Mainnet Mainnet
} else { } else {
throw IllegalArgumentException("Unknown network name: $networkNameFromResources") throw IllegalArgumentException("Unknown network name: $networkNameFromResources")
} }

View File

@ -1,14 +1,15 @@
package cash.z.ecc.android.sdk.demoapp package cash.z.ecc.android.sdk.demoapp
object DemoConstants { object DemoConstants {
const val SEND_AMOUNT: Double = 0.000018
val sendAmount: Double = 0.000018
// corresponds to address: zs15tzaulx5weua5c7l47l4pku2pw9fzwvvnsp4y80jdpul0y3nwn5zp7tmkcclqaca3mdjqjkl7hx // corresponds to address: zs15tzaulx5weua5c7l47l4pku2pw9fzwvvnsp4y80jdpul0y3nwn5zp7tmkcclqaca3mdjqjkl7hx
val initialSeedWords: String = const val INITIAL_SEED_WORDS: String =
"wish puppy smile loan doll curve hole maze file ginger hair nose key relax knife witness cannon grab despair throw review deal slush frame" "wish puppy smile loan doll curve hole maze file ginger hair nose key relax knife witness cannon grab despair" +
" throw review deal slush frame"
// corresponds to seed: urban kind wise collect social marble riot primary craft lucky head cause syrup odor artist decorate rhythm phone style benefit portion bus truck top // corresponds to seed: urban kind wise collect social marble riot primary craft lucky head cause syrup odor
val toAddress: String = // artist decorate rhythm phone style benefit portion bus truck top
const val TO_ADDRESS: String =
"zs1lcdmue7rewgvzh3jd09sfvwq3sumu6hkhpk53q94kcneuffjkdg9e3tyxrugkmpza5c3c5e6eqh" "zs1lcdmue7rewgvzh3jd09sfvwq3sumu6hkhpk53q94kcneuffjkdg9e3tyxrugkmpza5c3c5e6eqh"
} }

View File

@ -1,14 +1,16 @@
package cash.z.ecc.android.sdk.demoapp package cash.z.ecc.android.sdk.demoapp
object DemoConstants { object DemoConstants {
val utxoEndHeight: Int = 1075590 const val UTXO_END_HEIGHT: Int = 1075590
val sendAmount: Double = 0.00017 const val SEND_AMOUNT: Double = 0.00017
// corresponds to address: ztestsapling1zhqvuq8zdwa8nsnde7074kcfsat0w25n08jzuvz5skzcs6h9raxu898l48xwr8fmkny3zqqrgd9 // corresponds to address: ztestsapling1zhqvuq8zdwa8nsnde7074kcfsat0w25n08jzuvz5skzcs6h9raxu898l48xwr8fmkny3zqqrgd9
val initialSeedWords: String = const val INITIAL_SEED_WORDS: String =
"wish puppy smile loan doll curve hole maze file ginger hair nose key relax knife witness cannon grab despair throw review deal slush frame" "wish puppy smile loan doll curve hole maze file ginger hair nose key relax knife witness cannon grab despair" +
" throw review deal slush frame"
// corresponds to seed: urban kind wise collect social marble riot primary craft lucky head cause syrup odor artist decorate rhythm phone style benefit portion bus truck top // corresponds to seed: urban kind wise collect social marble riot primary craft lucky head cause syrup odor artist
val toAddress: String = // decorate rhythm phone style benefit portion bus truck top
const val TO_ADDRESS: String =
"ztestsapling1ddttvrm6ueug4vwlczs8daqjaul60aur4udnvcz9qdnjt9ekt2tsxheqvv3mn50wvhmzj4ge9rl" "ztestsapling1ddttvrm6ueug4vwlczs8daqjaul60aur4udnvcz9qdnjt9ekt2tsxheqvv3mn50wvhmzj4ge9rl"
} }

View File

@ -153,7 +153,9 @@ android {
} }
kotlinOptions { kotlinOptions {
// Tricky: fix: By default, the kotlin_module name will not include the version (in classes.jar/META-INF). Instead it has a colon, which breaks compilation on Windows. This is one way to set it explicitly to the proper value. See https://github.com/zcash/zcash-android-wallet-sdk/issues/222 for more info. // Tricky: fix: By default, the kotlin_module name will not include the version (in classes.jar/META-INF).
// Instead it has a colon, which breaks compilation on Windows. This is one way to set it explicitly to the
// proper value. See https://github.com/zcash/zcash-android-wallet-sdk/issues/222 for more info.
freeCompilerArgs += listOf("-module-name", "$myArtifactId-${myVersion}_release") freeCompilerArgs += listOf("-module-name", "$myArtifactId-${myVersion}_release")
} }
@ -271,8 +273,10 @@ dependencies {
// Locked Versions // Locked Versions
// these should be checked regularly and removed when possible // these should be checked regularly and removed when possible
// solves error: Duplicate class com.google.common.util.concurrent.ListenableFuture found in modules jetified-guava-26.0-android.jar (com.google.guava:guava:26.0-android) and listenablefuture-1.0.jar (com.google.guava:listenablefuture:1.0) // solves error: Duplicate class com.google.common.util.concurrent.ListenableFuture found in modules
// per this recommendation from Chris Povirk, given guava's decision to split ListenableFuture away from Guava: https://groups.google.com/d/msg/guava-discuss/GghaKwusjcY/bCIAKfzOEwAJ // jetified-guava-26.0-android.jar (com.google.guava:guava:26.0-android) and listenablefuture-1.0.jar
// (com.google.guava:listenablefuture:1.0) per this recommendation from Chris Povirk, given guava's decision to
// split ListenableFuture away from Guava: https://groups.google.com/d/msg/guava-discuss/GghaKwusjcY/bCIAKfzOEwAJ
implementation(libs.guava) implementation(libs.guava)
// OKIO is a transitive dependency used when writing param files to disk. Like GSON, this can be // OKIO is a transitive dependency used when writing param files to disk. Like GSON, this can be
// replaced if needed. For compatibility, we match the library version used in grpc-okhttp: // replaced if needed. For compatibility, we match the library version used in grpc-okhttp:
@ -301,7 +305,8 @@ dependencies {
androidTestImplementation(libs.coroutines.okhttp) androidTestImplementation(libs.coroutines.okhttp)
androidTestImplementation(libs.kotlin.test) androidTestImplementation(libs.kotlin.test)
androidTestImplementation(libs.kotlinx.coroutines.test) androidTestImplementation(libs.kotlinx.coroutines.test)
// used by 'ru.gildor.corutines.okhttp.await' (to make simple suspended requests) and breaks on versions higher than 3.8.0 // used by 'ru.gildor.corutines.okhttp.await' (to make simple suspended requests) and breaks on versions higher
// than 3.8.0
androidTestImplementation(libs.okhttp) androidTestImplementation(libs.okhttp)
// sample mnemonic plugin // sample mnemonic plugin

View File

@ -9,6 +9,7 @@ import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.ZcashNetwork import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.util.TestWallet import cash.z.ecc.android.sdk.util.TestWallet
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Ignore
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.junit.runners.Parameterized import org.junit.runners.Parameterized
@ -74,6 +75,10 @@ class SanityTest(
} }
@Test @Test
@Ignore(
"This test needs to be refactored to a separate test module. It causes SSLHandshakeException: Chain " +
"validation failed on CI"
)
fun testLatestHeight() = runBlocking { fun testLatestHeight() = runBlocking {
if (wallet.networkName == "mainnet") { if (wallet.networkName == "mainnet") {
val expectedHeight = BlockExplorer.fetchLatestHeight() val expectedHeight = BlockExplorer.fetchLatestHeight()
@ -94,6 +99,10 @@ class SanityTest(
} }
@Test @Test
@Ignore(
"This test needs to be refactored to a separate test module. It causes SSLHandshakeException: Chain " +
"validation failed on CI"
)
fun testSingleBlockDownload() = runBlocking { fun testSingleBlockDownload() = runBlocking {
// Fetch height directly because the synchronizer hasn't started, yet. Then we test the // Fetch height directly because the synchronizer hasn't started, yet. Then we test the
// result, only if there is no server communication problem. // result, only if there is no server communication problem.

View File

@ -13,6 +13,10 @@ import org.junit.runner.RunWith
import java.io.File import java.io.File
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@Ignore(
"These tests need to be refactored to a separate test module. They cause SSLHandshakeException: Chain " +
"validation failed on CI"
)
class SaplingParamToolTest { class SaplingParamToolTest {
val context: Context = InstrumentationRegistry.getInstrumentation().context val context: Context = InstrumentationRegistry.getInstrumentation().context

View File

@ -34,6 +34,7 @@ class Initializer private constructor(
suspend fun erase() = erase(context, network, alias) suspend fun erase() = erase(context, network, alias)
@Suppress("TooManyFunctions")
class Config private constructor( class Config private constructor(
val viewingKeys: MutableList<UnifiedFullViewingKey> = mutableListOf(), val viewingKeys: MutableList<UnifiedFullViewingKey> = mutableListOf(),
var alias: String = ZcashSdk.DEFAULT_ALIAS var alias: String = ZcashSdk.DEFAULT_ALIAS
@ -236,6 +237,7 @@ class Initializer private constructor(
numberOfAccounts: Int = 1 numberOfAccounts: Int = 1
): Config = ): Config =
apply { apply {
@Suppress("SpreadOperator")
setViewingKeys( setViewingKeys(
*DerivationTool.deriveUnifiedFullViewingKeys( *DerivationTool.deriveUnifiedFullViewingKeys(
seed, seed,
@ -423,7 +425,7 @@ class Initializer private constructor(
*/ */
internal fun validateAlias(alias: String) { internal fun validateAlias(alias: String) {
require( require(
alias.length in 1..99 && alias[0].isLetter() && alias.length in ZcashSdk.ALIAS_MIN_LENGTH..ZcashSdk.ALIAS_MAX_LENGTH && alias[0].isLetter() &&
alias.all { it.isLetterOrDigit() || it == '_' } alias.all { it.isLetterOrDigit() || it == '_' }
) { ) {
"ERROR: Invalid alias ($alias). For security, the alias must be shorter than 100 " + "ERROR: Invalid alias ($alias). For security, the alias must be shorter than 100 " +

View File

@ -98,6 +98,7 @@ import kotlin.coroutines.EmptyCoroutineContext
*/ */
@OptIn(kotlinx.coroutines.ObsoleteCoroutinesApi::class) @OptIn(kotlinx.coroutines.ObsoleteCoroutinesApi::class)
@FlowPreview @FlowPreview
@Suppress("TooManyFunctions")
class SdkSynchronizer internal constructor( class SdkSynchronizer internal constructor(
private val storage: TransactionRepository, private val storage: TransactionRepository,
private val txManager: OutboundTransactionManager, private val txManager: OutboundTransactionManager,
@ -329,7 +330,9 @@ class SdkSynchronizer internal constructor(
// Storage APIs // Storage APIs
// //
// TODO: turn this section into the data access API. For now, just aggregate all the things that we want to do with the underlying data // TODO [#682]: turn this section into the data access API. For now, just aggregate all the things that we want
// to do with the underlying data
// TODO [#682]: https://github.com/zcash/zcash-android-wallet-sdk/issues/682
suspend fun findBlockHash(height: BlockHeight): ByteArray? { suspend fun findBlockHash(height: BlockHeight): ByteArray? {
return (storage as? PagedTransactionRepository)?.findBlockHash(height) return (storage as? PagedTransactionRepository)?.findBlockHash(height)
@ -363,7 +366,8 @@ class SdkSynchronizer internal constructor(
suspend fun refreshAllBalances() { suspend fun refreshAllBalances() {
refreshSaplingBalance() refreshSaplingBalance()
refreshTransparentBalance() refreshTransparentBalance()
// TODO: refresh orchard balance // TODO [#682]: refresh orchard balance
// TODO [#682]: https://github.com/zcash/zcash-android-wallet-sdk/issues/682
twig("Warning: Orchard balance does not yet refresh. Only some of the plumbing is in place.") twig("Warning: Orchard balance does not yet refresh. Only some of the plumbing is in place.")
} }
@ -378,11 +382,7 @@ class SdkSynchronizer internal constructor(
} }
suspend fun isValidAddress(address: String): Boolean { suspend fun isValidAddress(address: String): Boolean {
try { return !validateAddress(address).isNotValid
return !validateAddress(address).isNotValid
} catch (t: Throwable) {
}
return false
} }
private fun CoroutineScope.onReady() = launch(CoroutineExceptionHandler(::onCriticalError)) { private fun CoroutineScope.onReady() = launch(CoroutineExceptionHandler(::onCriticalError)) {
@ -411,7 +411,8 @@ class SdkSynchronizer internal constructor(
is Enhancing -> ENHANCING is Enhancing -> ENHANCING
}.let { synchronizerStatus -> }.let { synchronizerStatus ->
// ignore enhancing status for now // ignore enhancing status for now
// TODO: clean this up and handle enhancing gracefully // TODO [#682]: clean this up and handle enhancing gracefully
// TODO [#682]: https://github.com/zcash/zcash-android-wallet-sdk/issues/682
if (synchronizerStatus != ENHANCING) _status.send(synchronizerStatus) if (synchronizerStatus != ENHANCING) _status.send(synchronizerStatus)
} }
}.launchIn(this) }.launchIn(this)
@ -440,13 +441,6 @@ class SdkSynchronizer internal constructor(
onCriticalErrorHandler?.invoke(error) onCriticalErrorHandler?.invoke(error)
} }
private fun onFailedSend(error: Throwable): Boolean {
twig("ERROR while submitting transaction: $error")
return onSubmissionErrorHandler?.invoke(error)?.also {
if (it) twig("submission error handler signaled that we should try again!")
} == true
}
private fun onProcessorError(error: Throwable): Boolean { private fun onProcessorError(error: Throwable): Boolean {
twig("ERROR while processing data: $error") twig("ERROR while processing data: $error")
if (onProcessorErrorHandler == null) { if (onProcessorErrorHandler == null) {
@ -496,6 +490,7 @@ class SdkSynchronizer internal constructor(
// refresh anyway if: // refresh anyway if:
// - if it's the first time we finished scanning // - if it's the first time we finished scanning
// - if we check for blocks 5 times and find nothing was mined // - if we check for blocks 5 times and find nothing was mined
@Suppress("MagicNumber")
val shouldRefresh = !scannedRange.isEmpty() || elapsedMillis > (ZcashSdk.POLL_INTERVAL * 5) val shouldRefresh = !scannedRange.isEmpty() || elapsedMillis > (ZcashSdk.POLL_INTERVAL * 5)
val reason = if (scannedRange.isEmpty()) "it's been a while" else "new blocks were scanned" val reason = if (scannedRange.isEmpty()) "it's been a while" else "new blocks were scanned"
@ -527,11 +522,12 @@ class SdkSynchronizer internal constructor(
} }
} }
@Suppress("LongMethod", "ComplexMethod")
private suspend fun refreshPendingTransactions() { private suspend fun refreshPendingTransactions() {
twig("[cleanup] beginning to refresh and clean up pending transactions") twig("[cleanup] beginning to refresh and clean up pending transactions")
// TODO: this would be the place to clear out any stale pending transactions. Remove filter // TODO [#682]: this would be the place to clear out any stale pending transactions. Remove filter logic and
// logic and then delete any pending transaction with sufficient confirmations (all in one // then delete any pending transaction with sufficient confirmations (all in one db transaction).
// db transaction). // TODO [#682]: https://github.com/zcash/zcash-android-wallet-sdk/issues/682
val allPendingTxs = txManager.getAll().first() val allPendingTxs = txManager.getAll().first()
val lastScannedHeight = storage.lastScannedHeight() val lastScannedHeight = storage.lastScannedHeight()
@ -596,15 +592,23 @@ class SdkSynchronizer internal constructor(
} }
.forEach { .forEach {
val result = txManager.abort(it) val result = txManager.abort(it)
twig("[cleanup] FOUND EXPIRED pendingTX (lastScanHeight: $lastScannedHeight expiryHeight: ${it.expiryHeight}): and ${it.id} ${if (result > 0) "successfully removed" else "failed to remove"} it") twig(
"[cleanup] FOUND EXPIRED pendingTX (lastScanHeight: $lastScannedHeight " +
" expiryHeight: ${it.expiryHeight}): and ${it.id} " +
"${if (result > 0) "successfully removed" else "failed to remove"} it"
)
} }
twig("[cleanup] deleting expired transactions from storage", -1) twig("[cleanup] deleting expired transactions from storage", -1)
val expiredCount = storage.deleteExpired(lastScannedHeight) val expiredCount = storage.deleteExpired(lastScannedHeight)
if (expiredCount > 0) twig("[cleanup] deleted $expiredCount expired transaction(s)!") if (expiredCount > 0) {
twig("[cleanup] deleted $expiredCount expired transaction(s)!")
}
hasCleaned = hasCleaned || (expiredCount > 0) hasCleaned = hasCleaned || (expiredCount > 0)
if (hasCleaned) refreshAllBalances() if (hasCleaned) {
refreshAllBalances()
}
twig("[cleanup] done refreshing and cleaning up pending transactions", -1) twig("[cleanup] done refreshing and cleaning up pending transactions", -1)
} }
@ -711,6 +715,7 @@ class SdkSynchronizer internal constructor(
txManager.isValidUnifiedAddress(address) txManager.isValidUnifiedAddress(address)
override suspend fun validateAddress(address: String): AddressType { override suspend fun validateAddress(address: String): AddressType {
@Suppress("TooGenericExceptionCaught")
return try { return try {
if (isValidShieldedAddr(address)) { if (isValidShieldedAddr(address)) {
Shielded Shielded
@ -779,7 +784,10 @@ object DefaultSynchronizerFactory {
) )
} }
// TODO [#242]: Don't hard code page size. It is a workaround for Uncaught Exception: android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views. and is probably related to FlowPagedList // TODO [#242]: Don't hard code page size. It is a workaround for Uncaught Exception:
// android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy
// can touch its views. and is probably related to FlowPagedList
// TODO [#242]: https://github.com/zcash/zcash-android-wallet-sdk/issues/242
private const val DEFAULT_PAGE_SIZE = 1000 private const val DEFAULT_PAGE_SIZE = 1000
suspend fun defaultTransactionRepository(initializer: Initializer): TransactionRepository = suspend fun defaultTransactionRepository(initializer: Initializer): TransactionRepository =
PagedTransactionRepository.new( PagedTransactionRepository.new(

View File

@ -22,6 +22,7 @@ import kotlinx.coroutines.runBlocking
* support for coroutines, we favor their use in the SDK and incorporate that choice into this * support for coroutines, we favor their use in the SDK and incorporate that choice into this
* contract. * contract.
*/ */
@Suppress("TooManyFunctions")
interface Synchronizer { interface Synchronizer {
// //

View File

@ -61,6 +61,7 @@ import java.util.concurrent.atomic.AtomicInteger
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
import kotlin.math.roundToInt import kotlin.math.roundToInt
import kotlin.time.Duration.Companion.days
/** /**
* Responsible for processing the compact blocks that are received from the lightwallet server. This class encapsulates * Responsible for processing the compact blocks that are received from the lightwallet server. This class encapsulates
@ -78,6 +79,7 @@ import kotlin.math.roundToInt
*/ */
@OptIn(kotlinx.coroutines.ObsoleteCoroutinesApi::class) @OptIn(kotlinx.coroutines.ObsoleteCoroutinesApi::class)
@OpenForTesting @OpenForTesting
@Suppress("TooManyFunctions", "LargeClass")
class CompactBlockProcessor internal constructor( class CompactBlockProcessor internal constructor(
val downloader: CompactBlockDownloader, val downloader: CompactBlockDownloader,
private val repository: TransactionRepository, private val repository: TransactionRepository,
@ -219,12 +221,15 @@ class CompactBlockProcessor internal constructor(
when (result) { when (result) {
BlockProcessingResult.Reconnecting -> { BlockProcessingResult.Reconnecting -> {
val napTime = calculatePollInterval(true) val napTime = calculatePollInterval(true)
twig("Unable to process new blocks because we are disconnected! Attempting to reconnect in ${napTime}ms") twig(
"Unable to process new blocks because we are disconnected! Attempting to " +
"reconnect in ${napTime}ms"
)
delay(napTime) delay(napTime)
} }
BlockProcessingResult.NoBlocksToProcess, BlockProcessingResult.FailedEnhance -> { BlockProcessingResult.NoBlocksToProcess, BlockProcessingResult.FailedEnhance -> {
val noWorkDone = val noWorkDone = currentInfo.lastDownloadRange?.isEmpty()
currentInfo.lastDownloadRange?.isEmpty() ?: true && currentInfo.lastScanRange?.isEmpty() ?: true ?: true && currentInfo.lastScanRange?.isEmpty() ?: true
val summary = if (noWorkDone) { val summary = if (noWorkDone) {
"Nothing to process: no new blocks to download or scan" "Nothing to process: no new blocks to download or scan"
} else { } else {
@ -232,13 +237,18 @@ class CompactBlockProcessor internal constructor(
} }
consecutiveChainErrors.set(0) consecutiveChainErrors.set(0)
val napTime = calculatePollInterval() val napTime = calculatePollInterval()
twig("$summary${if (result == BlockProcessingResult.FailedEnhance) " (but there were enhancement errors! We ignore those, for now. Memos in this block range are probably missing! This will be improved in a future release.)" else ""}! Sleeping for ${napTime}ms (latest height: ${currentInfo.networkBlockHeight}).") twig(
"$summary${if (result == BlockProcessingResult.FailedEnhance) " (but there were" +
" enhancement errors! We ignore those, for now. Memos in this block range are" +
" probably missing! This will be improved in a future release.)" else ""}! Sleeping" +
" for ${napTime}ms (latest height: ${currentInfo.networkBlockHeight})."
)
delay(napTime) delay(napTime)
} }
is BlockProcessingResult.Error -> { is BlockProcessingResult.Error -> {
if (consecutiveChainErrors.get() >= RETRIES) { if (consecutiveChainErrors.get() >= RETRIES) {
val errorMessage = val errorMessage = "ERROR: unable to resolve reorg at height $result after " +
"ERROR: unable to resolve reorg at height $result after ${consecutiveChainErrors.get()} correction attempts!" "${consecutiveChainErrors.get()} correction attempts!"
fail(CompactBlockProcessorException.FailedReorgRepair(errorMessage)) fail(CompactBlockProcessorException.FailedReorgRepair(errorMessage))
} else { } else {
handleChainError(result.failedAtHeight) handleChainError(result.failedAtHeight)
@ -313,9 +323,10 @@ class CompactBlockProcessor internal constructor(
*/ */
private suspend fun updateRanges(): Boolean = withContext(IO) { private suspend fun updateRanges(): Boolean = withContext(IO) {
try { try {
// TODO: rethink this and make it easier to understand what's happening. Can we reduce this // TODO [#683]: rethink this and make it easier to understand what's happening. Can we reduce this
// so that we only work with actual changing info rather than periodic snapshots? Do we need // so that we only work with actual changing info rather than periodic snapshots? Do we need
// to calculate these derived values every time? // to calculate these derived values every time?
// TODO [#683]: https://github.com/zcash/zcash-android-wallet-sdk/issues/683
ProcessorInfo( ProcessorInfo(
networkBlockHeight = downloader.getLatestBlockHeight(), networkBlockHeight = downloader.getLatestBlockHeight(),
lastScannedHeight = getLastScannedHeight(), lastScannedHeight = getLastScannedHeight(),
@ -335,7 +346,10 @@ class CompactBlockProcessor internal constructor(
networkBlockHeight = initialInfo.networkBlockHeight, networkBlockHeight = initialInfo.networkBlockHeight,
lastScannedHeight = initialInfo.lastScannedHeight, lastScannedHeight = initialInfo.lastScannedHeight,
lastDownloadedHeight = initialInfo.lastDownloadedHeight, lastDownloadedHeight = initialInfo.lastDownloadedHeight,
lastScanRange = if (initialInfo.lastScannedHeight != null && initialInfo.networkBlockHeight != null) { lastScanRange = if (
initialInfo.lastScannedHeight != null &&
initialInfo.networkBlockHeight != null
) {
initialInfo.lastScannedHeight + 1..initialInfo.networkBlockHeight initialInfo.lastScannedHeight + 1..initialInfo.networkBlockHeight
} else { } else {
null null
@ -392,6 +406,7 @@ class CompactBlockProcessor internal constructor(
Twig.sprout("enhancing") Twig.sprout("enhancing")
twig("Enhancing transaction details for blocks $lastScanRange") twig("Enhancing transaction details for blocks $lastScanRange")
setState(Enhancing) setState(Enhancing)
@Suppress("TooGenericExceptionCaught")
return try { return try {
val newTxs = repository.findNewTransactions(lastScanRange) val newTxs = repository.findNewTransactions(lastScanRange)
if (newTxs.isEmpty()) { if (newTxs.isEmpty()) {
@ -411,17 +426,19 @@ class CompactBlockProcessor internal constructor(
twig("Done enhancing transaction details") twig("Done enhancing transaction details")
BlockProcessingResult.Success BlockProcessingResult.Success
} catch (t: Throwable) { } catch (t: Throwable) {
twig("Failed to enhance due to $t") twig("Failed to enhance due to: ${t.message} caused by: ${t.cause}")
t.printStackTrace()
BlockProcessingResult.FailedEnhance BlockProcessingResult.FailedEnhance
} finally { } finally {
Twig.clip("enhancing") Twig.clip("enhancing")
} }
} }
// TODO: we still need a way to identify those transactions that failed to be enhanced // TODO [#683]: we still need a way to identify those transactions that failed to be enhanced
// TODO [#683]: https://github.com/zcash/zcash-android-wallet-sdk/issues/683
private suspend fun enhance(transaction: ConfirmedTransaction) = withContext(Dispatchers.IO) { private suspend fun enhance(transaction: ConfirmedTransaction) = withContext(Dispatchers.IO) {
var downloaded = false var downloaded = false
@Suppress("TooGenericExceptionCaught")
try { try {
twig("START: enhancing transaction (id:${transaction.id} block:${transaction.minedHeight})") twig("START: enhancing transaction (id:${transaction.id} block:${transaction.minedHeight})")
downloader.fetchTransaction(transaction.rawTransactionId)?.let { tx -> downloader.fetchTransaction(transaction.rawTransactionId)?.let { tx ->
@ -478,12 +495,16 @@ class CompactBlockProcessor internal constructor(
if (onSetupErrorListener?.invoke(error) != true) { if (onSetupErrorListener?.invoke(error) != true) {
throw error throw error
} else { } else {
twig("Warning: An ${error::class.java.simpleName} was encountered while verifying setup but it was ignored by the onSetupErrorHandler. Ignoring message: ${error.message}") twig(
"Warning: An ${error::class.java.simpleName} was encountered while verifying setup but " +
"it was ignored by the onSetupErrorHandler. Ignoring message: ${error.message}"
)
} }
} }
} }
private suspend fun updateBirthdayHeight() { private suspend fun updateBirthdayHeight() {
@Suppress("TooGenericExceptionCaught")
try { try {
val betterBirthday = calculateBirthdayHeight() val betterBirthday = calculateBirthdayHeight()
if (betterBirthday > birthdayHeight) { if (betterBirthday > birthdayHeight) {
@ -496,13 +517,17 @@ class CompactBlockProcessor internal constructor(
} }
var failedUtxoFetches = 0 var failedUtxoFetches = 0
@Suppress("MagicNumber")
internal suspend fun refreshUtxos(tAddress: String, startHeight: BlockHeight): Int? = internal suspend fun refreshUtxos(tAddress: String, startHeight: BlockHeight): Int? =
withContext(IO) { withContext(IO) {
var count: Int? = null var count: Int? = null
// todo: cleanup the way that we prevent this from running excessively // TODO [683]: cleanup the way that we prevent this from running excessively
// For now, try for about 3 blocks per app launch. If the service fails it is // For now, try for about 3 blocks per app launch. If the service fails it is
// probably disabled on ligthtwalletd, so then stop trying until the next app launch. // probably disabled on ligthtwalletd, so then stop trying until the next app launch.
// TODO [#683]: https://github.com/zcash/zcash-android-wallet-sdk/issues/683
if (failedUtxoFetches < 9) { // there are 3 attempts per block if (failedUtxoFetches < 9) { // there are 3 attempts per block
@Suppress("TooGenericExceptionCaught")
try { try {
retryUpTo(3) { retryUpTo(3) {
val result = downloader.lightWalletService.fetchUtxos(tAddress, startHeight) val result = downloader.lightWalletService.fetchUtxos(tAddress, startHeight)
@ -510,10 +535,17 @@ class CompactBlockProcessor internal constructor(
} }
} catch (e: Throwable) { } catch (e: Throwable) {
failedUtxoFetches++ failedUtxoFetches++
twig("Warning: Fetching UTXOs is repeatedly failing! We will only try about ${(9 - failedUtxoFetches + 2) / 3} more times then give up for this session.") twig(
"Warning: Fetching UTXOs is repeatedly failing! We will only try about " +
"${(9 - failedUtxoFetches + 2) / 3} more times then give up for this session. " +
"Exception message: ${e.message}, caused by: ${e.cause}."
)
} }
} else { } else {
twig("Warning: gave up on fetching UTXOs for this session. It seems to unavailable on lightwalletd.") twig(
"Warning: gave up on fetching UTXOs for this session. It seems to unavailable on " +
"lightwalletd."
)
} }
count count
} }
@ -530,6 +562,7 @@ class CompactBlockProcessor internal constructor(
twig("Checking for UTXOs above height $aboveHeight") twig("Checking for UTXOs above height $aboveHeight")
result.forEach { utxo: Service.GetAddressUtxosReply -> result.forEach { utxo: Service.GetAddressUtxosReply ->
twig("Found UTXO at height ${utxo.height.toInt()} with ${utxo.valueZat} zatoshi") twig("Found UTXO at height ${utxo.height.toInt()} with ${utxo.valueZat} zatoshi")
@Suppress("TooGenericExceptionCaught")
try { try {
rustBackend.putUtxo( rustBackend.putUtxo(
tAddress, tAddress,
@ -540,9 +573,14 @@ class CompactBlockProcessor internal constructor(
BlockHeight(utxo.height) BlockHeight(utxo.height)
) )
} catch (t: Throwable) { } catch (t: Throwable) {
// TODO: more accurately track the utxos that were skipped (in theory, this could fail for other reasons) // TODO [#683]: more accurately track the utxos that were skipped (in theory, this could fail for other
// reasons)
// TODO [#683]: https://github.com/zcash/zcash-android-wallet-sdk/issues/683
skipped++ skipped++
twig("Warning: Ignoring transaction at height ${utxo.height} @ index ${utxo.index} because it already exists") twig(
"Warning: Ignoring transaction at height ${utxo.height} @ index ${utxo.index} because " +
"it already exists. Exception message: ${t.message}, caused by: ${t.cause}."
)
} }
} }
// return the number of UTXOs that were downloaded // return the number of UTXOs that were downloaded
@ -554,7 +592,9 @@ class CompactBlockProcessor internal constructor(
* *
* @param range the range of blocks to download. * @param range the range of blocks to download.
*/ */
@VisibleForTesting // allow mocks to verify how this is called, rather than the downloader, which is more complex @VisibleForTesting
// allow mocks to verify how this is called, rather than the downloader, which is more complex
@Suppress("MagicNumber")
internal suspend fun downloadNewBlocks(range: ClosedRange<BlockHeight>?) = internal suspend fun downloadNewBlocks(range: ClosedRange<BlockHeight>?) =
withContext<Unit>(IO) { withContext<Unit>(IO) {
if (null == range || range.isEmpty()) { if (null == range || range.isEmpty()) {
@ -571,7 +611,10 @@ class CompactBlockProcessor internal constructor(
(if (missingBlockCount.rem(DOWNLOAD_BATCH_SIZE) == 0L) 0 else 1) (if (missingBlockCount.rem(DOWNLOAD_BATCH_SIZE) == 0L) 0 else 1)
) )
var progress: Int var progress: Int
twig("found $missingBlockCount missing blocks, downloading in $batches batches of $DOWNLOAD_BATCH_SIZE...") twig(
"found $missingBlockCount missing blocks, downloading in $batches batches of " +
"$DOWNLOAD_BATCH_SIZE..."
)
for (i in 1..batches) { for (i in 1..batches) {
retryUpTo(RETRIES, { CompactBlockProcessorException.FailedDownload(it) }) { retryUpTo(RETRIES, { CompactBlockProcessorException.FailedDownload(it) }) {
val end = BlockHeight.new( val end = BlockHeight.new(
@ -582,7 +625,10 @@ class CompactBlockProcessor internal constructor(
) )
) // subtract 1 on the first value because the range is inclusive ) // subtract 1 on the first value because the range is inclusive
var count = 0 var count = 0
twig("downloaded $downloadedBlockHeight..$end (batch $i of $batches) [${downloadedBlockHeight..end}]") { twig(
"downloaded $downloadedBlockHeight..$end (batch $i of $batches) " +
"[${downloadedBlockHeight..end}]"
) {
count = downloader.downloadBlockRange(downloadedBlockHeight..end) count = downloader.downloadBlockRange(downloadedBlockHeight..end)
} }
twig("downloaded $count blocks!") twig("downloaded $count blocks!")
@ -628,6 +674,7 @@ class CompactBlockProcessor internal constructor(
* *
* @param range the range of blocks to scan. * @param range the range of blocks to scan.
*/ */
@Suppress("MagicNumber")
private suspend fun scanNewBlocks(range: ClosedRange<BlockHeight>?): Boolean = withContext(IO) { private suspend fun scanNewBlocks(range: ClosedRange<BlockHeight>?): Boolean = withContext(IO) {
if (null == range || range.isEmpty()) { if (null == range || range.isEmpty()) {
twig("no blocks to scan for range $range") twig("no blocks to scan for range $range")
@ -649,16 +696,26 @@ class CompactBlockProcessor internal constructor(
val lastScannedHeight = val lastScannedHeight =
BlockHeight.new(network, range.start.value + metrics.cumulativeItems - 1) BlockHeight.new(network, range.start.value + metrics.cumulativeItems - 1)
val percentValue = val percentValue =
(lastScannedHeight.value - range.start.value) / (range.endInclusive.value - range.start.value + 1).toFloat() * 100.0f (lastScannedHeight.value - range.start.value) /
val percent = "%.0f".format(percentValue.coerceAtMost(100f).coerceAtLeast(0f)) (range.endInclusive.value - range.start.value + 1).toFloat() * 100.0f
twig("batch scanned ($percent%): $lastScannedHeight/${range.endInclusive} | ${metrics.batchTime}ms, ${metrics.batchItems}blks, ${metrics.batchIps.format()}bps") val percent = "%.0f".format(
percentValue.coerceAtMost(100f)
.coerceAtLeast(0f)
)
twig(
"batch scanned ($percent%): $lastScannedHeight/${range.endInclusive} | " +
"${metrics.batchTime}ms, ${metrics.batchItems}blks, ${metrics.batchIps.format()}bps"
)
if (currentInfo.lastScannedHeight != lastScannedHeight) { if (currentInfo.lastScannedHeight != lastScannedHeight) {
scannedNewBlocks = true scannedNewBlocks = true
updateProgress(lastScannedHeight = lastScannedHeight) updateProgress(lastScannedHeight = lastScannedHeight)
} }
// if we made progress toward our scan, then keep trying // if we made progress toward our scan, then keep trying
} while (result && scannedNewBlocks && lastScannedHeight < range.endInclusive) } while (result && scannedNewBlocks && lastScannedHeight < range.endInclusive)
twig("batch scan complete! Total time: ${metrics.cumulativeTime} Total blocks measured: ${metrics.cumulativeItems} Cumulative bps: ${metrics.cumulativeIps.format()}") twig(
"batch scan complete! Total time: ${metrics.cumulativeTime} Total blocks measured: " +
"${metrics.cumulativeItems} Cumulative bps: ${metrics.cumulativeIps.format()}"
)
} }
Twig.clip("scanning") Twig.clip("scanning")
result result
@ -704,7 +761,8 @@ class CompactBlockProcessor internal constructor(
} }
private suspend fun handleChainError(errorHeight: BlockHeight) { private suspend fun handleChainError(errorHeight: BlockHeight) {
// TODO consider an error object containing hash information // TODO [#683]: consider an error object containing hash information
// TODO [#683]: https://github.com/zcash/zcash-android-wallet-sdk/issues/683
printValidationErrorInfo(errorHeight) printValidationErrorInfo(errorHeight)
determineLowerBound(errorHeight).let { lowerBound -> determineLowerBound(errorHeight).let { lowerBound ->
twig("handling chain error at $errorHeight by rewinding to block $lowerBound") twig("handling chain error at $errorHeight by rewinding to block $lowerBound")
@ -714,9 +772,10 @@ class CompactBlockProcessor internal constructor(
} }
suspend fun getNearestRewindHeight(height: BlockHeight): BlockHeight { suspend fun getNearestRewindHeight(height: BlockHeight): BlockHeight {
// TODO: add a concept of original checkpoint height to the processor. For now, derive it // TODO [#683]: add a concept of original checkpoint height to the processor. For now, derive it
val originalCheckpoint = // add one because we already have the checkpoint. Add one again because we delete ABOVE the block
lowerBoundHeight + MAX_REORG_SIZE + 2 // add one because we already have the checkpoint. Add one again because we delete ABOVE the block // TODO [#683]: https://github.com/zcash/zcash-android-wallet-sdk/issues/683
val originalCheckpoint = lowerBoundHeight + MAX_REORG_SIZE + 2
return if (height < originalCheckpoint) { return if (height < originalCheckpoint) {
originalCheckpoint originalCheckpoint
} else { } else {
@ -732,10 +791,10 @@ class CompactBlockProcessor internal constructor(
*/ */
suspend fun quickRewind() { suspend fun quickRewind() {
val height = max(currentInfo.lastScannedHeight, repository.lastScannedHeight()) val height = max(currentInfo.lastScannedHeight, repository.lastScannedHeight())
val blocksPerDay = 60 * 60 * 24 * 1000 / ZcashSdk.BLOCK_INTERVAL_MILLIS.toInt() val blocksPer14Days = 14.days.inWholeMilliseconds / ZcashSdk.BLOCK_INTERVAL_MILLIS.toInt()
val twoWeeksBack = BlockHeight.new( val twoWeeksBack = BlockHeight.new(
network, network,
(height.value - blocksPerDay * 14).coerceAtLeast(lowerBoundHeight.value) (height.value - blocksPer14Days).coerceAtLeast(lowerBoundHeight.value)
) )
rewindToNearestHeight(twoWeeksBack, false) rewindToNearestHeight(twoWeeksBack, false)
} }
@ -744,6 +803,7 @@ class CompactBlockProcessor internal constructor(
* @param alsoClearBlockCache when true, also clear the block cache which forces a redownload of * @param alsoClearBlockCache when true, also clear the block cache which forces a redownload of
* blocks. Otherwise, the cached blocks will be used in the rescan, which in most cases, is fine. * blocks. Otherwise, the cached blocks will be used in the rescan, which in most cases, is fine.
*/ */
@Suppress("LongMethod")
suspend fun rewindToNearestHeight( suspend fun rewindToNearestHeight(
height: BlockHeight, height: BlockHeight,
alsoClearBlockCache: Boolean = false alsoClearBlockCache: Boolean = false
@ -753,19 +813,36 @@ class CompactBlockProcessor internal constructor(
val lastScannedHeight = currentInfo.lastScannedHeight val lastScannedHeight = currentInfo.lastScannedHeight
val lastLocalBlock = repository.lastScannedHeight() val lastLocalBlock = repository.lastScannedHeight()
val targetHeight = getNearestRewindHeight(height) val targetHeight = getNearestRewindHeight(height)
twig("Rewinding from $lastScannedHeight to requested height: $height using target height: $targetHeight with last local block: $lastLocalBlock")
if ((null == lastScannedHeight && targetHeight < lastLocalBlock) || (null != lastScannedHeight && targetHeight < lastScannedHeight)) { twig(
"Rewinding from $lastScannedHeight to requested height: $height using target height: " +
"$targetHeight with last local block: $lastLocalBlock"
)
if (null == lastScannedHeight && targetHeight < lastLocalBlock) {
twig("Rewinding because targetHeight is less than lastLocalBlock.")
rustBackend.rewindToHeight(targetHeight)
} else if (null != lastScannedHeight && targetHeight < lastScannedHeight) {
twig("Rewinding because targetHeight is less than lastScannedHeight.")
rustBackend.rewindToHeight(targetHeight) rustBackend.rewindToHeight(targetHeight)
} else { } else {
twig("not rewinding dataDb because the last scanned height is $lastScannedHeight and the last local block is $lastLocalBlock both of which are less than the target height of $targetHeight") twig(
"not rewinding dataDb because the last scanned height is $lastScannedHeight and the" +
" last local block is $lastLocalBlock both of which are less than the target height of " +
"$targetHeight"
)
} }
val currentNetworkBlockHeight = currentInfo.networkBlockHeight val currentNetworkBlockHeight = currentInfo.networkBlockHeight
if (alsoClearBlockCache) { if (alsoClearBlockCache) {
twig("Also clearing block cache back to $targetHeight. These rewound blocks will download in the next scheduled scan") twig(
"Also clearing block cache back to $targetHeight. These rewound blocks will " +
"download in the next scheduled scan"
)
downloader.rewindToHeight(targetHeight) downloader.rewindToHeight(targetHeight)
// communicate that the wallet is no longer synced because it might remain this way for 20+ seconds because we only download on 20s time boundaries so we can't trigger any immediate action // communicate that the wallet is no longer synced because it might remain this way for 20+
// seconds because we only download on 20s time boundaries so we can't trigger any immediate action
setState(Downloading) setState(Downloading)
if (null == currentNetworkBlockHeight) { if (null == currentNetworkBlockHeight) {
updateProgress( updateProgress(
@ -800,7 +877,11 @@ class CompactBlockProcessor internal constructor(
if (null != lastScannedHeight) { if (null != lastScannedHeight) {
val range = (targetHeight + 1)..lastScannedHeight val range = (targetHeight + 1)..lastScannedHeight
twig("We kept the cache blocks in place so we don't need to wait for the next scheduled download to rescan. Instead we will rescan and validate blocks ${range.start}..${range.endInclusive}") twig(
"We kept the cache blocks in place so we don't need to wait for the next " +
"scheduled download to rescan. Instead we will rescan and validate blocks " +
"${range.start}..${range.endInclusive}"
)
if (validateAndScanNewBlocks(range) == BlockProcessingResult.Success) { if (validateAndScanNewBlocks(range) == BlockProcessingResult.Success) {
enhanceTransactionDetails(range) enhanceTransactionDetails(range)
} }
@ -811,19 +892,27 @@ class CompactBlockProcessor internal constructor(
/** insightful function for debugging these critical errors */ /** insightful function for debugging these critical errors */
private suspend fun printValidationErrorInfo(errorHeight: BlockHeight, count: Int = 11) { private suspend fun printValidationErrorInfo(errorHeight: BlockHeight, count: Int = 11) {
// Note: blocks are public information so it's okay to print them but, still, let's not unless we're debugging something // Note: blocks are public information so it's okay to print them but, still, let's not unless we're
// debugging something
if (!BuildConfig.DEBUG) return if (!BuildConfig.DEBUG) return
var errorInfo = fetchValidationErrorInfo(errorHeight) var errorInfo = fetchValidationErrorInfo(errorHeight)
twig("validation failed at block ${errorInfo.errorHeight} which had hash ${errorInfo.actualPrevHash} but the expected hash was ${errorInfo.expectedPrevHash}") twig(
"validation failed at block ${errorInfo.errorHeight} which had hash " +
"${errorInfo.actualPrevHash} but the expected hash was ${errorInfo.expectedPrevHash}"
)
errorInfo = fetchValidationErrorInfo(errorHeight + 1) errorInfo = fetchValidationErrorInfo(errorHeight + 1)
twig("The next block block: ${errorInfo.errorHeight} which had hash ${errorInfo.actualPrevHash} but the expected hash was ${errorInfo.expectedPrevHash}") twig(
"The next block block: ${errorInfo.errorHeight} which had hash ${errorInfo.actualPrevHash} but " +
"the expected hash was ${errorInfo.expectedPrevHash}"
)
twig("=================== BLOCKS [$errorHeight..${errorHeight.value + count - 1}]: START ========") twig("=================== BLOCKS [$errorHeight..${errorHeight.value + count - 1}]: START ========")
repeat(count) { i -> repeat(count) { i ->
val height = errorHeight + i val height = errorHeight + i
val block = downloader.compactBlockStore.findCompactBlock(height) val block = downloader.compactBlockStore.findCompactBlock(height)
// sometimes the initial block was inserted via checkpoint and will not appear in the cache. We can get the hash another way but prevHash is correctly null. // sometimes the initial block was inserted via checkpoint and will not appear in the cache. We can get
// the hash another way but prevHash is correctly null.
val hash = block?.hash?.toByteArray() val hash = block?.hash?.toByteArray()
?: (repository as PagedTransactionRepository).findBlockHash(height) ?: (repository as PagedTransactionRepository).findBlockHash(height)
twig( twig(
@ -858,7 +947,10 @@ class CompactBlockProcessor internal constructor(
private fun determineLowerBound(errorHeight: BlockHeight): BlockHeight { private fun determineLowerBound(errorHeight: BlockHeight): BlockHeight {
val offset = min(MAX_REORG_SIZE, REWIND_DISTANCE * (consecutiveChainErrors.get() + 1)) val offset = min(MAX_REORG_SIZE, REWIND_DISTANCE * (consecutiveChainErrors.get() + 1))
return BlockHeight(max(errorHeight.value - offset, lowerBoundHeight.value)).also { return BlockHeight(max(errorHeight.value - offset, lowerBoundHeight.value)).also {
twig("offset = min($MAX_REORG_SIZE, $REWIND_DISTANCE * (${consecutiveChainErrors.get() + 1})) = $offset") twig(
"offset = min($MAX_REORG_SIZE, $REWIND_DISTANCE * (${consecutiveChainErrors.get() + 1})) = " +
"$offset"
)
twig("lowerBound = max($errorHeight - $offset, $lowerBoundHeight) = $it") twig("lowerBound = max($errorHeight - $offset, $lowerBoundHeight) = $it")
} }
} }
@ -883,6 +975,7 @@ class CompactBlockProcessor internal constructor(
suspend fun calculateBirthdayHeight(): BlockHeight { suspend fun calculateBirthdayHeight(): BlockHeight {
var oldestTransactionHeight: BlockHeight? = null var oldestTransactionHeight: BlockHeight? = null
@Suppress("TooGenericExceptionCaught")
try { try {
val tempOldestTransactionHeight = repository.receivedTransactions val tempOldestTransactionHeight = repository.receivedTransactions
.first() .first()
@ -890,10 +983,12 @@ class CompactBlockProcessor internal constructor(
?.minedBlockHeight ?.minedBlockHeight
?: lowerBoundHeight ?: lowerBoundHeight
// to be safe adjust for reorgs (and generally a little cushion is good for privacy) // to be safe adjust for reorgs (and generally a little cushion is good for privacy)
// so we round down to the nearest 100 and then subtract 100 to ensure that the result is always at least 100 blocks away // so we round down to the nearest 100 and then subtract 100 to ensure that the result is always at least
// 100 blocks away
oldestTransactionHeight = BlockHeight.new( oldestTransactionHeight = BlockHeight.new(
network, network,
tempOldestTransactionHeight.value - tempOldestTransactionHeight.value.rem(ZcashSdk.MAX_REORG_SIZE) - ZcashSdk.MAX_REORG_SIZE.toLong() tempOldestTransactionHeight.value -
tempOldestTransactionHeight.value.rem(MAX_REORG_SIZE) - MAX_REORG_SIZE.toLong()
) )
} catch (t: Throwable) { } catch (t: Throwable) {
twig("failed to calculate birthday due to: $t") twig("failed to calculate birthday due to: $t")
@ -943,6 +1038,7 @@ class CompactBlockProcessor internal constructor(
*/ */
suspend fun getBalanceInfo(accountIndex: Int = 0): WalletBalance = suspend fun getBalanceInfo(accountIndex: Int = 0): WalletBalance =
twigTask("checking balance info", -1) { twigTask("checking balance info", -1) {
@Suppress("TooGenericExceptionCaught")
try { try {
val balanceTotal = rustBackend.getBalance(accountIndex) val balanceTotal = rustBackend.getBalance(accountIndex)
twig("found total balance: $balanceTotal") twig("found total balance: $balanceTotal")
@ -1092,6 +1188,7 @@ class CompactBlockProcessor internal constructor(
/** /**
* The amount of scan progress from 0 to 100. * The amount of scan progress from 0 to 100.
*/ */
@Suppress("MagicNumber")
val scanProgress val scanProgress
get() = when { get() = when {
lastScannedHeight == null -> 0 lastScannedHeight == null -> 0

View File

@ -26,7 +26,7 @@ import java.io.File
* *
* @param context the application context * @param context the application context
*/ */
@SuppressWarnings("TooManyFunctions") @Suppress("TooManyFunctions")
internal class DatabaseCoordinator private constructor(context: Context) { internal class DatabaseCoordinator private constructor(context: Context) {
/* /*

View File

@ -1,3 +1,5 @@
@file:Suppress("TooManyFunctions")
package cash.z.ecc.android.sdk.db.entity package cash.z.ecc.android.sdk.db.entity
import android.text.format.DateUtils import android.text.format.DateUtils
@ -6,6 +8,7 @@ import androidx.room.Entity
import androidx.room.ForeignKey import androidx.room.ForeignKey
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import androidx.room.RoomWarnings import androidx.room.RoomWarnings
import cash.z.ecc.android.sdk.internal.transaction.PersistentTransactionManager
import cash.z.ecc.android.sdk.model.BlockHeight import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.Zatoshi import cash.z.ecc.android.sdk.model.Zatoshi
@ -103,6 +106,7 @@ data class PendingTransactionEntity(
val valueZatoshi: Zatoshi val valueZatoshi: Zatoshi
get() = Zatoshi(value) get() = Zatoshi(value)
@Suppress("ComplexMethod")
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other !is PendingTransactionEntity) return false if (other !is PendingTransactionEntity) return false
@ -183,6 +187,7 @@ data class ConfirmedTransaction(
BlockHeight(minedHeight) BlockHeight(minedHeight)
} }
@Suppress("ComplexMethod")
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other !is ConfirmedTransaction) return false if (other !is ConfirmedTransaction) return false
@ -352,21 +357,35 @@ fun PendingTransaction.isSubmitted(): Boolean {
} }
fun PendingTransaction.isExpired(latestHeight: BlockHeight?, saplingActivationHeight: BlockHeight): Boolean { fun PendingTransaction.isExpired(latestHeight: BlockHeight?, saplingActivationHeight: BlockHeight): Boolean {
// TODO: test for off-by-one error here. Should we use <= or < // TODO [#687]: test for off-by-one error here. Should we use <= or <
if (latestHeight == null || latestHeight.value < saplingActivationHeight.value || expiryHeight < saplingActivationHeight.value) return false // TODO [#687]: https://github.com/zcash/zcash-android-wallet-sdk/issues/687
if (latestHeight == null ||
latestHeight.value < saplingActivationHeight.value ||
expiryHeight < saplingActivationHeight.value
) {
return false
}
return expiryHeight < latestHeight.value return expiryHeight < latestHeight.value
} }
// if we don't have info on a pendingtx after 100 blocks then it's probably safe to stop polling! // if we don't have info on a pendingtx after 100 blocks then it's probably safe to stop polling!
@Suppress("MagicNumber")
fun PendingTransaction.isLongExpired(latestHeight: BlockHeight?, saplingActivationHeight: BlockHeight): Boolean { fun PendingTransaction.isLongExpired(latestHeight: BlockHeight?, saplingActivationHeight: BlockHeight): Boolean {
if (latestHeight == null || latestHeight.value < saplingActivationHeight.value || expiryHeight < saplingActivationHeight.value) return false if (latestHeight == null ||
latestHeight.value < saplingActivationHeight.value ||
expiryHeight < saplingActivationHeight.value
) {
return false
}
return (latestHeight.value - expiryHeight) > 100 return (latestHeight.value - expiryHeight) > 100
} }
fun PendingTransaction.isMarkedForDeletion(): Boolean { fun PendingTransaction.isMarkedForDeletion(): Boolean {
return rawTransactionId == null && (errorCode ?: 0) == -9090 return rawTransactionId == null &&
(errorCode ?: 0) == PersistentTransactionManager.SAFE_TO_DELETE_ERROR_CODE
} }
@Suppress("MagicNumber")
fun PendingTransaction.isSafeToDiscard(): Boolean { fun PendingTransaction.isSafeToDiscard(): Boolean {
// invalid dates shouldn't happen or should be temporary // invalid dates shouldn't happen or should be temporary
if (createTime < 0) return false if (createTime < 0) return false

View File

@ -68,7 +68,10 @@ sealed class CompactBlockProcessorException(message: String, cause: Throwable? =
"No data db file found at path $path. Verify " + "No data db file found at path $path. Verify " +
"that the data DB has been initialized via `rustBackend.initDataDb(path)`" "that the data DB has been initialized via `rustBackend.initDataDb(path)`"
) )
open class ConfigurationException(message: String, cause: Throwable?) : CompactBlockProcessorException(message, cause) open class ConfigurationException(
message: String,
cause: Throwable?
) : CompactBlockProcessorException(message, cause)
class FileInsteadOfPath(fileName: String) : ConfigurationException( class FileInsteadOfPath(fileName: String) : ConfigurationException(
"Invalid Path: the given path appears to be a" + "Invalid Path: the given path appears to be a" +
" file name instead of a path: $fileName. The RustBackend expects the absolutePath to the database rather" + " file name instead of a path: $fileName. The RustBackend expects the absolutePath to the database rather" +
@ -87,7 +90,8 @@ sealed class CompactBlockProcessorException(message: String, cause: Throwable? =
"likely means a block was missed or a reorg was mishandled. See logs for details.", "likely means a block was missed or a reorg was mishandled. See logs for details.",
cause cause
) )
class Disconnected(cause: Throwable? = null) : CompactBlockProcessorException("Disconnected Error. Unable to download blocks due to ${cause?.message}", cause) class Disconnected(cause: Throwable? = null) :
CompactBlockProcessorException("Disconnected Error. Unable to download blocks due to ${cause?.message}", cause)
object Uninitialized : CompactBlockProcessorException( object Uninitialized : CompactBlockProcessorException(
"Cannot process blocks because the wallet has not been" + "Cannot process blocks because the wallet has not been" +
" initialized. Verify that the seed phrase was properly created or imported. If so, then this problem" + " initialized. Verify that the seed phrase was properly created or imported. If so, then this problem" +
@ -97,17 +101,41 @@ sealed class CompactBlockProcessorException(message: String, cause: Throwable? =
"Attempting to scan without an account. This is probably a setup error or a race condition." "Attempting to scan without an account. This is probably a setup error or a race condition."
) )
open class EnhanceTransactionError(message: String, val height: BlockHeight?, cause: Throwable) : CompactBlockProcessorException(message, cause) { open class EnhanceTransactionError(
class EnhanceTxDownloadError(height: BlockHeight?, cause: Throwable) : EnhanceTransactionError("Error while attempting to download a transaction to enhance", height, cause) message: String,
class EnhanceTxDecryptError(height: BlockHeight?, cause: Throwable) : EnhanceTransactionError("Error while attempting to decrypt and store a transaction to enhance", height, cause) val height: BlockHeight?,
cause: Throwable
) : CompactBlockProcessorException(message, cause) {
class EnhanceTxDownloadError(
height: BlockHeight?,
cause: Throwable
) : EnhanceTransactionError(
"Error while attempting to download a transaction to enhance",
height,
cause
)
class EnhanceTxDecryptError(
height: BlockHeight?,
cause: Throwable
) : EnhanceTransactionError(
"Error while attempting to decrypt and store a transaction to enhance",
height,
cause
)
} }
class MismatchedNetwork(clientNetwork: String?, serverNetwork: String?) : CompactBlockProcessorException( class MismatchedNetwork(clientNetwork: String?, serverNetwork: String?) : CompactBlockProcessorException(
"Incompatible server: this client expects a server using $clientNetwork but it was $serverNetwork! Try updating the client or switching servers." "Incompatible server: this client expects a server using $clientNetwork but it was $serverNetwork! Try " +
"updating the client or switching servers."
) )
class MismatchedBranch(clientBranch: String?, serverBranch: String?, networkName: String?) : CompactBlockProcessorException( class MismatchedBranch(
"Incompatible server: this client expects a server following consensus branch $clientBranch on $networkName but it was $serverBranch! Try updating the client or switching servers." clientBranch: String?,
serverBranch: String?,
networkName: String?
) : CompactBlockProcessorException(
"Incompatible server: this client expects a server following consensus branch $clientBranch on $networkName " +
"but it was $serverBranch! Try updating the client or switching servers."
) )
} }
@ -123,7 +151,10 @@ sealed class BirthdayException(message: String, cause: Throwable? = null) : SdkE
class MissingBirthdayFilesException(directory: String) : BirthdayException( class MissingBirthdayFilesException(directory: String) : BirthdayException(
"Cannot initialize wallet because no birthday files were found in the $directory directory." "Cannot initialize wallet because no birthday files were found in the $directory directory."
) )
class ExactBirthdayNotFoundException internal constructor(birthday: BlockHeight, nearestMatch: Checkpoint? = null) : BirthdayException( class ExactBirthdayNotFoundException internal constructor(
birthday: BlockHeight,
nearestMatch: Checkpoint? = null
) : BirthdayException(
"Unable to find birthday that exactly matches $birthday.${ "Unable to find birthday that exactly matches $birthday.${
if (nearestMatch != null) { if (nearestMatch != null) {
" An exact match was request but the nearest match found was ${nearestMatch.height}." " An exact match was request but the nearest match found was ${nearestMatch.height}."
@ -207,7 +238,11 @@ sealed class LightWalletException(message: String, cause: Throwable? = null) : S
) )
open class ChangeServerException(message: String, cause: Throwable? = null) : SdkException(message, cause) { open class ChangeServerException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
class ChainInfoNotMatching(val propertyNames: String, val expectedInfo: Service.LightdInfo, val actualInfo: Service.LightdInfo) : ChangeServerException( class ChainInfoNotMatching(
val propertyNames: String,
val expectedInfo: Service.LightdInfo,
val actualInfo: Service.LightdInfo
) : ChangeServerException(
"Server change error: the $propertyNames values did not match." "Server change error: the $propertyNames values did not match."
) )
class StatusException(val status: Status, cause: Throwable? = null) : SdkException(status.toMessage(), cause) { class StatusException(val status: Status, cause: Throwable? = null) : SdkException(status.toMessage(), cause) {
@ -215,7 +250,8 @@ sealed class LightWalletException(message: String, cause: Throwable? = null) : S
private fun Status.toMessage(): String { private fun Status.toMessage(): String {
return when (this.code) { return when (this.code) {
UNAVAILABLE -> { UNAVAILABLE -> {
"Error: the new server is unavailable. Verify that the host and port are correct. Failed with $this" "Error: the new server is unavailable. Verify that the host and port are correct. Failed " +
"with $this"
} }
else -> "Changing servers failed with status $this" else -> "Changing servers failed with status $this"
} }
@ -234,15 +270,14 @@ sealed class TransactionEncoderException(message: String, cause: Throwable? = nu
"Cannot send funds due to missing spend or output params and attempting to download them failed." "Cannot send funds due to missing spend or output params and attempting to download them failed."
) )
class TransactionNotFoundException(transactionId: Long) : TransactionEncoderException( class TransactionNotFoundException(transactionId: Long) : TransactionEncoderException(
"Unable to find transactionId " + "Unable to find transactionId $transactionId in the repository. This means the wallet created a transaction " +
"$transactionId in the repository. This means the wallet created a transaction and then returned a row ID " + "and then returned a row ID that does not actually exist. This is a scenario where the wallet should " +
"that does not actually exist. This is a scenario where the wallet should have thrown an exception but failed " + "have thrown an exception but failed to do so."
"to do so."
) )
class TransactionNotEncodedException(transactionId: Long) : TransactionEncoderException( class TransactionNotEncodedException(transactionId: Long) : TransactionEncoderException(
"The transaction returned by the wallet," + "The transaction returned by the wallet," +
" with id $transactionId, does not have any raw data. This is a scenario where the wallet should have thrown" + " with id $transactionId, does not have any raw data. This is a scenario where the wallet should have " +
" an exception but failed to do so." "thrown an exception but failed to do so."
) )
class IncompleteScanException(lastScannedHeight: BlockHeight) : TransactionEncoderException( class IncompleteScanException(lastScannedHeight: BlockHeight) : TransactionEncoderException(
"Cannot" + "Cannot" +

View File

@ -4,13 +4,19 @@ import cash.z.ecc.android.sdk.model.BlockHeight
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
class BatchMetrics(val range: ClosedRange<BlockHeight>, val batchSize: Int, private val onMetricComplete: ((BatchMetrics, Boolean) -> Unit)? = null) { class BatchMetrics(
val range: ClosedRange<BlockHeight>,
val batchSize: Int,
private val onMetricComplete: ((BatchMetrics, Boolean) -> Unit)? = null
) {
private var completedBatches = 0 private var completedBatches = 0
private var rangeStartTime = 0L private var rangeStartTime = 0L
private var batchStartTime = 0L private var batchStartTime = 0L
private var batchEndTime = 0L private var batchEndTime = 0L
private var rangeSize = range.endInclusive.value - range.start.value + 1 private var rangeSize = range.endInclusive.value - range.start.value + 1
private fun now() = System.currentTimeMillis() private fun now() = System.currentTimeMillis()
@Suppress("MagicNumber")
private fun ips(blocks: Long, time: Long) = 1000.0f * blocks / time private fun ips(blocks: Long, time: Long) = 1000.0f * blocks / time
val isComplete get() = completedBatches * batchSize >= rangeSize val isComplete get() = completedBatches * batchSize >= rangeSize

View File

@ -1,14 +1,17 @@
package cash.z.ecc.android.sdk.ext package cash.z.ecc.android.sdk.ext
import java.util.Locale
fun ByteArray.toHex(): String { fun ByteArray.toHex(): String {
val sb = StringBuilder(size * 2) val sb = StringBuilder(size * 2)
for (b in this) for (b in this) {
sb.append(String.format("%02x", b)) sb.append(String.format(Locale.ROOT, "%02x", b))
}
return sb.toString() return sb.toString()
} }
// Not used within the SDK, but is used by the Wallet app // Not used within the SDK, but is used by the Wallet app
@Suppress("unused") @Suppress("unused", "MagicNumber")
fun String.fromHex(): ByteArray { fun String.fromHex(): ByteArray {
val len = length val len = length
val data = ByteArray(len / 2) val data = ByteArray(len / 2)

View File

@ -7,8 +7,11 @@ import java.util.Locale
* omitted since this is not the source of truth for branch information but rather a tool for * omitted since this is not the source of truth for branch information but rather a tool for
* printing that information to users. * printing that information to users.
*/ */
@Suppress("MagicNumber")
enum class ConsensusBranchId(val displayName: String, val id: Long, val hexId: String) { enum class ConsensusBranchId(val displayName: String, val id: Long, val hexId: String) {
// TODO: see if we can find a way to not rely on this separate source of truth (either stop converting from hex to display name in the apps or use Rust to get this info) // TODO [#679]: see if we can find a way to not rely on this separate source of truth (either stop converting from
// hex to display name in the apps or use Rust to get this info)
// TODO [#679]: https://github.com/zcash/zcash-android-wallet-sdk/issues/679
SPROUT("Sprout", 0, "0"), SPROUT("Sprout", 0, "0"),
OVERWINTER("Overwinter", 0x5ba8_1b19, "5ba81b19"), OVERWINTER("Overwinter", 0x5ba8_1b19, "5ba81b19"),
SAPLING("Sapling", 0x76b8_09bb, "76b809bb"), SAPLING("Sapling", 0x76b8_09bb, "76b809bb"),

View File

@ -1,9 +1,10 @@
@file:Suppress("NOTHING_TO_INLINE") @file:Suppress("TooManyFunctions", "MatchingDeclarationName")
package cash.z.ecc.android.sdk.ext package cash.z.ecc.android.sdk.ext
import cash.z.ecc.android.sdk.ext.Conversions.USD_FORMATTER import cash.z.ecc.android.sdk.ext.Conversions.USD_FORMATTER
import cash.z.ecc.android.sdk.ext.Conversions.ZEC_FORMATTER import cash.z.ecc.android.sdk.ext.Conversions.ZEC_FORMATTER
import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.model.Zatoshi import cash.z.ecc.android.sdk.model.Zatoshi
import java.math.BigDecimal import java.math.BigDecimal
import java.math.MathContext import java.math.MathContext
@ -18,8 +19,10 @@ import java.util.Locale
* accurately rounded values to the user. * accurately rounded values to the user.
*/ */
// TODO: provide a dynamic way to configure this globally for the SDK // TODO [#678]: provide a dynamic way to configure this globally for the SDK
// For now, just make these vars so at least they could be modified in one place // For now, just make these vars so at least they could be modified in one place
// TODO [#678]: https://github.com/zcash/zcash-android-wallet-sdk/issues/678
@Suppress("MagicNumber")
object Conversions { object Conversions {
var ONE_ZEC_IN_ZATOSHI = BigDecimal(Zatoshi.ZATOSHI_PER_ZEC, MathContext.DECIMAL128) var ONE_ZEC_IN_ZATOSHI = BigDecimal(Zatoshi.ZATOSHI_PER_ZEC, MathContext.DECIMAL128)
var ZEC_FORMATTER = NumberFormat.getInstance(Locale.getDefault()).apply { var ZEC_FORMATTER = NumberFormat.getInstance(Locale.getDefault()).apply {
@ -47,7 +50,7 @@ object Conversions {
* @return this Zatoshi value represented as ZEC, in a string with at least [minDecimals] and at * @return this Zatoshi value represented as ZEC, in a string with at least [minDecimals] and at
* most [maxDecimals] * most [maxDecimals]
*/ */
inline fun Zatoshi?.convertZatoshiToZecString( fun Zatoshi?.convertZatoshiToZecString(
maxDecimals: Int = ZEC_FORMATTER.maximumFractionDigits, maxDecimals: Int = ZEC_FORMATTER.maximumFractionDigits,
minDecimals: Int = ZEC_FORMATTER.minimumFractionDigits minDecimals: Int = ZEC_FORMATTER.minimumFractionDigits
): String { ): String {
@ -65,7 +68,7 @@ inline fun Zatoshi?.convertZatoshiToZecString(
* @return this Double ZEC value represented as a string with at least [minDecimals] and at most * @return this Double ZEC value represented as a string with at least [minDecimals] and at most
* [maxDecimals]. * [maxDecimals].
*/ */
inline fun Double?.toZecString( fun Double?.toZecString(
maxDecimals: Int = ZEC_FORMATTER.maximumFractionDigits, maxDecimals: Int = ZEC_FORMATTER.maximumFractionDigits,
minDecimals: Int = ZEC_FORMATTER.minimumFractionDigits minDecimals: Int = ZEC_FORMATTER.minimumFractionDigits
): String { ): String {
@ -83,7 +86,7 @@ inline fun Double?.toZecString(
* @return this BigDecimal ZEC value represented as a string with at least [minDecimals] and at most * @return this BigDecimal ZEC value represented as a string with at least [minDecimals] and at most
* [maxDecimals]. * [maxDecimals].
*/ */
inline fun BigDecimal?.toZecString( fun BigDecimal?.toZecString(
maxDecimals: Int = ZEC_FORMATTER.maximumFractionDigits, maxDecimals: Int = ZEC_FORMATTER.maximumFractionDigits,
minDecimals: Int = ZEC_FORMATTER.minimumFractionDigits minDecimals: Int = ZEC_FORMATTER.minimumFractionDigits
): String { ): String {
@ -101,7 +104,7 @@ inline fun BigDecimal?.toZecString(
* @return this Double ZEC value represented as a string with at least [minDecimals] and at most * @return this Double ZEC value represented as a string with at least [minDecimals] and at most
* [maxDecimals], which is 2 by default. Zero is always represented without any decimals. * [maxDecimals], which is 2 by default. Zero is always represented without any decimals.
*/ */
inline fun Double?.toUsdString( fun Double?.toUsdString(
maxDecimals: Int = USD_FORMATTER.maximumFractionDigits, maxDecimals: Int = USD_FORMATTER.maximumFractionDigits,
minDecimals: Int = USD_FORMATTER.minimumFractionDigits minDecimals: Int = USD_FORMATTER.minimumFractionDigits
): String { ): String {
@ -123,7 +126,7 @@ inline fun Double?.toUsdString(
* @return this BigDecimal USD value represented as a string with at least [minDecimals] and at most * @return this BigDecimal USD value represented as a string with at least [minDecimals] and at most
* [maxDecimals], which is 2 by default. * [maxDecimals], which is 2 by default.
*/ */
inline fun BigDecimal?.toUsdString( fun BigDecimal?.toUsdString(
maxDecimals: Int = USD_FORMATTER.maximumFractionDigits, maxDecimals: Int = USD_FORMATTER.maximumFractionDigits,
minDecimals: Int = USD_FORMATTER.minimumFractionDigits minDecimals: Int = USD_FORMATTER.minimumFractionDigits
): String { ): String {
@ -141,7 +144,7 @@ inline fun BigDecimal?.toUsdString(
* *
* @return a currency formatter, appropriate for the default locale. * @return a currency formatter, appropriate for the default locale.
*/ */
inline fun currencyFormatter(maxDecimals: Int, minDecimals: Int): NumberFormat { fun currencyFormatter(maxDecimals: Int, minDecimals: Int): NumberFormat {
return NumberFormat.getInstance(Locale.getDefault()).apply { return NumberFormat.getInstance(Locale.getDefault()).apply {
roundingMode = ZEC_FORMATTER.roundingMode roundingMode = ZEC_FORMATTER.roundingMode
maximumFractionDigits = maxDecimals maximumFractionDigits = maxDecimals
@ -162,7 +165,7 @@ inline fun currencyFormatter(maxDecimals: Int, minDecimals: Int): NumberFormat {
* @return this Long Zatoshi value represented as ZEC using a BigDecimal with the given scale, * @return this Long Zatoshi value represented as ZEC using a BigDecimal with the given scale,
* rounded accurately out to 128 digits. * rounded accurately out to 128 digits.
*/ */
inline fun Zatoshi?.convertZatoshiToZec(scale: Int = ZEC_FORMATTER.maximumFractionDigits): BigDecimal { fun Zatoshi?.convertZatoshiToZec(scale: Int = ZEC_FORMATTER.maximumFractionDigits): BigDecimal {
return BigDecimal(this?.value ?: 0L, MathContext.DECIMAL128).divide( return BigDecimal(this?.value ?: 0L, MathContext.DECIMAL128).divide(
Conversions.ONE_ZEC_IN_ZATOSHI, Conversions.ONE_ZEC_IN_ZATOSHI,
MathContext.DECIMAL128 MathContext.DECIMAL128
@ -176,7 +179,7 @@ inline fun Zatoshi?.convertZatoshiToZec(scale: Int = ZEC_FORMATTER.maximumFracti
* @return this ZEC value represented as Zatoshi, rounded accurately out to 128 digits, in order to * @return this ZEC value represented as Zatoshi, rounded accurately out to 128 digits, in order to
* minimize cumulative errors when applied repeatedly over a sequence of calculations. * minimize cumulative errors when applied repeatedly over a sequence of calculations.
*/ */
inline fun BigDecimal?.convertZecToZatoshi(): Zatoshi { fun BigDecimal?.convertZecToZatoshi(): Zatoshi {
if (this == null) return Zatoshi(0L) if (this == null) return Zatoshi(0L)
if (this < BigDecimal.ZERO) { if (this < BigDecimal.ZERO) {
throw IllegalArgumentException( throw IllegalArgumentException(
@ -197,7 +200,7 @@ inline fun BigDecimal?.convertZecToZatoshi(): Zatoshi {
* @return this Double ZEC value converted into a BigDecimal, with the proper rounding mode for use * @return this Double ZEC value converted into a BigDecimal, with the proper rounding mode for use
* with other formatting functions. * with other formatting functions.
*/ */
inline fun Double?.toZec(decimals: Int = ZEC_FORMATTER.maximumFractionDigits): BigDecimal { fun Double?.toZec(decimals: Int = ZEC_FORMATTER.maximumFractionDigits): BigDecimal {
return BigDecimal(this?.toString() ?: "0.0", MathContext.DECIMAL128).setScale( return BigDecimal(this?.toString() ?: "0.0", MathContext.DECIMAL128).setScale(
decimals, decimals,
ZEC_FORMATTER.roundingMode ZEC_FORMATTER.roundingMode
@ -214,7 +217,7 @@ inline fun Double?.toZec(decimals: Int = ZEC_FORMATTER.maximumFractionDigits): B
* @return this Double ZEC value converted into Zatoshi, with proper rounding and precision by * @return this Double ZEC value converted into Zatoshi, with proper rounding and precision by
* leveraging an intermediate BigDecimal object. * leveraging an intermediate BigDecimal object.
*/ */
inline fun Double?.convertZecToZatoshi(decimals: Int = ZEC_FORMATTER.maximumFractionDigits): Zatoshi { fun Double?.convertZecToZatoshi(decimals: Int = ZEC_FORMATTER.maximumFractionDigits): Zatoshi {
return this.toZec(decimals).convertZecToZatoshi() return this.toZec(decimals).convertZecToZatoshi()
} }
@ -227,7 +230,7 @@ inline fun Double?.convertZecToZatoshi(decimals: Int = ZEC_FORMATTER.maximumFrac
* *
* @return this BigDecimal ZEC adjusted to the default scale and rounding mode. * @return this BigDecimal ZEC adjusted to the default scale and rounding mode.
*/ */
inline fun BigDecimal?.toZec(decimals: Int = ZEC_FORMATTER.maximumFractionDigits): BigDecimal { fun BigDecimal?.toZec(decimals: Int = ZEC_FORMATTER.maximumFractionDigits): BigDecimal {
return (this ?: BigDecimal.ZERO).setScale(decimals, ZEC_FORMATTER.roundingMode) return (this ?: BigDecimal.ZERO).setScale(decimals, ZEC_FORMATTER.roundingMode)
} }
@ -240,7 +243,7 @@ inline fun BigDecimal?.toZec(decimals: Int = ZEC_FORMATTER.maximumFractionDigits
* *
* @return this Double USD value converted into a BigDecimal, with proper rounding and precision. * @return this Double USD value converted into a BigDecimal, with proper rounding and precision.
*/ */
inline fun Double?.toUsd(decimals: Int = USD_FORMATTER.maximumFractionDigits): BigDecimal { fun Double?.toUsd(decimals: Int = USD_FORMATTER.maximumFractionDigits): BigDecimal {
return BigDecimal(this?.toString() ?: "0.0", MathContext.DECIMAL128).setScale( return BigDecimal(this?.toString() ?: "0.0", MathContext.DECIMAL128).setScale(
decimals, decimals,
USD_FORMATTER.roundingMode USD_FORMATTER.roundingMode
@ -256,7 +259,7 @@ inline fun Double?.toUsd(decimals: Int = USD_FORMATTER.maximumFractionDigits): B
* *
* @return this BigDecimal USD value converted into USD, with proper rounding and precision. * @return this BigDecimal USD value converted into USD, with proper rounding and precision.
*/ */
inline fun BigDecimal?.toUsd(decimals: Int = USD_FORMATTER.maximumFractionDigits): BigDecimal { fun BigDecimal?.toUsd(decimals: Int = USD_FORMATTER.maximumFractionDigits): BigDecimal {
return (this ?: BigDecimal.ZERO).setScale(decimals, USD_FORMATTER.roundingMode) return (this ?: BigDecimal.ZERO).setScale(decimals, USD_FORMATTER.roundingMode)
} }
@ -268,7 +271,7 @@ inline fun BigDecimal?.toUsd(decimals: Int = USD_FORMATTER.maximumFractionDigits
* *
* @return this BigDecimal USD value converted into USD, with proper rounding and precision. * @return this BigDecimal USD value converted into USD, with proper rounding and precision.
*/ */
inline fun BigDecimal?.convertZecToUsd(zecPrice: BigDecimal): BigDecimal { fun BigDecimal?.convertZecToUsd(zecPrice: BigDecimal): BigDecimal {
if (this == null) return BigDecimal.ZERO if (this == null) return BigDecimal.ZERO
if (this < BigDecimal.ZERO) { if (this < BigDecimal.ZERO) {
throw IllegalArgumentException( throw IllegalArgumentException(
@ -287,7 +290,7 @@ inline fun BigDecimal?.convertZecToUsd(zecPrice: BigDecimal): BigDecimal {
* *
* @return this BigDecimal USD value converted into ZEC, with proper rounding and precision. * @return this BigDecimal USD value converted into ZEC, with proper rounding and precision.
*/ */
inline fun BigDecimal?.convertUsdToZec(zecPrice: BigDecimal): BigDecimal { fun BigDecimal?.convertUsdToZec(zecPrice: BigDecimal): BigDecimal {
if (this == null) return BigDecimal.ZERO if (this == null) return BigDecimal.ZERO
if (this < BigDecimal.ZERO) { if (this < BigDecimal.ZERO) {
throw IllegalArgumentException( throw IllegalArgumentException(
@ -310,7 +313,7 @@ inline fun BigDecimal?.convertUsdToZec(zecPrice: BigDecimal): BigDecimal {
* @return this BigDecimal value converted from one currency into the other, based on the given * @return this BigDecimal value converted from one currency into the other, based on the given
* price. * price.
*/ */
inline fun BigDecimal.convertCurrency(zecPrice: BigDecimal, isUsd: Boolean): BigDecimal { fun BigDecimal.convertCurrency(zecPrice: BigDecimal, isUsd: Boolean): BigDecimal {
return if (isUsd) { return if (isUsd) {
this.convertUsdToZec(zecPrice) this.convertUsdToZec(zecPrice)
} else { } else {
@ -323,15 +326,19 @@ inline fun BigDecimal.convertCurrency(zecPrice: BigDecimal, isUsd: Boolean): Big
* *
* @return this string as a BigDecimal or null when parsing fails. * @return this string as a BigDecimal or null when parsing fails.
*/ */
inline fun String?.safelyConvertToBigDecimal(): BigDecimal? { fun String?.safelyConvertToBigDecimal(): BigDecimal? {
if (this.isNullOrEmpty()) return BigDecimal.ZERO if (this.isNullOrEmpty()) {
return try { return BigDecimal.ZERO
// ignore commas and whitespace
var sanitizedInput = this.filter { it.isDigit() or (it == '.') }
BigDecimal.ZERO.max(BigDecimal(sanitizedInput, MathContext.DECIMAL128))
} catch (t: Throwable) {
return null
} }
val result = try {
// ignore commas and whitespace
val sanitizedInput = this.filter { it.isDigit() or (it == '.') }
BigDecimal.ZERO.max(BigDecimal(sanitizedInput, MathContext.DECIMAL128))
} catch (nfe: NumberFormatException) {
twig("Exception while converting String to BigDecimal: ${nfe.message} caused by: ${nfe.cause}")
null
}
return result
} }
/** /**
@ -343,7 +350,7 @@ inline fun String?.safelyConvertToBigDecimal(): BigDecimal? {
* @return the abbreviated string unless the string is too short, in which case the original string * @return the abbreviated string unless the string is too short, in which case the original string
* is returned. * is returned.
*/ */
inline fun String.toAbbreviatedAddress(startLength: Int = 8, endLength: Int = 8) = fun String.toAbbreviatedAddress(startLength: Int = 8, endLength: Int = 8) =
if (length > startLength + endLength) "${take(startLength)}${takeLast(endLength)}" else this if (length > startLength + endLength) "${take(startLength)}${takeLast(endLength)}" else this
/** /**
@ -355,7 +362,7 @@ inline fun String.toAbbreviatedAddress(startLength: Int = 8, endLength: Int = 8)
* *
* @return the masked version of this string, typically for use in logs. * @return the masked version of this string, typically for use in logs.
*/ */
internal inline fun String.masked(addressCharsToShow: Int = 4): String = internal fun String.masked(addressCharsToShow: Int = 4): String =
if (startsWith("ztest") || startsWith("zs")) "****${takeLast(addressCharsToShow)}" if (startsWith("ztest") || startsWith("zs")) "****${takeLast(addressCharsToShow)}"
else "***masked***" else "***masked***"
@ -364,4 +371,4 @@ internal inline fun String.masked(addressCharsToShow: Int = 4): String =
* *
* @return true when this function starts with 'z' rather than 't'. * @return true when this function starts with 'z' rather than 't'.
*/ */
inline fun String?.isShielded() = this != null && startsWith('z') fun String?.isShielded() = this != null && startsWith('z')

View File

@ -8,6 +8,7 @@ import cash.z.ecc.android.sdk.model.Zatoshi
* becomes easier to reduce privacy by segmenting the anonymity set of users, particularly as it * becomes easier to reduce privacy by segmenting the anonymity set of users, particularly as it
* relates to network requests. * relates to network requests.
*/ */
@Suppress("MagicNumber")
object ZcashSdk { object ZcashSdk {
/** /**
@ -26,8 +27,8 @@ object ZcashSdk {
const val MAX_MEMO_SIZE = 512 const val MAX_MEMO_SIZE = 512
/** /**
* The amount of blocks ahead of the current height where new transactions are set to expire. This value is controlled * The amount of blocks ahead of the current height where new transactions are set to expire. This value is
* by the rust backend but it is helpful to know what it is set to and should be kept in sync. * controlled by the rust backend but it is helpful to know what it is set to and should be kept in sync.
*/ */
const val EXPIRY_OFFSET = 20 const val EXPIRY_OFFSET = 20
@ -62,14 +63,14 @@ object ZcashSdk {
const val RETRIES = 5 const val RETRIES = 5
/** /**
* The default maximum amount of time to wait during retry backoff intervals. Failed loops will never wait longer than * The default maximum amount of time to wait during retry backoff intervals. Failed loops will never wait longer
* this before retyring. * than this before retyring.
*/ */
const val MAX_BACKOFF_INTERVAL = 600_000L const val MAX_BACKOFF_INTERVAL = 600_000L
/** /**
* Default number of blocks to rewind when a chain reorg is detected. This should be large enough to recover from the * Default number of blocks to rewind when a chain reorg is detected. This should be large enough to recover from
* reorg but smaller than the theoretical max reorg size of 100. * the reorg but smaller than the theoretical max reorg size of 100.
*/ */
const val REWIND_DISTANCE = 10 const val REWIND_DISTANCE = 10
@ -95,5 +96,19 @@ object ZcashSdk {
*/ */
const val DEFAULT_SHIELD_FUNDS_MEMO_PREFIX = "shielding:" const val DEFAULT_SHIELD_FUNDS_MEMO_PREFIX = "shielding:"
/**
* The default alias used as part of a file name for the preferences and databases. This
* enables multiple wallets to exist on one device, which is also helpful for sweeping funds.
*/
const val DEFAULT_ALIAS: String = "zcash_sdk" const val DEFAULT_ALIAS: String = "zcash_sdk"
/**
* The minimum alias length to be valid for our use.
*/
const val ALIAS_MIN_LENGTH: Int = 1
/**
* The maximum alias length to be valid for our use.
*/
const val ALIAS_MAX_LENGTH: Int = 99
} }

View File

@ -13,6 +13,7 @@ import okio.buffer
import okio.sink import okio.sink
import java.io.File import java.io.File
@Suppress("UtilityClassWithPublicConstructor")
class SaplingParamTool { class SaplingParamTool {
companion object { companion object {
@ -34,6 +35,7 @@ class SaplingParamTool {
} }
} }
if (hadError) { if (hadError) {
@Suppress("TooGenericExceptionCaught")
try { try {
Bush.trunk.twigTask("attempting to download missing params") { Bush.trunk.twigTask("attempting to download missing params") {
fetchParams(destinationDir) fetchParams(destinationDir)
@ -131,7 +133,8 @@ class SaplingParamTool {
* @return an http client suitable for downloading params data. * @return an http client suitable for downloading params data.
*/ */
private fun createHttpClient(): OkHttpClient { private fun createHttpClient(): OkHttpClient {
// TODO: add logging and timeouts // TODO [#686]: add logging and timeouts
// TODO [#686]: https://github.com/zcash/zcash-android-wallet-sdk/issues/686
return OkHttpClient() return OkHttpClient()
} }
} }

View File

@ -1,6 +1,7 @@
@file:Suppress("NOTHING_TO_INLINE") @file:Suppress("NOTHING_TO_INLINE", "MagicNumber")
package cash.z.ecc.android.sdk.internal package cash.z.ecc.android.sdk.internal
import java.util.Locale
import java.util.concurrent.CopyOnWriteArraySet import java.util.concurrent.CopyOnWriteArraySet
import kotlin.math.roundToLong import kotlin.math.roundToLong
@ -95,12 +96,28 @@ inline fun twig(t: Throwable) = t.stackTraceToString().lines().forEach {
/** /**
* Times a tiny log. * Times a tiny log.
*/ */
inline fun <R> twig(logMessage: String, priority: Int = 0, block: () -> R): R = Bush.trunk.twig(logMessage, priority, block) inline fun <R> twig(
logMessage: String,
priority: Int = 0,
block: () -> R
): R = Bush.trunk.twig(
logMessage,
priority,
block
)
/** /**
* Meticulously times a tiny task. * Meticulously times a tiny task.
*/ */
inline fun <R> twigTask(logMessage: String, priority: Int = 0, block: () -> R): R = Bush.trunk.twigTask(logMessage, priority, block) inline fun <R> twigTask(
logMessage: String,
priority: Int = 0,
block: () -> R
): R = Bush.trunk.twigTask(
logMessage,
priority,
block
)
/** /**
* A tiny log that does nothing. No one hears this twig fall in the woods. * A tiny log that does nothing. No one hears this twig fall in the woods.
@ -145,7 +162,11 @@ open class TroubleshootingTwig(
*/ */
fun spiffy(stackFrame: Int = 4, tag: String = "@TWIG"): (String) -> String = { logMessage: String -> fun spiffy(stackFrame: Int = 4, tag: String = "@TWIG"): (String) -> String = { logMessage: String ->
val stack = Thread.currentThread().stackTrace[stackFrame] val stack = Thread.currentThread().stackTrace[stackFrame]
val time = String.format("$tag %1\$tD %1\$tI:%1\$tM:%1\$tS.%1\$tN", System.currentTimeMillis()) val time = String.format(
Locale.ENGLISH,
"$tag %1\$tD %1\$tI:%1\$tM:%1\$tS.%1\$tN",
System.currentTimeMillis()
)
val className = stack.className.split(".").lastOrNull()?.split("\$")?.firstOrNull() val className = stack.className.split(".").lastOrNull()?.split("\$")?.firstOrNull()
val tags = Bush.leaves.joinToString(" #", "#") val tags = Bush.leaves.joinToString(" #", "#")
"$time[$className:${stack.lineNumber}]($tags) $logMessage" "$time[$className:${stack.lineNumber}]($tags) $logMessage"

View File

@ -56,7 +56,8 @@ open class CompactBlockDownloader private constructor(val compactBlockStore: Com
* @param height the height to which the data will rewind. * @param height the height to which the data will rewind.
*/ */
suspend fun rewindToHeight(height: BlockHeight) = suspend fun rewindToHeight(height: BlockHeight) =
// TODO: cancel anything in flight // TODO [#685]: cancel anything in flight
// TODO [#685]: https://github.com/zcash/zcash-android-wallet-sdk/issues/685
compactBlockStore.rewindTo(height) compactBlockStore.rewindTo(height)
/** /**
@ -80,7 +81,7 @@ open class CompactBlockDownloader private constructor(val compactBlockStore: Com
try { try {
result = lightWalletService.getServerInfo() result = lightWalletService.getServerInfo()
} catch (e: StatusRuntimeException) { } catch (e: StatusRuntimeException) {
retryUpTo(6) { retryUpTo(GET_SERVER_INFO_RETRIES) {
twig("WARNING: reconnecting to service in response to failure (retry #${it + 1}): $e") twig("WARNING: reconnecting to service in response to failure (retry #${it + 1}): $e")
lightWalletService.reconnect() lightWalletService.reconnect()
result = lightWalletService.getServerInfo() result = lightWalletService.getServerInfo()
@ -93,6 +94,7 @@ open class CompactBlockDownloader private constructor(val compactBlockStore: Com
newService: LightWalletService, newService: LightWalletService,
errorHandler: (Throwable) -> Unit = { throw it } errorHandler: (Throwable) -> Unit = { throw it }
) = withContext(IO) { ) = withContext(IO) {
@Suppress("TooGenericExceptionCaught")
try { try {
val existing = lightWalletService.getServerInfo() val existing = lightWalletService.getServerInfo()
val new = newService.getServerInfo() val new = newService.getServerInfo()
@ -139,6 +141,7 @@ open class CompactBlockDownloader private constructor(val compactBlockStore: Com
// //
private suspend fun CoroutineScope.gracefullyShutdown(service: LightWalletService) = launch { private suspend fun CoroutineScope.gracefullyShutdown(service: LightWalletService) = launch {
@Suppress("MagicNumber")
delay(2_000L) delay(2_000L)
tryWarn("Warning: error while shutting down service") { tryWarn("Warning: error while shutting down service") {
service.shutdown() service.shutdown()
@ -160,4 +163,8 @@ open class CompactBlockDownloader private constructor(val compactBlockStore: Com
it.add("chainName") it.add("chainName")
} }
} }
companion object {
private const val GET_SERVER_INFO_RETRIES = 6
}
} }

View File

@ -54,6 +54,7 @@ abstract class DerivedDataDb : RoomDatabase() {
// Migrations // Migrations
// //
@Suppress("MagicNumber")
companion object { companion object {
val MIGRATION_3_4 = object : Migration(3, 4) { val MIGRATION_3_4 = object : Migration(3, 4) {
@ -248,6 +249,7 @@ interface AccountDao {
* whether transactions are mined. * whether transactions are mined.
*/ */
@Dao @Dao
@Suppress("TooManyFunctions")
interface TransactionDao { interface TransactionDao {
@Query("SELECT COUNT(id_tx) FROM transactions") @Query("SELECT COUNT(id_tx) FROM transactions")
suspend fun count(): Int suspend fun count(): Int
@ -365,7 +367,8 @@ interface TransactionDao {
ON transactions.id_tx = sent_notes.tx ON transactions.id_tx = sent_notes.tx
LEFT JOIN blocks LEFT JOIN blocks
ON transactions.block = blocks.height ON transactions.block = blocks.height
/* we want all received txs except those that are change and all sent transactions (even those that haven't been mined yet). Note: every entry in the 'send_notes' table has a non-null value for 'address' */ /* we want all received txs except those that are change and all sent transactions (even those that haven't been
mined yet). Note: every entry in the 'send_notes' table has a non-null value for 'address' */
WHERE ( sent_notes.address IS NULL WHERE ( sent_notes.address IS NULL
AND received_notes.is_change != 1 ) AND received_notes.is_change != 1 )
OR sent_notes.address IS NOT NULL OR sent_notes.address IS NOT NULL
@ -421,7 +424,11 @@ interface TransactionDao {
LIMIT :limit LIMIT :limit
""" """
) )
suspend fun findAllTransactionsByRange(blockRangeStart: Long, blockRangeEnd: Long = blockRangeStart, limit: Int = Int.MAX_VALUE): List<ConfirmedTransaction> suspend fun findAllTransactionsByRange(
blockRangeStart: Long,
blockRangeEnd: Long = blockRangeStart,
limit: Int = Int.MAX_VALUE
): List<ConfirmedTransaction>
// Experimental: cleanup cancelled transactions // Experimental: cleanup cancelled transactions
// This should probably be a rust call but there's not a lot of bandwidth for this // This should probably be a rust call but there's not a lot of bandwidth for this
@ -431,6 +438,7 @@ interface TransactionDao {
@Transaction @Transaction
suspend fun cleanupCancelledTx(rawTransactionId: ByteArray): Boolean { suspend fun cleanupCancelledTx(rawTransactionId: ByteArray): Boolean {
var success = false var success = false
@Suppress("TooGenericExceptionCaught")
try { try {
var hasInitialMatch = false var hasInitialMatch = false
twig("[cleanup] cleanupCancelledTx starting...") twig("[cleanup] cleanupCancelledTx starting...")
@ -452,6 +460,7 @@ interface TransactionDao {
@Transaction @Transaction
suspend fun removeInvalidOutboundTransaction(transactionId: Long): Boolean { suspend fun removeInvalidOutboundTransaction(transactionId: Long): Boolean {
var success = false var success = false
@Suppress("TooGenericExceptionCaught")
try { try {
twig("[cleanup] removing invalid transactionId:$transactionId") twig("[cleanup] removing invalid transactionId:$transactionId")
val result = unspendTransactionNotes(transactionId) val result = unspendTransactionNotes(transactionId)
@ -461,7 +470,8 @@ interface TransactionDao {
deleteSentNote(noteId) deleteSentNote(noteId)
} }
// delete the UTXOs because these are effectively cached and we don't have a good way of knowing whether they're spent // delete the UTXOs because these are effectively cached and we don't have a good way of knowing whether
// they're spent
deleteUtxos(transactionId).let { count -> deleteUtxos(transactionId).let { count ->
twig("[cleanup] removed $count UTXOs matching transactionId $transactionId") twig("[cleanup] removed $count UTXOs matching transactionId $transactionId")
} }

View File

@ -40,6 +40,7 @@ abstract class PendingTransactionDb : RoomDatabase() {
* Data access object providing crud for pending transactions. * Data access object providing crud for pending transactions.
*/ */
@Dao @Dao
@Suppress("TooManyFunctions")
interface PendingTransactionDao { interface PendingTransactionDao {
@Insert(onConflict = OnConflictStrategy.ABORT) @Insert(onConflict = OnConflictStrategy.ABORT)
suspend fun create(transaction: PendingTransactionEntity): Long suspend fun create(transaction: PendingTransactionEntity): Long
@ -72,7 +73,10 @@ interface PendingTransactionDao {
@Query("UPDATE pending_transactions SET minedHeight = :minedHeight WHERE id = :id") @Query("UPDATE pending_transactions SET minedHeight = :minedHeight WHERE id = :id")
suspend fun updateMinedHeight(id: Long, minedHeight: Long) suspend fun updateMinedHeight(id: Long, minedHeight: Long)
@Query("UPDATE pending_transactions SET raw = :raw, rawTransactionId = :rawTransactionId, expiryHeight = :expiryHeight WHERE id = :id") @Query(
"UPDATE pending_transactions SET raw = :raw, rawTransactionId = :rawTransactionId," +
" expiryHeight = :expiryHeight WHERE id = :id"
)
suspend fun updateEncoding(id: Long, raw: ByteArray, rawTransactionId: ByteArray, expiryHeight: Long?) suspend fun updateEncoding(id: Long, raw: ByteArray, rawTransactionId: ByteArray, expiryHeight: Long?)
@Query("UPDATE pending_transactions SET errorMessage = :errorMessage, errorCode = :errorCode WHERE id = :id") @Query("UPDATE pending_transactions SET errorMessage = :errorMessage, errorCode = :errorCode WHERE id = :id")

View File

@ -2,10 +2,13 @@
package cash.z.ecc.android.sdk.internal.ext package cash.z.ecc.android.sdk.internal.ext
import java.util.Locale
internal fun ByteArray.toHexReversed(): String { internal fun ByteArray.toHexReversed(): String {
val sb = StringBuilder(size * 2) val sb = StringBuilder(size * 2)
var i = size - 1 var i = size - 1
while (i >= 0) while (i >= 0) {
sb.append(String.format("%02x", this[i--])) sb.append(String.format(Locale.ROOT, "%02x", this[i--]))
}
return sb.toString() return sb.toString()
} }

View File

@ -2,6 +2,7 @@ package cash.z.ecc.android.sdk.internal.ext
import cash.z.ecc.android.sdk.internal.twig import cash.z.ecc.android.sdk.internal.twig
@Suppress("SwallowedException", "TooGenericExceptionCaught")
internal inline fun <R> tryNull(block: () -> R): R? { internal inline fun <R> tryNull(block: () -> R): R? {
return try { return try {
block() block()
@ -17,6 +18,7 @@ internal inline fun <R> tryNull(block: () -> R): R? {
* @param ifContains only convert an exception into a warning if it contains the given text * @param ifContains only convert an exception into a warning if it contains the given text
* @param unlessContains convert all exceptions into warnings unless they contain the given text * @param unlessContains convert all exceptions into warnings unless they contain the given text
*/ */
@Suppress("TooGenericExceptionCaught")
internal inline fun <R> tryWarn( internal inline fun <R> tryWarn(
message: String, message: String,
ifContains: String? = null, ifContains: String? = null,

View File

@ -1,21 +1,25 @@
package cash.z.ecc.android.sdk.internal.ext package cash.z.ecc.android.sdk.internal.ext
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import android.provider.Settings import android.provider.Settings
import cash.z.ecc.android.sdk.internal.twig
import kotlin.properties.ReadOnlyProperty import kotlin.properties.ReadOnlyProperty
import kotlin.properties.ReadWriteProperty import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
@Deprecated(message = InsecureWarning.message) @Deprecated(message = InsecureWarning.message)
class SampleSpendingKeyProvider(private val seedValue: String) : ReadWriteProperty<Any?, String> { class SampleSpendingKeyProvider(private val seedValue: String) : ReadWriteProperty<Any?, String> {
override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) { override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
twig("Set value called on property: $property, with value: $value.")
} }
override fun getValue(thisRef: Any?, property: KProperty<*>): String { override fun getValue(thisRef: Any?, property: KProperty<*>): String {
// dynamically generating keys, based on seed is out of scope for this sample // dynamically generating keys, based on seed is out of scope for this sample
if (seedValue != "dummyseed") throw IllegalStateException("This sample provider only supports the dummy seed") if (seedValue != "dummyseed") {
error("This sample provider only supports the dummy seed")
}
return "secret-extended-key-test1q0f0urnmqqqqpqxlree5urprcmg9pdgvr2c88qhm862etv65eu84r9zwannpz4g88299xyhv7wf9" + return "secret-extended-key-test1q0f0urnmqqqqpqxlree5urprcmg9pdgvr2c88qhm862etv65eu84r9zwannpz4g88299xyhv7wf9" +
"xkecag653jlwwwyxrymfraqsnz8qfgds70qjammscxxyl7s7p9xz9w906epdpy8ztsjd7ez7phcd5vj7syx68sjskqs8j9lef2uu" + "xkecag653jlwwwyxrymfraqsnz8qfgds70qjammscxxyl7s7p9xz9w906epdpy8ztsjd7ez7phcd5vj7syx68sjskqs8j9lef2uu" +
"acghsh8puuvsy9u25pfvcdznta33qe6xh5lrlnhdkgymnpdug4jm6tpf803cad6tqa9c0ewq9l03fqxatevm97jmuv8u0ccxjews5" "acghsh8puuvsy9u25pfvcdznta33qe6xh5lrlnhdkgymnpdug4jm6tpf803cad6tqa9c0ewq9l03fqxatevm97jmuv8u0ccxjews5"
@ -79,7 +83,7 @@ class BlockingProvider<T>(var value: T, val delay: Long = 5000L) : ReadWriteProp
* https://github.com/scottyab/AESCrypt-Android/blob/master/aescrypt/src/main/java/com/scottyab/aescrypt/AESCrypt.java * https://github.com/scottyab/AESCrypt-Android/blob/master/aescrypt/src/main/java/com/scottyab/aescrypt/AESCrypt.java
* https://github.com/iamMehedi/Secured-Preference-Store * https://github.com/iamMehedi/Secured-Preference-Store
*/ */
@SuppressLint("HardwareIds") @Suppress("HardwareIds", "UtilityClassWithPublicConstructor")
@Deprecated(message = InsecureWarning.message) @Deprecated(message = InsecureWarning.message)
class SeedGenerator { class SeedGenerator {
companion object { companion object {

View File

@ -26,12 +26,15 @@ suspend inline fun retryUpTo(
) { ) {
var failedAttempts = 0 var failedAttempts = 0
while (failedAttempts <= retries) { while (failedAttempts <= retries) {
@Suppress("TooGenericExceptionCaught")
try { try {
block(failedAttempts) block(failedAttempts)
return return
} catch (t: Throwable) { } catch (t: Throwable) {
failedAttempts++ failedAttempts++
if (failedAttempts > retries) throw exceptionWrapper(t) if (failedAttempts > retries) {
throw exceptionWrapper(t)
}
val duration = (initialDelayMillis.toDouble() * Math.pow(2.0, failedAttempts.toDouble() - 1)).toLong() val duration = (initialDelayMillis.toDouble() * Math.pow(2.0, failedAttempts.toDouble() - 1)).toLong()
twig("failed due to $t retrying ($failedAttempts/$retries) in ${duration}s...") twig("failed due to $t retrying ($failedAttempts/$retries) in ${duration}s...")
delay(duration) delay(duration)
@ -52,6 +55,7 @@ suspend inline fun retryUpTo(
inline fun retrySimple(retries: Int = 2, sleepTime: Long = 20L, block: (Int) -> Unit) { inline fun retrySimple(retries: Int = 2, sleepTime: Long = 20L, block: (Int) -> Unit) {
var failedAttempts = 0 var failedAttempts = 0
while (failedAttempts <= retries) { while (failedAttempts <= retries) {
@Suppress("TooGenericExceptionCaught")
try { try {
block(failedAttempts) block(failedAttempts)
return return
@ -73,14 +77,17 @@ inline fun retrySimple(retries: Int = 2, sleepTime: Long = 20L, block: (Int) ->
* @param maxDelayMillis the maximum delay between retries. * @param maxDelayMillis the maximum delay between retries.
* @param block the logic to run once and then run again if it fails. * @param block the logic to run once and then run again if it fails.
*/ */
@Suppress("MagicNumber")
suspend inline fun retryWithBackoff( suspend inline fun retryWithBackoff(
noinline onErrorListener: ((Throwable) -> Boolean)? = null, noinline onErrorListener: ((Throwable) -> Boolean)? = null,
initialDelayMillis: Long = 1000L, initialDelayMillis: Long = 1000L,
maxDelayMillis: Long = MAX_BACKOFF_INTERVAL, maxDelayMillis: Long = MAX_BACKOFF_INTERVAL,
block: () -> Unit block: () -> Unit
) { ) {
var sequence = 0 // count up to the max and then reset to half. So that we don't repeat the max but we also don't repeat too much. // count up to the max and then reset to half. So that we don't repeat the max but we also don't repeat too much.
var sequence = 0
while (true) { while (true) {
@Suppress("TooGenericExceptionCaught")
try { try {
block() block()
return return
@ -92,7 +99,10 @@ suspend inline fun retryWithBackoff(
sequence++ sequence++
// initialDelay^(sequence/4) + jitter // initialDelay^(sequence/4) + jitter
var duration = Math.pow(initialDelayMillis.toDouble(), (sequence.toDouble() / 4.0)).toLong() + Random.nextLong(1000L) var duration = Math.pow(
initialDelayMillis.toDouble(),
(sequence.toDouble() / 4.0)
).toLong() + Random.nextLong(1000L)
if (duration > maxDelayMillis) { if (duration > maxDelayMillis) {
duration = maxDelayMillis - Random.nextLong(1000L) // include jitter but don't exceed max delay duration = maxDelayMillis - Random.nextLong(1000L) // include jitter but don't exceed max delay
sequence /= 2 sequence /= 2

View File

@ -86,7 +86,7 @@ class FlowPagedListBuilder<Key, Value>(
return when (this) { return when (this) {
is ExecutorCoroutineDispatcher -> executor is ExecutorCoroutineDispatcher -> executor
is MainCoroutineDispatcher -> MainThreadExecutor() is MainCoroutineDispatcher -> MainThreadExecutor()
else -> throw IllegalStateException("Unable to create executor based on dispatcher: $this") else -> error("Unable to create executor based on dispatcher: $this")
} }
} }

View File

@ -63,11 +63,9 @@ class LightWalletGrpcService private constructor(
override fun submitTransaction(spendTransaction: ByteArray): Service.SendResponse { override fun submitTransaction(spendTransaction: ByteArray): Service.SendResponse {
if (spendTransaction.isEmpty()) { if (spendTransaction.isEmpty()) {
return Service.SendResponse.newBuilder().setErrorCode(3000) return Service.SendResponse.newBuilder()
.setErrorMessage( .setErrorCode(EMPTY_TRANSACTION_ERROR_CODE)
"ERROR: failed to submit transaction because it was empty" + .setErrorMessage(EMPTY_TRANSACTION_ERROR_MESSAGE)
" so this request was ignored on the client-side."
)
.build() .build()
} }
val request = val request =
@ -138,6 +136,10 @@ class LightWalletGrpcService private constructor(
} }
companion object { companion object {
private const val EMPTY_TRANSACTION_ERROR_CODE = 3000
private const val EMPTY_TRANSACTION_ERROR_MESSAGE = "ERROR: failed to submit transaction because it was" +
" empty so this request was ignored on the client-side."
fun new(context: Context, lightWalletEndpoint: LightWalletEndpoint): LightWalletGrpcService { fun new(context: Context, lightWalletEndpoint: LightWalletEndpoint): LightWalletGrpcService {
val channel = createDefaultChannel(context, lightWalletEndpoint) val channel = createDefaultChannel(context, lightWalletEndpoint)

View File

@ -71,7 +71,10 @@ interface LightWalletService {
* *
* @return a list of transactions that correspond to the given address for the given range. * @return a list of transactions that correspond to the given address for the given range.
*/ */
fun getTAddressTransactions(tAddress: String, blockHeightRange: ClosedRange<BlockHeight>): List<Service.RawTransaction> fun getTAddressTransactions(
tAddress: String,
blockHeightRange: ClosedRange<BlockHeight>
): List<Service.RawTransaction>
/** /**
* Reconnect to the same or a different server. This is useful when the connection is * Reconnect to the same or a different server. This is useful when the connection is

View File

@ -30,6 +30,7 @@ import java.io.File
* *
* @param pageSize transactions per page. This influences pre-fetch and memory configuration. * @param pageSize transactions per page. This influences pre-fetch and memory configuration.
*/ */
@Suppress("TooManyFunctions")
internal class PagedTransactionRepository private constructor( internal class PagedTransactionRepository private constructor(
private val zcashNetwork: ZcashNetwork, private val zcashNetwork: ZcashNetwork,
private val db: DerivedDataDb, private val db: DerivedDataDb,
@ -105,11 +106,14 @@ internal class PagedTransactionRepository private constructor(
} }
} }
// TODO: begin converting these into Data Access API. For now, just collect the desired operations and iterate/refactor, later // TODO [#681]: begin converting these into Data Access API. For now, just collect the desired
// operations and iterate/refactor, later
// TODO [#681]: https://github.com/zcash/zcash-android-wallet-sdk/issues/681
suspend fun findBlockHash(height: BlockHeight): ByteArray? = blocks.findHashByHeight(height.value) suspend fun findBlockHash(height: BlockHeight): ByteArray? = blocks.findHashByHeight(height.value)
suspend fun getTransactionCount(): Int = transactions.count() suspend fun getTransactionCount(): Int = transactions.count()
// TODO: convert this into a wallet repository rather than "transaction repository" // TODO [#681]: convert this into a wallet repository rather than "transaction repository"
// TODO [#681]: https://github.com/zcash/zcash-android-wallet-sdk/issues/681
companion object { companion object {
@Suppress("LongParameterList") @Suppress("LongParameterList")
@ -149,8 +153,11 @@ internal class PagedTransactionRepository private constructor(
.addMigrations(DerivedDataDb.MIGRATION_5_6) .addMigrations(DerivedDataDb.MIGRATION_5_6)
.addMigrations(DerivedDataDb.MIGRATION_6_7) .addMigrations(DerivedDataDb.MIGRATION_6_7)
.build().also { .build().also {
// TODO: document why we do this. My guess is to catch database issues early or to trigger migrations--I forget why it was added but there was a good reason? // TODO [#681]: document why we do this. My guess is to catch database issues early or to trigger
// migrations--I forget why it was added but there was a good reason?
// TODO [#681]: https://github.com/zcash/zcash-android-wallet-sdk/issues/681
withContext(SdkDispatchers.DATABASE_IO) { withContext(SdkDispatchers.DATABASE_IO) {
// TODO [#649]: StrictMode policy violation: LeakedClosableViolation
// TODO [#649]: https://github.com/zcash/zcash-android-wallet-sdk/issues/649 // TODO [#649]: https://github.com/zcash/zcash-android-wallet-sdk/issues/649
it.openHelper.writableDatabase.beginTransaction() it.openHelper.writableDatabase.beginTransaction()
it.openHelper.writableDatabase.endTransaction() it.openHelper.writableDatabase.endTransaction()
@ -190,7 +197,8 @@ internal class PagedTransactionRepository private constructor(
rustBackend: RustBackend, rustBackend: RustBackend,
checkpoint: Checkpoint checkpoint: Checkpoint
) { ) {
// TODO: consider converting these to typed exceptions in the welding layer // TODO [#681]: consider converting these to typed exceptions in the welding layer
// TODO [#681]: https://github.com/zcash/zcash-android-wallet-sdk/issues/681
tryWarn( tryWarn(
"Warning: did not initialize the blocks table. It probably was already initialized.", "Warning: did not initialize the blocks table. It probably was already initialized.",
ifContains = "table is not empty" ifContains = "table is not empty"
@ -208,11 +216,13 @@ internal class PagedTransactionRepository private constructor(
rustBackend: RustBackend, rustBackend: RustBackend,
viewingKeys: List<UnifiedFullViewingKey> viewingKeys: List<UnifiedFullViewingKey>
) { ) {
// TODO: consider converting these to typed exceptions in the welding layer // TODO [#681]: consider converting these to typed exceptions in the welding layer
// TODO [#681]: https://github.com/zcash/zcash-android-wallet-sdk/issues/681
tryWarn( tryWarn(
"Warning: did not initialize the accounts table. It probably was already initialized.", "Warning: did not initialize the accounts table. It probably was already initialized.",
ifContains = "table is not empty" ifContains = "table is not empty"
) { ) {
@Suppress("SpreadOperator")
rustBackend.initAccountsTable(*viewingKeys.toTypedArray()) rustBackend.initAccountsTable(*viewingKeys.toTypedArray())
twig("Initialized the accounts table with ${viewingKeys.size} viewingKey(s)") twig("Initialized the accounts table with ${viewingKeys.size} viewingKey(s)")
} }

View File

@ -21,8 +21,6 @@ import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File import java.io.File
import java.io.PrintWriter
import java.io.StringWriter
import kotlin.math.max import kotlin.math.max
/** /**
@ -36,6 +34,7 @@ import kotlin.math.max
* id. * id.
* @property service the lightwallet service used to submit transactions. * @property service the lightwallet service used to submit transactions.
*/ */
@Suppress("TooManyFunctions")
class PersistentTransactionManager( class PersistentTransactionManager(
db: PendingTransactionDb, db: PendingTransactionDb,
internal val encoder: TransactionEncoder, internal val encoder: TransactionEncoder,
@ -85,6 +84,7 @@ class PersistentTransactionManager(
memo = memo.toByteArray(), memo = memo.toByteArray(),
accountIndex = fromAccountIndex accountIndex = fromAccountIndex
) )
@Suppress("TooGenericExceptionCaught")
try { try {
safeUpdate("creating tx in DB") { safeUpdate("creating tx in DB") {
tx = findById(create(tx))!! tx = findById(create(tx))!!
@ -113,6 +113,8 @@ class PersistentTransactionManager(
): PendingTransaction = withContext(Dispatchers.IO) { ): PendingTransaction = withContext(Dispatchers.IO) {
twig("managing the creation of a transaction") twig("managing the creation of a transaction")
var tx = pendingTx as PendingTransactionEntity var tx = pendingTx as PendingTransactionEntity
@Suppress("TooGenericExceptionCaught")
try { try {
twig("beginning to encode transaction with : $encoder") twig("beginning to encode transaction with : $encoder")
val encodedTx = encoder.createTransaction( val encodedTx = encoder.createTransaction(
@ -150,6 +152,7 @@ class PersistentTransactionManager(
): PendingTransaction { ): PendingTransaction {
twig("managing the creation of a shielding transaction") twig("managing the creation of a shielding transaction")
var tx = pendingTx as PendingTransactionEntity var tx = pendingTx as PendingTransactionEntity
@Suppress("TooGenericExceptionCaught")
try { try {
twig("beginning to encode shielding transaction with : $encoder") twig("beginning to encode shielding transaction with : $encoder")
val encodedTx = encoder.createShieldingTransaction( val encodedTx = encoder.createShieldingTransaction(
@ -186,11 +189,17 @@ class PersistentTransactionManager(
" transaction found that matches the one being submitted. Verify that the" + " transaction found that matches the one being submitted. Verify that the" +
" transaction still exists among the set of pending transactions." " transaction still exists among the set of pending transactions."
) )
@Suppress("TooGenericExceptionCaught")
try { try {
// do nothing if failed or cancelled // do nothing if failed or cancelled
when { when {
tx.isFailedEncoding() -> twig("Warning: this transaction will not be submitted because it failed to be encoded.") tx.isFailedEncoding() ->
tx.isCancelled() -> twig("Warning: ignoring cancelled transaction with id ${tx.id}. We will not submit it to the network because it has been cancelled.") twig("Warning: this transaction will not be submitted because it failed to be encoded.")
tx.isCancelled() ->
twig(
"Warning: ignoring cancelled transaction with id ${tx.id}. We will not submit it to" +
" the network because it has been cancelled."
)
else -> { else -> {
twig("submitting transaction with memo: ${tx.memo} amount: ${tx.value}", -1) twig("submitting transaction with memo: ${tx.memo} amount: ${tx.value}", -1)
val response = service.submitTransaction(tx.raw) val response = service.submitTransaction(tx.raw)
@ -260,7 +269,11 @@ class PersistentTransactionManager(
withContext(IO) { withContext(IO) {
twig("[cleanup] marking pendingTx $id for deletion") twig("[cleanup] marking pendingTx $id for deletion")
removeRawTransactionId(id) removeRawTransactionId(id)
updateError(id, "safe to delete", -9090) updateError(
id,
SAFE_TO_DELETE_ERROR_MESSAGE,
SAFE_TO_DELETE_ERROR_CODE
)
} }
} }
@ -285,19 +298,23 @@ class PersistentTransactionManager(
// //
/** /**
* Updating the pending transaction is often done at the end of a function but still should * Updating the pending transaction is often done at the end of a function but still should happen within a
* happen within a try/catch block, surrounded by logging. So this helps with that while also * try/catch block, surrounded by logging. So this helps with that while also ensuring that no other coroutines are
* ensuring that no other coroutines are concurrently interacting with the DAO. * concurrently interacting with the DAO.
*/ */
private suspend fun <R> safeUpdate(logMessage: String = "", priority: Int = 0, block: suspend PendingTransactionDao.() -> R): R? { private suspend fun <R> safeUpdate(
logMessage: String = "",
priority: Int = 0,
block: suspend PendingTransactionDao.() -> R
): R? {
@Suppress("TooGenericExceptionCaught")
return try { return try {
twig(logMessage, priority) twig(logMessage, priority)
pendingTransactionDao { block() } pendingTransactionDao { block() }
} catch (t: Throwable) { } catch (t: Throwable) {
val stacktrace = StringWriter().also { t.printStackTrace(PrintWriter(it)) }.toString()
twig( twig(
"Unknown error while attempting to '$logMessage':" + "Unknown error while attempting to '$logMessage':" +
" ${t.message} caused by: ${t.cause} stacktrace: $stacktrace", " ${t.message} caused by: ${t.cause} stacktrace: ${t.stackTrace}",
priority priority
) )
null null
@ -318,5 +335,8 @@ class PersistentTransactionManager(
/** Error code for an error while submitting a transaction */ /** Error code for an error while submitting a transaction */
const val ERROR_SUBMITTING = 3000 const val ERROR_SUBMITTING = 3000
private const val SAFE_TO_DELETE_ERROR_MESSAGE = "safe to delete"
const val SAFE_TO_DELETE_ERROR_CODE = -9090
} }
} }

View File

@ -10,6 +10,7 @@ import kotlinx.coroutines.flow.Flow
* particularly after failed attempts or dropped connectivity. The intent is to help see outbound * particularly after failed attempts or dropped connectivity. The intent is to help see outbound
* transactions through to completion. * transactions through to completion.
*/ */
@Suppress("TooManyFunctions")
interface OutboundTransactionManager { interface OutboundTransactionManager {
/** /**
* Initialize a spend with the main purpose of creating an idea to use for tracking it until * Initialize a spend with the main purpose of creating an idea to use for tracking it until

View File

@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.Flow
/** /**
* Repository of wallet transactions, providing an agnostic interface to the underlying information. * Repository of wallet transactions, providing an agnostic interface to the underlying information.
*/ */
@Suppress("TooManyFunctions")
interface TransactionRepository { interface TransactionRepository {
/** /**

View File

@ -124,6 +124,7 @@ internal class WalletTransactionEncoder(
"creating transaction to spend $amount zatoshi to" + "creating transaction to spend $amount zatoshi to" +
" ${toAddress.masked()} with memo $memo" " ${toAddress.masked()} with memo $memo"
) { ) {
@Suppress("TooGenericExceptionCaught")
try { try {
val branchId = getConsensusBranchId() val branchId = getConsensusBranchId()
SaplingParamTool.ensureParams((rustBackend as RustBackend).pathParamsDir) SaplingParamTool.ensureParams((rustBackend as RustBackend).pathParamsDir)
@ -137,7 +138,7 @@ internal class WalletTransactionEncoder(
memo memo
) )
} catch (t: Throwable) { } catch (t: Throwable) {
twig("${t.message}") twig("Caught exception while creating transaction ${t.message}, caused by: ${t.cause}.")
throw t throw t
} }
}.also { result -> }.also { result ->
@ -151,6 +152,7 @@ internal class WalletTransactionEncoder(
memo: ByteArray? = byteArrayOf() memo: ByteArray? = byteArrayOf()
): Long { ): Long {
return twigTask("creating transaction to shield all UTXOs") { return twigTask("creating transaction to shield all UTXOs") {
@Suppress("TooGenericExceptionCaught")
try { try {
SaplingParamTool.ensureParams((rustBackend as RustBackend).pathParamsDir) SaplingParamTool.ensureParams((rustBackend as RustBackend).pathParamsDir)
twig("params exist! attempting to shield...") twig("params exist! attempting to shield...")
@ -160,9 +162,10 @@ internal class WalletTransactionEncoder(
memo memo
) )
} catch (t: Throwable) { } catch (t: Throwable) {
// TODO: if this error matches: Insufficient balance (have 0, need 1000 including fee) // TODO [#680]: if this error matches: Insufficient balance (have 0, need 1000 including fee)
// then consider custom error that says no UTXOs existed to shield // then consider custom error that says no UTXOs existed to shield
twig("Shield failed due to: ${t.message}") // TODO [#680]: https://github.com/zcash/zcash-android-wallet-sdk/issues/680
twig("Shield failed due to: ${t.message}, caused by: ${t.cause}.")
throw t throw t
} }
}.also { result -> }.also { result ->

View File

@ -31,6 +31,7 @@ internal class NativeLibraryLoader(private val libraryName: String) {
} }
} }
@Suppress("TooGenericExceptionCaught")
private suspend fun loadRustLibrary() { private suspend fun loadRustLibrary() {
try { try {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {

View File

@ -20,6 +20,7 @@ import java.io.File
* not be called directly by code outside of the SDK. Instead, one of the higher-level components * not be called directly by code outside of the SDK. Instead, one of the higher-level components
* should be used such as Wallet.kt or CompactBlockProcessor.kt. * should be used such as Wallet.kt or CompactBlockProcessor.kt.
*/ */
@Suppress("TooManyFunctions")
internal class RustBackend private constructor( internal class RustBackend private constructor(
override val network: ZcashNetwork, override val network: ZcashNetwork,
val birthdayHeight: BlockHeight, val birthdayHeight: BlockHeight,
@ -67,6 +68,7 @@ internal class RustBackend private constructor(
numberOfAccounts: Int numberOfAccounts: Int
): Array<UnifiedFullViewingKey> { ): Array<UnifiedFullViewingKey> {
return DerivationTool.deriveUnifiedFullViewingKeys(seed, network, numberOfAccounts).apply { return DerivationTool.deriveUnifiedFullViewingKeys(seed, network, numberOfAccounts).apply {
@Suppress("SpreadOperator")
initAccountsTable(*this) initAccountsTable(*this)
} }
} }
@ -96,7 +98,10 @@ internal class RustBackend private constructor(
} }
override suspend fun getTransparentAddress(account: Int, index: Int): String { override suspend fun getTransparentAddress(account: Int, index: Int): String {
throw NotImplementedError("TODO: implement this at the zcash_client_sqlite level. But for now, use DerivationTool, instead to derive addresses from seeds") throw NotImplementedError(
"TODO: implement this at the zcash_client_sqlite level. But for now, use " +
"DerivationTool, instead to derive addresses from seeds"
)
} }
override suspend fun getBalance(account: Int): Zatoshi { override suspend fun getBalance(account: Int): Zatoshi {
@ -325,15 +330,18 @@ internal class RustBackend private constructor(
// * We're able to keep the "unsafe" byteArray functions private and wrap them in typeSafe // * We're able to keep the "unsafe" byteArray functions private and wrap them in typeSafe
// * equivalents and, eventually, surface any parse errors (for now, errors are only logged). // * equivalents and, eventually, surface any parse errors (for now, errors are only logged).
// */ // */
// override fun parseTransactionDataList(tdl: LocalRpcTypes.TransactionDataList): LocalRpcTypes.TransparentTransactionList { // override fun parseTransactionDataList(
// return try { // tdl: LocalRpcTypes.TransactionDataList
// // serialize the list, send it over to rust and get back a serialized set of results that we parse out and return // ): LocalRpcTypes.TransparentTransactionList {
// return LocalRpcTypes.TransparentTransactionList.parseFrom(parseTransactionDataList(tdl.toByteArray())) // return try {
// } catch (t: Throwable) { // // serialize the list, send it over to rust and get back a serialized set of results that we parse out
// twig("ERROR: failed to parse transaction data list due to: $t caused by: ${t.cause}") // // and return
// LocalRpcTypes.TransparentTransactionList.newBuilder().build() // return LocalRpcTypes.TransparentTransactionList.parseFrom(parseTransactionDataList(tdl.toByteArray()))
// } // } catch (t: Throwable) {
// } // twig("ERROR: failed to parse transaction data list due to: $t caused by: ${t.cause}")
// LocalRpcTypes.TransparentTransactionList.newBuilder().build()
// }
// }
/** /**
* Exposes all of the librustzcash functions along with helpers for loading the static library. * Exposes all of the librustzcash functions along with helpers for loading the static library.
@ -390,6 +398,7 @@ internal class RustBackend private constructor(
): Boolean ): Boolean
@JvmStatic @JvmStatic
@Suppress("LongParameterList")
private external fun initBlocksTable( private external fun initBlocksTable(
dbDataPath: String, dbDataPath: String,
height: Long, height: Long,
@ -483,6 +492,7 @@ internal class RustBackend private constructor(
) )
@JvmStatic @JvmStatic
@Suppress("LongParameterList")
private external fun createToAddress( private external fun createToAddress(
dbDataPath: String, dbDataPath: String,
consensusBranchId: Long, consensusBranchId: Long,
@ -516,6 +526,7 @@ internal class RustBackend private constructor(
private external fun branchIdForHeight(height: Long, networkId: Int): Long private external fun branchIdForHeight(height: Long, networkId: Int): Long
@JvmStatic @JvmStatic
@Suppress("LongParameterList")
private external fun putUtxo( private external fun putUtxo(
dbDataPath: String, dbDataPath: String,
tAddress: String, tAddress: String,

View File

@ -13,10 +13,12 @@ import cash.z.ecc.android.sdk.type.UnifiedFullViewingKey
* It is not documented because it is not intended to be used, directly. * It is not documented because it is not intended to be used, directly.
* Instead, use the synchronizer or one of its subcomponents. * Instead, use the synchronizer or one of its subcomponents.
*/ */
@Suppress("TooManyFunctions")
internal interface RustBackendWelding { internal interface RustBackendWelding {
val network: ZcashNetwork val network: ZcashNetwork
@Suppress("LongParameterList")
suspend fun createToAddress( suspend fun createToAddress(
consensusBranchId: Long, consensusBranchId: Long,
account: Int, account: Int,
@ -75,6 +77,7 @@ internal interface RustBackendWelding {
*/ */
suspend fun validateCombinedChain(): BlockHeight? suspend fun validateCombinedChain(): BlockHeight?
@Suppress("LongParameterList")
suspend fun putUtxo( suspend fun putUtxo(
tAddress: String, tAddress: String,
txId: ByteArray, txId: ByteArray,
@ -84,7 +87,10 @@ internal interface RustBackendWelding {
height: BlockHeight height: BlockHeight
): Boolean ): Boolean
suspend fun clearUtxos(tAddress: String, aboveHeightInclusive: BlockHeight = BlockHeight(network.saplingActivationHeight.value)): Boolean suspend fun clearUtxos(
tAddress: String,
aboveHeightInclusive: BlockHeight = BlockHeight(network.saplingActivationHeight.value)
): Boolean
suspend fun getDownloadedUtxoBalance(address: String): WalletBalance suspend fun getDownloadedUtxoBalance(address: String): WalletBalance

View File

@ -44,7 +44,8 @@ data class BlockHeight internal constructor(val value: Long) : Comparable<BlockH
/** /**
* @param zcashNetwork Network to use for the block height. * @param zcashNetwork Network to use for the block height.
* @param blockHeight The block height. Must be in range of a UInt32 AND must be greater than the network's sapling activation height. * @param blockHeight The block height. Must be in range of a UInt32 AND must be greater than the network's
* sapling activation height.
*/ */
fun new(zcashNetwork: ZcashNetwork, blockHeight: Long): BlockHeight { fun new(zcashNetwork: ZcashNetwork, blockHeight: Long): BlockHeight {
require(blockHeight >= zcashNetwork.saplingActivationHeight.value) { require(blockHeight >= zcashNetwork.saplingActivationHeight.value) {

View File

@ -34,7 +34,8 @@ internal object CheckpointTool {
network: ZcashNetwork, network: ZcashNetwork,
birthdayHeight: BlockHeight? birthdayHeight: BlockHeight?
): Checkpoint { ): Checkpoint {
// TODO: potentially pull from shared preferences first // TODO [#684]: potentially pull from shared preferences first
// TODO [#684]: https://github.com/zcash/zcash-android-wallet-sdk/issues/684
return loadCheckpointFromAssets(context, network, birthdayHeight) return loadCheckpointFromAssets(context, network, birthdayHeight)
} }
@ -125,7 +126,8 @@ internal object CheckpointTool {
} }
/** /**
* @param treeFiles A list of files, sorted in descending order based on `int` value of the first part of the filename. * @param treeFiles A list of files, sorted in descending order based on `int` value of the first part of
* the filename.
*/ */
@VisibleForTesting @VisibleForTesting
internal suspend fun getFirstValidWalletBirthday( internal suspend fun getFirstValidWalletBirthday(
@ -136,6 +138,7 @@ internal object CheckpointTool {
): Checkpoint { ): Checkpoint {
var lastException: Exception? = null var lastException: Exception? = null
treeFiles.forEach { treefile -> treeFiles.forEach { treefile ->
@Suppress("TooGenericExceptionCaught")
try { try {
val jsonString = withContext(Dispatchers.IO) { val jsonString = withContext(Dispatchers.IO) {
context.assets.open("$directory/$treefile").use { inputStream -> context.assets.open("$directory/$treefile").use { inputStream ->
@ -157,7 +160,8 @@ internal object CheckpointTool {
lastException = exception lastException = exception
if (IS_FALLBACK_ON_FAILURE) { if (IS_FALLBACK_ON_FAILURE) {
// TODO: If we ever add crash analytics hooks, this would be something to report // TODO [#684]: If we ever add crash analytics hooks, this would be something to report
// TODO [#684]: https://github.com/zcash/zcash-android-wallet-sdk/issues/684
twig("Malformed birthday file $t") twig("Malformed birthday file $t")
} else { } else {
throw exception throw exception

View File

@ -5,8 +5,10 @@ import cash.z.ecc.android.sdk.jni.RustBackendWelding
import cash.z.ecc.android.sdk.model.ZcashNetwork import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.type.UnifiedFullViewingKey import cash.z.ecc.android.sdk.type.UnifiedFullViewingKey
@Suppress("UtilityClassWithPublicConstructor")
class DerivationTool { class DerivationTool {
@Suppress("TooManyFunctions")
companion object : RustBackendWelding.Derivation { companion object : RustBackendWelding.Derivation {
/** /**
@ -18,7 +20,11 @@ class DerivationTool {
* *
* @return the UFVKs derived from the seed, encoded as Strings. * @return the UFVKs derived from the seed, encoded as Strings.
*/ */
override suspend fun deriveUnifiedFullViewingKeys(seed: ByteArray, network: ZcashNetwork, numberOfAccounts: Int): Array<UnifiedFullViewingKey> = override suspend fun deriveUnifiedFullViewingKeys(
seed: ByteArray,
network: ZcashNetwork,
numberOfAccounts: Int
): Array<UnifiedFullViewingKey> =
withRustBackendLoaded { withRustBackendLoaded {
deriveUnifiedFullViewingKeysFromSeed(seed, numberOfAccounts, networkId = network.id).map { deriveUnifiedFullViewingKeysFromSeed(seed, numberOfAccounts, networkId = network.id).map {
UnifiedFullViewingKey(it) UnifiedFullViewingKey(it)
@ -32,7 +38,10 @@ class DerivationTool {
* *
* @return the viewing key that corresponds to the spending key. * @return the viewing key that corresponds to the spending key.
*/ */
override suspend fun deriveViewingKey(spendingKey: String, network: ZcashNetwork): String = withRustBackendLoaded { override suspend fun deriveViewingKey(
spendingKey: String,
network: ZcashNetwork
): String = withRustBackendLoaded {
deriveExtendedFullViewingKey(spendingKey, networkId = network.id) deriveExtendedFullViewingKey(spendingKey, networkId = network.id)
} }
@ -45,10 +54,13 @@ class DerivationTool {
* *
* @return the spending keys that correspond to the seed, formatted as Strings. * @return the spending keys that correspond to the seed, formatted as Strings.
*/ */
override suspend fun deriveSpendingKeys(seed: ByteArray, network: ZcashNetwork, numberOfAccounts: Int): Array<String> = override suspend fun deriveSpendingKeys(
withRustBackendLoaded { seed: ByteArray,
deriveExtendedSpendingKeys(seed, numberOfAccounts, networkId = network.id) network: ZcashNetwork,
} numberOfAccounts: Int
): Array<String> = withRustBackendLoaded {
deriveExtendedSpendingKeys(seed, numberOfAccounts, networkId = network.id)
}
/** /**
* Given a seed and account index, return the associated Unified Address. * Given a seed and account index, return the associated Unified Address.
@ -72,26 +84,45 @@ class DerivationTool {
* *
* @return the address that corresponds to the viewing key. * @return the address that corresponds to the viewing key.
*/ */
override suspend fun deriveUnifiedAddress(viewingKey: String, network: ZcashNetwork): String = withRustBackendLoaded { override suspend fun deriveUnifiedAddress(
viewingKey: String,
network: ZcashNetwork
): String = withRustBackendLoaded {
deriveUnifiedAddressFromViewingKey(viewingKey, networkId = network.id) deriveUnifiedAddressFromViewingKey(viewingKey, networkId = network.id)
} }
// WIP probably shouldn't be used just yet. Why? // WIP probably shouldn't be used just yet. Why?
// - because we need the private key associated with this seed and this function doesn't return it. // - because we need the private key associated with this seed and this function doesn't return it.
// - the underlying implementation needs to be split out into a few lower-level calls // - the underlying implementation needs to be split out into a few lower-level calls
override suspend fun deriveTransparentAddress(seed: ByteArray, network: ZcashNetwork, account: Int, index: Int): String = withRustBackendLoaded { override suspend fun deriveTransparentAddress(
seed: ByteArray,
network: ZcashNetwork,
account: Int,
index: Int
): String = withRustBackendLoaded {
deriveTransparentAddressFromSeed(seed, account, index, networkId = network.id) deriveTransparentAddressFromSeed(seed, account, index, networkId = network.id)
} }
override suspend fun deriveTransparentAddressFromPublicKey(publicKey: String, network: ZcashNetwork): String = withRustBackendLoaded { override suspend fun deriveTransparentAddressFromPublicKey(
publicKey: String,
network: ZcashNetwork
): String = withRustBackendLoaded {
deriveTransparentAddressFromPubKey(pk = publicKey, networkId = network.id) deriveTransparentAddressFromPubKey(pk = publicKey, networkId = network.id)
} }
override suspend fun deriveTransparentAddressFromAccountPrivateKey(privateKey: String, network: ZcashNetwork, index: Int): String = withRustBackendLoaded { override suspend fun deriveTransparentAddressFromAccountPrivateKey(
privateKey: String,
network: ZcashNetwork,
index: Int
): String = withRustBackendLoaded {
deriveTransparentAddressFromAccountPrivKey(sk = privateKey, index = index, networkId = network.id) deriveTransparentAddressFromAccountPrivKey(sk = privateKey, index = index, networkId = network.id)
} }
override suspend fun deriveTransparentAccountPrivateKey(seed: ByteArray, network: ZcashNetwork, account: Int): String = withRustBackendLoaded { override suspend fun deriveTransparentAccountPrivateKey(
seed: ByteArray,
network: ZcashNetwork,
account: Int
): String = withRustBackendLoaded {
deriveTransparentAccountPrivKeyFromSeed(seed, account, networkId = network.id) deriveTransparentAccountPrivKeyFromSeed(seed, account, networkId = network.id)
} }
@ -142,7 +173,12 @@ class DerivationTool {
private external fun deriveUnifiedAddressFromViewingKey(key: String, networkId: Int): String private external fun deriveUnifiedAddressFromViewingKey(key: String, networkId: Int): String
@JvmStatic @JvmStatic
private external fun deriveTransparentAddressFromSeed(seed: ByteArray, account: Int, index: Int, networkId: Int): String private external fun deriveTransparentAddressFromSeed(
seed: ByteArray,
account: Int,
index: Int,
networkId: Int
): String
@JvmStatic @JvmStatic
private external fun deriveTransparentAddressFromPubKey(pk: String, networkId: Int): String private external fun deriveTransparentAddressFromPubKey(pk: String, networkId: Int): String
@ -151,6 +187,10 @@ class DerivationTool {
private external fun deriveTransparentAddressFromAccountPrivKey(sk: String, index: Int, networkId: Int): String private external fun deriveTransparentAddressFromAccountPrivKey(sk: String, index: Int, networkId: Int): String
@JvmStatic @JvmStatic
private external fun deriveTransparentAccountPrivKeyFromSeed(seed: ByteArray, account: Int, networkId: Int): String private external fun deriveTransparentAccountPrivKeyFromSeed(
seed: ByteArray,
account: Int,
networkId: Int
): String
} }
} }

View File

@ -1,10 +1,10 @@
package cash.z.ecc.android.sdk.ext package cash.z.ecc.android.sdk.ext
import cash.z.ecc.android.sdk.model.Zatoshi import cash.z.ecc.android.sdk.model.Zatoshi
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import java.math.BigDecimal import java.math.BigDecimal
import java.math.MathContext import java.math.MathContext
import kotlin.test.assertEquals
internal class ConversionsTest { internal class ConversionsTest {

View File

@ -1,13 +0,0 @@
@file:Suppress("ktlint:filename")
package cash.z.wallet
import org.mockito.Mockito
/**
* Use in place of `any()` to fix the issue with mockito `any` returning null (so you can't pass it to functions that
* take a non-null param)
*
* TODO: perhaps submit this function to the mockito kotlin project because it allows the use of non-null 'any()'
*/
internal fun <T> anyNotNull() = Mockito.any<T>() as T

View File

@ -1,464 +0,0 @@
<?xml version='1.0' encoding='UTF-8'?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>ComplexCondition:CompactBlockProcessor.kt$CompactBlockProcessor$(null == lastScannedHeight &amp;&amp; targetHeight &lt; lastLocalBlock) || (null != lastScannedHeight &amp;&amp; targetHeight &lt; lastScannedHeight)</ID>
<ID>ComplexMethod:SdkSynchronizer.kt$SdkSynchronizer$private suspend fun refreshPendingTransactions()</ID>
<ID>ComplexMethod:SendFragment.kt$SendFragment$private fun onPendingTxUpdated(pendingTransaction: PendingTransaction?)</ID>
<ID>ComplexMethod:Transactions.kt$ConfirmedTransaction$override fun equals(other: Any?): Boolean</ID>
<ID>ComplexMethod:Transactions.kt$PendingTransactionEntity$override fun equals(other: Any?): Boolean</ID>
<ID>DestructuringDeclarationWithTooManyEntries:TestnetIntegrationTest.kt$TestnetIntegrationTest$val (height, hash, time, tree) = runBlocking { CheckpointTool.loadNearest( context, synchronizer.network, saplingActivation + 1 ) }</ID>
<ID>EmptyCatchBlock:SdkSynchronizer.kt$SdkSynchronizer${ }</ID>
<ID>EmptyFunctionBlock:MainActivity.kt$MainActivity${ }</ID>
<ID>EmptyFunctionBlock:Placeholders.kt$SampleSpendingKeyProvider${ }</ID>
<ID>EmptyFunctionBlock:ReproduceZ2TFailureTest.kt$ReproduceZ2TFailureTest${ }</ID>
<ID>EmptyFunctionBlock:SampleCodeTest.kt$SampleCodeTest${ }</ID>
<ID>ForbiddenComment:BalancePrinterUtil.kt$BalancePrinterUtil$// TODO: clear the dataDb but leave the cacheDb</ID>
<ID>ForbiddenComment:CheckpointTool.kt$CheckpointTool$// TODO: If we ever add crash analytics hooks, this would be something to report</ID>
<ID>ForbiddenComment:CheckpointTool.kt$CheckpointTool$// TODO: potentially pull from shared preferences first</ID>
<ID>ForbiddenComment:CompactBlockDownloader.kt$CompactBlockDownloader$// TODO: cancel anything in flight</ID>
<ID>ForbiddenComment:CompactBlockProcessor.kt$CompactBlockProcessor$// TODO: add a concept of original checkpoint height to the processor. For now, derive it</ID>
<ID>ForbiddenComment:CompactBlockProcessor.kt$CompactBlockProcessor$// TODO: more accurately track the utxos that were skipped (in theory, this could fail for other reasons)</ID>
<ID>ForbiddenComment:CompactBlockProcessor.kt$CompactBlockProcessor$// TODO: rethink this and make it easier to understand what's happening. Can we reduce this</ID>
<ID>ForbiddenComment:CompactBlockProcessor.kt$CompactBlockProcessor$// TODO: we still need a way to identify those transactions that failed to be enhanced</ID>
<ID>ForbiddenComment:CompactBlockProcessor.kt$CompactBlockProcessor$// todo: cleanup the way that we prevent this from running excessively</ID>
<ID>ForbiddenComment:ConsensusBranchId.kt$ConsensusBranchId.SPROUT$// TODO: see if we can find a way to not rely on this separate source of truth (either stop converting from hex to display name in the apps or use Rust to get this info)</ID>
<ID>ForbiddenComment:CurrencyFormatter.kt$Conversions$// TODO: provide a dynamic way to configure this globally for the SDK</ID>
<ID>ForbiddenComment:DarksideApi.kt$DarksideApi$// // TODO: change this service to accept ints as heights, like everywhere else</ID>
<ID>ForbiddenComment:GetAddressFragment.kt$GetAddressFragment$// TODO: show an example with the synchronizer</ID>
<ID>ForbiddenComment:PagedTransactionRepository.kt$PagedTransactionRepository$// TODO: begin converting these into Data Access API. For now, just collect the desired operations and iterate/refactor, later</ID>
<ID>ForbiddenComment:PagedTransactionRepository.kt$PagedTransactionRepository$// TODO: convert this into a wallet repository rather than "transaction repository"</ID>
<ID>ForbiddenComment:PagedTransactionRepository.kt$PagedTransactionRepository.Companion$// TODO: consider converting these to typed exceptions in the welding layer</ID>
<ID>ForbiddenComment:PagedTransactionRepository.kt$PagedTransactionRepository.Companion$// TODO: document why we do this. My guess is to catch database issues early or to trigger migrations--I forget why it was added but there was a good reason?</ID>
<ID>ForbiddenComment:SampleCodeTest.kt$SampleCodeTest$// TODO: call: Mnemonic::from_phrase(seed_phrase, Language::English).unwrap().entropy()</ID>
<ID>ForbiddenComment:SampleCodeTest.kt$SampleCodeTest$// TODO: call: bip39::Seed::new(&amp;Mnemonic::from_entropy(&amp;seed_bytes, Language::English).unwrap(), "")</ID>
<ID>ForbiddenComment:SampleCodeTest.kt$SampleCodeTest$// TODO: let mnemonic = Mnemonic::from_entropy(entropy, Language::English).unwrap();</ID>
<ID>ForbiddenComment:SampleCodeTest.kt$SampleCodeTest$// TODO: log(seedPhrase.asRawEntropy().asBip39seed())</ID>
<ID>ForbiddenComment:SaplingParamTool.kt$SaplingParamTool.Companion$// TODO: add logging and timeouts</ID>
<ID>ForbiddenComment:SdkSynchronizer.kt$SdkSynchronizer$// TODO: clean this up and handle enhancing gracefully</ID>
<ID>ForbiddenComment:SdkSynchronizer.kt$SdkSynchronizer$// TODO: refresh orchard balance</ID>
<ID>ForbiddenComment:SdkSynchronizer.kt$SdkSynchronizer$// TODO: this would be the place to clear out any stale pending transactions. Remove filter</ID>
<ID>ForbiddenComment:SdkSynchronizer.kt$SdkSynchronizer$// TODO: turn this section into the data access API. For now, just aggregate all the things that we want to do with the underlying data</ID>
<ID>ForbiddenComment:SetupTest.kt$SetupTest.Companion$// // TODO: fix this</ID>
<ID>ForbiddenComment:TestUtils.kt$* Use in place of `any()` to fix the issue with mockito `any` returning null (so you can't pass it to functions that * take a non-null param) * * TODO: perhaps submit this function to the mockito kotlin project because it allows the use of non-null 'any()'</ID>
<ID>ForbiddenComment:TestWallet.kt$TestWallet.Backups.DEFAULT$// TODO: get the proper birthday values for these wallets</ID>
<ID>ForbiddenComment:Transactions.kt$// TODO: test for off-by-one error here. Should we use &lt;= or &lt;</ID>
<ID>ForbiddenComment:WalletTransactionEncoder.kt$WalletTransactionEncoder$// TODO: if this error matches: Insufficient balance (have 0, need 1000 including fee)</ID>
<ID>FunctionParameterNaming:GetBlockFragment.kt$GetBlockFragment$_unused: View? = null</ID>
<ID>FunctionParameterNaming:GetBlockRangeFragment.kt$GetBlockRangeFragment$_unused: View</ID>
<ID>ImplicitDefaultLocale:BlockExt.kt$String.format("%02x", b)</ID>
<ID>ImplicitDefaultLocale:BlockExt.kt$String.format("%02x", this[i--])</ID>
<ID>ImplicitDefaultLocale:Twig.kt$TroubleshootingTwig.Companion$String.format("$tag %1\$tD %1\$tI:%1\$tM:%1\$tS.%1\$tN", System.currentTimeMillis())</ID>
<ID>InvalidPackageDeclaration:OpenForTesting.kt$package cash.z.ecc.android.sdk.annotation</ID>
<ID>LargeClass:CompactBlockProcessor.kt$CompactBlockProcessor</ID>
<ID>LongMethod:SdkSynchronizer.kt$SdkSynchronizer$private suspend fun refreshPendingTransactions()</ID>
<ID>LongParameterList:PagedTransactionRepository.kt$PagedTransactionRepository.Companion$( appContext: Context, zcashNetwork: ZcashNetwork, pageSize: Int = 10, rustBackend: RustBackend, birthday: Checkpoint, viewingKeys: List&lt;UnifiedViewingKey>, overwriteVks: Boolean = false )</ID>
<ID>LongParameterList:RustBackend.kt$RustBackend.Companion$( dbDataPath: String, account: Int, extsk: String, tsk: String, memo: ByteArray, spendParamsPath: String, outputParamsPath: String, networkId: Int )</ID>
<ID>LongParameterList:RustBackend.kt$RustBackend.Companion$( dbDataPath: String, consensusBranchId: Long, account: Int, extsk: String, to: String, value: Long, memo: ByteArray, spendParamsPath: String, outputParamsPath: String, networkId: Int )</ID>
<ID>LongParameterList:RustBackend.kt$RustBackend.Companion$( dbDataPath: String, height: Long, hash: String, time: Long, saplingTree: String, networkId: Int )</ID>
<ID>LongParameterList:RustBackend.kt$RustBackend.Companion$( dbDataPath: String, tAddress: String, txId: ByteArray, index: Int, script: ByteArray, value: Long, height: Long, networkId: Int )</ID>
<ID>LongParameterList:RustBackendWelding.kt$RustBackendWelding$( consensusBranchId: Long, account: Int, extsk: String, to: String, value: Long, memo: ByteArray? = byteArrayOf() )</ID>
<ID>LongParameterList:RustBackendWelding.kt$RustBackendWelding$( tAddress: String, txId: ByteArray, index: Int, script: ByteArray, value: Long, height: BlockHeight )</ID>
<ID>MagicNumber:BatchMetrics.kt$BatchMetrics$1000.0f</ID>
<ID>MagicNumber:BlockExt.kt$16</ID>
<ID>MagicNumber:BlockExt.kt$4</ID>
<ID>MagicNumber:CompactBlockDownloader.kt$CompactBlockDownloader$2_000L</ID>
<ID>MagicNumber:CompactBlockDownloader.kt$CompactBlockDownloader$6</ID>
<ID>MagicNumber:CompactBlockProcessor.kt$CompactBlockProcessor$100</ID>
<ID>MagicNumber:CompactBlockProcessor.kt$CompactBlockProcessor$100.0f</ID>
<ID>MagicNumber:CompactBlockProcessor.kt$CompactBlockProcessor$1000</ID>
<ID>MagicNumber:CompactBlockProcessor.kt$CompactBlockProcessor$100f</ID>
<ID>MagicNumber:CompactBlockProcessor.kt$CompactBlockProcessor$14</ID>
<ID>MagicNumber:CompactBlockProcessor.kt$CompactBlockProcessor$24</ID>
<ID>MagicNumber:CompactBlockProcessor.kt$CompactBlockProcessor$3</ID>
<ID>MagicNumber:CompactBlockProcessor.kt$CompactBlockProcessor$60</ID>
<ID>MagicNumber:CompactBlockProcessor.kt$CompactBlockProcessor$9</ID>
<ID>MagicNumber:CompactBlockProcessor.kt$CompactBlockProcessor.ProcessorInfo$100</ID>
<ID>MagicNumber:CompactBlockProcessor.kt$CompactBlockProcessor.ProcessorInfo$100.0f</ID>
<ID>MagicNumber:ConsensusBranchId.kt$ConsensusBranchId.BLOSSOM$0x2bb4_0e60</ID>
<ID>MagicNumber:ConsensusBranchId.kt$ConsensusBranchId.CANOPY$0xe9ff_75a6</ID>
<ID>MagicNumber:ConsensusBranchId.kt$ConsensusBranchId.HEARTWOOD$0xf5b9_230b</ID>
<ID>MagicNumber:ConsensusBranchId.kt$ConsensusBranchId.OVERWINTER$0x5ba8_1b19</ID>
<ID>MagicNumber:ConsensusBranchId.kt$ConsensusBranchId.SAPLING$0x76b8_09bb</ID>
<ID>MagicNumber:CurrencyFormatter.kt$Conversions$6</ID>
<ID>MagicNumber:DemoConstants.kt$DemoConstants$0.000018</ID>
<ID>MagicNumber:DemoConstants.kt$DemoConstants$0.00017</ID>
<ID>MagicNumber:DemoConstants.kt$DemoConstants$1075590</ID>
<ID>MagicNumber:DerivedDataDb.kt$DerivedDataDb.Companion.&lt;no name provided>$3</ID>
<ID>MagicNumber:DerivedDataDb.kt$DerivedDataDb.Companion.&lt;no name provided>$4</ID>
<ID>MagicNumber:DerivedDataDb.kt$DerivedDataDb.Companion.&lt;no name provided>$5</ID>
<ID>MagicNumber:DerivedDataDb.kt$DerivedDataDb.Companion.&lt;no name provided>$6</ID>
<ID>MagicNumber:DerivedDataDb.kt$DerivedDataDb.Companion.&lt;no name provided>$7</ID>
<ID>MagicNumber:Ext.kt$1000L</ID>
<ID>MagicNumber:GetBalanceFragment.kt$GetBalanceFragment$100</ID>
<ID>MagicNumber:GetBalanceFragment.kt$GetBalanceFragment$12</ID>
<ID>MagicNumber:GetBlockRangeFragment.kt$GetBlockRangeFragment$1000</ID>
<ID>MagicNumber:GetBlockRangeFragment.kt$GetBlockRangeFragment$1000.0</ID>
<ID>MagicNumber:GetPrivateKeyFragment.kt$GetPrivateKeyFragment$5</ID>
<ID>MagicNumber:Initializer.kt$99</ID>
<ID>MagicNumber:LightWalletGrpcService.kt$LightWalletGrpcService$3000</ID>
<ID>MagicNumber:ListTransactionsFragment.kt$ListTransactionsFragment$100</ID>
<ID>MagicNumber:ListUtxosFragment.kt$ListUtxosFragment$100</ID>
<ID>MagicNumber:PersistentTransactionManager.kt$PersistentTransactionManager$9090</ID>
<ID>MagicNumber:SdkSynchronizer.kt$SdkSynchronizer$5</ID>
<ID>MagicNumber:SendFragment.kt$SendFragment$100</ID>
<ID>MagicNumber:SendFragment.kt$SendFragment$12</ID>
<ID>MagicNumber:TransactionViewHolder.kt$TransactionViewHolder$1000L</ID>
<ID>MagicNumber:TransactionViewHolder.kt$TransactionViewHolder$180f</ID>
<ID>MagicNumber:Transactions.kt$100</ID>
<ID>MagicNumber:Transactions.kt$30</ID>
<ID>MagicNumber:Transactions.kt$9090</ID>
<ID>MagicNumber:Twig.kt$10L</ID>
<ID>MagicNumber:Twig.kt$1e5</ID>
<ID>MagicNumber:Twig.kt$TroubleshootingTwig$6</ID>
<ID>MagicNumber:UtxoViewHolder.kt$UtxoViewHolder$1000L</ID>
<ID>MagicNumber:WalletService.kt$1000L</ID>
<ID>MagicNumber:WalletService.kt$4.0</ID>
<ID>MagicNumber:ZcashSdk.kt$ZcashSdk$10</ID>
<ID>MagicNumber:ZcashSdk.kt$ZcashSdk$100</ID>
<ID>MagicNumber:ZcashSdk.kt$ZcashSdk$150</ID>
<ID>MagicNumber:ZcashSdk.kt$ZcashSdk$1_000L</ID>
<ID>MagicNumber:ZcashSdk.kt$ZcashSdk$20</ID>
<ID>MagicNumber:ZcashSdk.kt$ZcashSdk$20_000L</ID>
<ID>MagicNumber:ZcashSdk.kt$ZcashSdk$5</ID>
<ID>MagicNumber:ZcashSdk.kt$ZcashSdk$512</ID>
<ID>MagicNumber:ZcashSdk.kt$ZcashSdk$600_000L</ID>
<ID>MagicNumber:ZcashSdk.kt$ZcashSdk$75_000L</ID>
<ID>MatchingDeclarationName:CurrencyFormatter.kt$Conversions</ID>
<ID>MaxLineLength:BatchMetrics.kt$BatchMetrics$class</ID>
<ID>MaxLineLength:BlockHeight.kt$BlockHeight.Companion$*</ID>
<ID>MaxLineLength:BranchIdTest.kt$BranchIdTest$assertEquals("Invalid branch ID for $networkName at height $height on ${rustBackend.network.networkName}", branchId, actual)</ID>
<ID>MaxLineLength:BranchIdTest.kt$BranchIdTest$assertEquals("Invalid branch Id Hex value for $networkName at height $height on ${rustBackend.network.networkName}", branchHex, clientBranch)</ID>
<ID>MaxLineLength:BranchIdTest.kt$BranchIdTest.Companion$val mainnetBackend = runBlocking { RustBackend.init("", "", "", ZcashNetwork.Mainnet, ZcashNetwork.Mainnet.saplingActivationHeight) }</ID>
<ID>MaxLineLength:BranchIdTest.kt$BranchIdTest.Companion$val testnetBackend = runBlocking { RustBackend.init("", "", "", ZcashNetwork.Testnet, ZcashNetwork.Testnet.saplingActivationHeight) }</ID>
<ID>MaxLineLength:ChangeServiceTest.kt$ChangeServiceTest$"Exception was of the wrong type. Expected ${ChainInfoNotMatching::class.simpleName} but was ${caughtException!!::class.simpleName}"</ID>
<ID>MaxLineLength:ChangeServiceTest.kt$ChangeServiceTest$downloader</ID>
<ID>MaxLineLength:CheckpointTool.kt$CheckpointTool$* @param treeFiles A list of files, sorted in descending order based on `int` value of the first part of the filename.</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$"ERROR: unable to resolve reorg at height $result after ${consecutiveChainErrors.get()} correction attempts!"</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$(lastScannedHeight.value - range.start.value) / (range.endInclusive.value - range.start.value + 1).toFloat() * 100.0f</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$// Note: blocks are public information so it's okay to print them but, still, let's not unless we're debugging something</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$// TODO: more accurately track the utxos that were skipped (in theory, this could fail for other reasons)</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$// communicate that the wallet is no longer synced because it might remain this way for 20+ seconds because we only download on 20s time boundaries so we can't trigger any immediate action</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$// so we round down to the nearest 100 and then subtract 100 to ensure that the result is always at least 100 blocks away</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$// sometimes the initial block was inserted via checkpoint and will not appear in the cache. We can get the hash another way but prevHash is correctly null.</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$currentInfo.lastDownloadRange?.isEmpty() ?: true &amp;&amp; currentInfo.lastScanRange?.isEmpty() ?: true</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$if</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$lastScanRange</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$lowerBoundHeight + MAX_REORG_SIZE + 2</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$tempOldestTransactionHeight.value - tempOldestTransactionHeight.value.rem(ZcashSdk.MAX_REORG_SIZE) - ZcashSdk.MAX_REORG_SIZE.toLong()</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$twig</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$twig("$summary${if (result == BlockProcessingResult.FailedEnhance) " (but there were enhancement errors! We ignore those, for now. Memos in this block range are probably missing! This will be improved in a future release.)" else ""}! Sleeping for ${napTime}ms (latest height: ${currentInfo.networkBlockHeight}).")</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$twig("Also clearing block cache back to $targetHeight. These rewound blocks will download in the next scheduled scan")</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$twig("Rewinding from $lastScannedHeight to requested height: $height using target height: $targetHeight with last local block: $lastLocalBlock")</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$twig("The next block block: ${errorInfo.errorHeight} which had hash ${errorInfo.actualPrevHash} but the expected hash was ${errorInfo.expectedPrevHash}")</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$twig("Unable to process new blocks because we are disconnected! Attempting to reconnect in ${napTime}ms")</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$twig("Warning: An ${error::class.java.simpleName} was encountered while verifying setup but it was ignored by the onSetupErrorHandler. Ignoring message: ${error.message}")</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$twig("Warning: Fetching UTXOs is repeatedly failing! We will only try about ${(9 - failedUtxoFetches + 2) / 3} more times then give up for this session.")</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$twig("Warning: Ignoring transaction at height ${utxo.height} @ index ${utxo.index} because it already exists")</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$twig("We kept the cache blocks in place so we don't need to wait for the next scheduled download to rescan. Instead we will rescan and validate blocks ${range.start}..${range.endInclusive}")</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$twig("batch scan complete! Total time: ${metrics.cumulativeTime} Total blocks measured: ${metrics.cumulativeItems} Cumulative bps: ${metrics.cumulativeIps.format()}")</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$twig("batch scanned ($percent%): $lastScannedHeight/${range.endInclusive} | ${metrics.batchTime}ms, ${metrics.batchItems}blks, ${metrics.batchIps.format()}bps")</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$twig("found $missingBlockCount missing blocks, downloading in $batches batches of $DOWNLOAD_BATCH_SIZE...")</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$twig("not rewinding dataDb because the last scanned height is $lastScannedHeight and the last local block is $lastLocalBlock both of which are less than the target height of $targetHeight")</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$twig("validation failed at block ${errorInfo.errorHeight} which had hash ${errorInfo.actualPrevHash} but the expected hash was ${errorInfo.expectedPrevHash}")</ID>
<ID>MaxLineLength:ConsensusBranchId.kt$ConsensusBranchId.SPROUT$// TODO: see if we can find a way to not rely on this separate source of truth (either stop converting from hex to display name in the apps or use Rust to get this info)</ID>
<ID>MaxLineLength:DarksideApi.kt$DarksideApi$Darkside.DarksideEmptyBlocks.newBuilder().setHeight(startHeight.value).setCount(count).setNonce(nonce).build()</ID>
<ID>MaxLineLength:DarksideApi.kt$DarksideApi$onNext(it.newBuilderForType().setData(it.data).setHeight(tipHeight.value).build())</ID>
<ID>MaxLineLength:DarksideApi.kt$DarksideApi$twig("resetting darksidewalletd with saplingActivation=$saplingActivationHeight branchId=$branchId chainName=$chainName")</ID>
<ID>MaxLineLength:DarksideApi.kt$DarksideApi.EmptyResponse$if (error != null) throw RuntimeException("Server responded with an error: $error caused by ${error?.cause}")</ID>
<ID>MaxLineLength:DarksideTestCoordinator.kt$DarksideTestCoordinator$if</ID>
<ID>MaxLineLength:DarksideTestCoordinator.kt$DarksideTestCoordinator$}</ID>
<ID>MaxLineLength:DarksideTestCoordinator.kt$DarksideTestCoordinator.Companion$"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/after-large-reorg.txt"</ID>
<ID>MaxLineLength:DarksideTestCoordinator.kt$DarksideTestCoordinator.Companion$"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/after-small-reorg.txt"</ID>
<ID>MaxLineLength:DarksideTestCoordinator.kt$DarksideTestCoordinator.Companion$"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/before-reorg.txt"</ID>
<ID>MaxLineLength:DarksideTestCoordinator.kt$DarksideTestCoordinator.Companion$"still champion voice habit trend flight survey between bitter process artefact blind carbon truly provide dizzy crush flush breeze blouse charge solid fish spread"</ID>
<ID>MaxLineLength:DarksideTestCoordinator.kt$DarksideTestCoordinator.DarksideChainMaker$.</ID>
<ID>MaxLineLength:DarksideTestCoordinator.kt$DarksideTestCoordinator.DarksideTestValidator$assertTrue("invalid available balance. Expected a minimum of $available but found ${balance?.available}", available &lt;= balance?.available?.value!!)</ID>
<ID>MaxLineLength:DarksideTestCoordinator.kt$DarksideTestCoordinator.DarksideTestValidator$assertTrue("invalid total balance. Expected a minimum of $total but found ${balance?.total}", total &lt;= balance?.total?.value!!)</ID>
<ID>MaxLineLength:DarksideTestPrerequisites.kt$DarksideTestPrerequisites$"Darkside tests are configured to only run on the Android Emulator. Please see https://github.com/zcash/zcash-android-wallet-sdk/blob/master/docs/tests/Darkside.md"</ID>
<ID>MaxLineLength:DemoConstants.kt$DemoConstants$"wish puppy smile loan doll curve hole maze file ginger hair nose key relax knife witness cannon grab despair throw review deal slush frame"</ID>
<ID>MaxLineLength:DemoConstants.kt$DemoConstants$// corresponds to seed: urban kind wise collect social marble riot primary craft lucky head cause syrup odor artist decorate rhythm phone style benefit portion bus truck top</ID>
<ID>MaxLineLength:DerivationTool.kt$DerivationTool.Companion$override suspend</ID>
<ID>MaxLineLength:DerivationTool.kt$DerivationTool.Companion$private</ID>
<ID>MaxLineLength:DerivedDataDb.kt$TransactionDao$ /* we want all received txs except those that are change and all sent transactions (even those that haven't been mined yet). Note: every entry in the 'send_notes' table has a non-null value for 'address' */</ID>
<ID>MaxLineLength:DerivedDataDb.kt$TransactionDao$// delete the UTXOs because these are effectively cached and we don't have a good way of knowing whether they're spent</ID>
<ID>MaxLineLength:DerivedDataDb.kt$TransactionDao$suspend</ID>
<ID>MaxLineLength:Exceptions.kt$BirthdayException.ExactBirthdayNotFoundException$class</ID>
<ID>MaxLineLength:Exceptions.kt$CompactBlockProcessorException$ConfigurationException : CompactBlockProcessorException</ID>
<ID>MaxLineLength:Exceptions.kt$CompactBlockProcessorException$Disconnected : CompactBlockProcessorException</ID>
<ID>MaxLineLength:Exceptions.kt$CompactBlockProcessorException.EnhanceTransactionError$EnhanceTxDecryptError : EnhanceTransactionError</ID>
<ID>MaxLineLength:Exceptions.kt$CompactBlockProcessorException.EnhanceTransactionError$EnhanceTxDownloadError : EnhanceTransactionError</ID>
<ID>MaxLineLength:Exceptions.kt$CompactBlockProcessorException.EnhanceTransactionError$open</ID>
<ID>MaxLineLength:Exceptions.kt$CompactBlockProcessorException.MismatchedBranch$"Incompatible server: this client expects a server following consensus branch $clientBranch on $networkName but it was $serverBranch! Try updating the client or switching servers."</ID>
<ID>MaxLineLength:Exceptions.kt$CompactBlockProcessorException.MismatchedBranch$class</ID>
<ID>MaxLineLength:Exceptions.kt$CompactBlockProcessorException.MismatchedNetwork$"Incompatible server: this client expects a server using $clientNetwork but it was $serverNetwork! Try updating the client or switching servers."</ID>
<ID>MaxLineLength:Exceptions.kt$LightWalletException.ChangeServerException.ChainInfoNotMatching$class</ID>
<ID>MaxLineLength:Exceptions.kt$LightWalletException.ChangeServerException.StatusException.Companion$"Error: the new server is unavailable. Verify that the host and port are correct. Failed with $this"</ID>
<ID>MaxLineLength:Exceptions.kt$TransactionEncoderException.TransactionNotEncodedException$" with id $transactionId, does not have any raw data. This is a scenario where the wallet should have thrown"</ID>
<ID>MaxLineLength:Exceptions.kt$TransactionEncoderException.TransactionNotFoundException$"$transactionId in the repository. This means the wallet created a transaction and then returned a row ID "</ID>
<ID>MaxLineLength:Exceptions.kt$TransactionEncoderException.TransactionNotFoundException$"that does not actually exist. This is a scenario where the wallet should have thrown an exception but failed "</ID>
<ID>MaxLineLength:GetAddressFragment.kt$GetAddressFragment$val taddress = DerivationTool.deriveTransparentAddress(seed, ZcashNetwork.fromResources(requireApplicationContext()))</ID>
<ID>MaxLineLength:GetAddressFragment.kt$GetAddressFragment$val zaddress = DerivationTool.deriveShieldedAddress(seed, ZcashNetwork.fromResources(requireApplicationContext()))</ID>
<ID>MaxLineLength:GetAddressFragment.kt$GetAddressFragment$viewingKey = runBlocking { DerivationTool.deriveUnifiedViewingKeys(seed, ZcashNetwork.fromResources(requireApplicationContext())).first() }</ID>
<ID>MaxLineLength:GetBalanceFragment.kt$GetBalanceFragment$val viewingKey = runBlocking { DerivationTool.deriveUnifiedViewingKeys(seed, ZcashNetwork.fromResources(requireApplicationContext())).first() }</ID>
<ID>MaxLineLength:GetBlockFragment.kt$GetBlockFragment$val newHeight = min(binding.textBlockHeight.text.toString().toLongOrNull() ?: network.saplingActivationHeight.value, network.saplingActivationHeight.value)</ID>
<ID>MaxLineLength:GetBlockRangeFragment.kt$GetBlockRangeFragment$val end = max(binding.textEndHeight.text.toString().toLongOrNull() ?: network.saplingActivationHeight.value, network.saplingActivationHeight.value)</ID>
<ID>MaxLineLength:GetBlockRangeFragment.kt$GetBlockRangeFragment$val start = max(binding.textStartHeight.text.toString().toLongOrNull() ?: network.saplingActivationHeight.value, network.saplingActivationHeight.value)</ID>
<ID>MaxLineLength:InboundTxTests.kt$InboundTxTests.Companion$"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/0821a89be7f2fc1311792c3fa1dd2171a8cdfb2effd98590cbd5ebcdcfcf491f.txt"</ID>
<ID>MaxLineLength:InboundTxTests.kt$InboundTxTests.Companion$"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/15a677b6770c5505fb47439361d3d3a7c21238ee1a6874fdedad18ae96850590.txt"</ID>
<ID>MaxLineLength:InboundTxTests.kt$InboundTxTests.Companion$"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/4dcc95dd0a2f1f51bd64bb9f729b423c6de1690664a1b6614c75925e781662f7.txt"</ID>
<ID>MaxLineLength:InboundTxTests.kt$InboundTxTests.Companion$"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/71935e29127a7de0b96081f4c8a42a9c11584d83adedfaab414362a6f3d965cf.txt"</ID>
<ID>MaxLineLength:InboundTxTests.kt$InboundTxTests.Companion$"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/75f2cdd2ff6a94535326abb5d9e663d53cbfa5f31ebb24b4d7e420e9440d41a2.txt"</ID>
<ID>MaxLineLength:InboundTxTests.kt$InboundTxTests.Companion$"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/7690c8ec740c1be3c50e2aedae8bf907ac81141ae8b6a134c1811706c73f49a6.txt"</ID>
<ID>MaxLineLength:InboundTxTests.kt$InboundTxTests.Companion$"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/8f064d23c66dc36e32445e5f3b50e0f32ac3ddb78cff21fb521eb6c19c07c99a.txt"</ID>
<ID>MaxLineLength:InboundTxTests.kt$InboundTxTests.Companion$"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/d2e7be14bbb308f9d4d68de424d622cbf774226d01cd63cc6f155fafd5cd212c.txt"</ID>
<ID>MaxLineLength:InboundTxTests.kt$InboundTxTests.Companion$"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/e6566be3a4f9a80035dab8e1d97e40832a639e3ea938fb7972ea2f8482ff51ce.txt"</ID>
<ID>MaxLineLength:InboundTxTests.kt$InboundTxTests.Companion$"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/e9527891b5d43d1ac72f2c0a3ac18a33dc5a0529aec04fa600616ed35f8123f8.txt"</ID>
<ID>MaxLineLength:InboundTxTests.kt$InboundTxTests.Companion$"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/34e507cab780546f980176f3ff2695cd404917508c7e5ee18cc1d2ff3858cb08.txt"</ID>
<ID>MaxLineLength:InboundTxTests.kt$InboundTxTests.Companion$"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/4eaa902279f8380914baf5bcc470d8b7c11d84fda809f67f517a7cb48912b87b.txt"</ID>
<ID>MaxLineLength:InboundTxTests.kt$InboundTxTests.Companion$"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/6edf869063eccff3345676b0fed9f1aa6988fb2524e3d9ca7420a13cfadcd76c.txt"</ID>
<ID>MaxLineLength:InboundTxTests.kt$InboundTxTests.Companion$"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/72a29d7db511025da969418880b749f7fc0fc910cdb06f52193b5fa5c0401d9d.txt"</ID>
<ID>MaxLineLength:InboundTxTests.kt$InboundTxTests.Companion$"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/73c5edf8ffba774d99155121ccf07e67fbcf14284458f7e732751fea60d3bcbc.txt"</ID>
<ID>MaxLineLength:InboundTxTests.kt$InboundTxTests.Companion$"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/c9e35e6ff444b071d63bf9bab6480409d6361760445c8a28d24179adb35c2495.txt"</ID>
<ID>MaxLineLength:InboundTxTests.kt$InboundTxTests.Companion$"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/de97394ae220c28a33ba78b944e82dabec8cb404a4407650b134b3d5950358c0.txt"</ID>
<ID>MaxLineLength:InboundTxTests.kt$InboundTxTests.Companion$"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/ff6ea36765dc29793775c7aa71de19fca039c5b5b873a0497866e9c4bc48af01.txt"</ID>
<ID>MaxLineLength:InboundTxTests.kt$InboundTxTests.Companion$"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/t-shielded-spend.txt"</ID>
<ID>MaxLineLength:InboundTxTests.kt$InboundTxTests.Companion$private const val blocksUrl = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/before-reorg.txt"</ID>
<ID>MaxLineLength:InboundTxTests.kt$InboundTxTests.Companion$private const val tx663174 = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/0821a89be7f2fc1311792c3fa1dd2171a8cdfb2effd98590cbd5ebcdcfcf491f.txt"</ID>
<ID>MaxLineLength:InboundTxTests.kt$InboundTxTests.Companion$private const val tx663188 = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/15a677b6770c5505fb47439361d3d3a7c21238ee1a6874fdedad18ae96850590.txt"</ID>
<ID>MaxLineLength:InboundTxTests.kt$InboundTxTests.Companion$private const val txIndexReorg = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/tx-index-reorg/t1.txt"</ID>
<ID>MaxLineLength:InitializerTest.kt$InitializerTest$// "zxviews1qv886f6hqqqqpqy2ajg9sm22vs4gm4hhajthctfkfws34u45pjtut3qmz0eatpqzvllgsvlk3x0y35ktx5fnzqqzueyph20k3328kx46y3u5xs4750cwuwjuuccfp7la6rh8yt2vjz6tylsrwzy3khtjjzw7etkae6gw3vq608k7quka4nxkeqdxxsr9xxdagv2rhhwugs6w0cquu2ykgzgaln2vyv6ah3ram2h6lrpxuznyczt2xl3lyxcwlk4wfz5rh7wzfd7642c2ae5d7"</ID>
<ID>MaxLineLength:InitializerTest.kt$InitializerTest$// "zxviews1qvn6j50dqqqqpqxqkvqgx2sp63jccr4k5t8zefadpzsu0yy73vczfznwc794xz6lvy3yp5ucv43lww48zz95ey5vhrsq83dqh0ky9junq0cww2wjp9c3cd45n5l5x8l2g9atnx27e9jgyy8zasjy26gugjtefphan9al3tx208m8ekev5kkx3ug6pd0qk4gq4j4wfuxajn388pfpq54wklwktqkyjz9e6gam0n09xjc35ncd3yah5aa9ezj55lk4u7v7hn0v86vz7ygq4qj2v",</ID>
<ID>MaxLineLength:InitializerTest.kt$InitializerTest$// assertEquals("Height should equal sapling activation height when defaultToOldestHeight is true", ZcashSdk.SAPLING_ACTIVATION_HEIGHT, initializer.birthday.height)</ID>
<ID>MaxLineLength:InitializerTest.kt$InitializerTest$// assertEquals("Incorrect height used for import.", ZcashSdk.SAPLING_ACTIVATION_HEIGHT, initializer.birthday.height)</ID>
<ID>MaxLineLength:InitializerTest.kt$InitializerTest$// assertNotEquals("Height should not equal sapling activation height when defaultToOldestHeight is false", ZcashSdk.SAPLING_ACTIVATION_HEIGHT, h)</ID>
<ID>MaxLineLength:LightWalletService.kt$LightWalletService$fun</ID>
<ID>MaxLineLength:ListUtxosFragment.kt$ListUtxosFragment$binding.inputRangeStart.setText(ZcashNetwork.fromResources(requireApplicationContext()).saplingActivationHeight.toString())</ID>
<ID>MaxLineLength:MaintainedTest.kt$TestPurpose.DARKSIDE$* These tests require a running instance of [darksidewalletd](https://github.com/zcash/lightwalletd/blob/master/docs/darksidewalletd.md).</ID>
<ID>MaxLineLength:MultiAccountIntegrationTest.kt$// private val secondKey = "zxviews1q0w208wwqqqqpqyxp978kt2qgq5gcyx4er907zhczxpepnnhqn0a47ztefjnk65w2573v7g5fd3hhskrg7srpxazfvrj4n2gm4tphvr74a9xnenpaxy645dmuqkevkjtkf5jld2f7saqs3xyunwquhksjpqwl4zx8zj73m8gk2d5d30pck67v5hua8u3chwtxyetmzjya8jdjtyn2aum7au0agftfh5q9m4g596tev9k365s84jq8n3laa5f4palt330dq0yede053sdyfv6l"</ID>
<ID>MaxLineLength:MultiAccountTest.kt$// private const val blocksUrl = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/before-reorg.txt"</ID>
<ID>MaxLineLength:MultiAccountTest.kt$// private val secondKey = "zxviews1q0w208wwqqqqpqyxp978kt2qgq5gcyx4er907zhczxpepnnhqn0a47ztefjnk65w2573v7g5fd3hhskrg7srpxazfvrj4n2gm4tphvr74a9xnenpaxy645dmuqkevkjtkf5jld2f7saqs3xyunwquhksjpqwl4zx8zj73m8gk2d5d30pck67v5hua8u3chwtxyetmzjya8jdjtyn2aum7au0agftfh5q9m4g596tev9k365s84jq8n3laa5f4palt330dq0yede053sdyfv6l"</ID>
<ID>MaxLineLength:MultiAccountTest.kt$// twig("&lt;DONE importing viewing key>&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;")</ID>
<ID>MaxLineLength:MultiAccountTest.kt$// twig("&lt;importing viewing key>&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;")</ID>
<ID>MaxLineLength:MultiRecipientIntegrationTest.kt$// "average talent frozen work brand output major soldier witness keen brown bind indicate burden furnace long crime joke inhale chronic ordinary renew boat flame",</ID>
<ID>MaxLineLength:MultiRecipientIntegrationTest.kt$// "echo viable panic unaware stay magnet cake museum yellow abandon mountain height lunch advance tongue market bamboo cushion okay morning minute icon obtain december",</ID>
<ID>MaxLineLength:MultiRecipientIntegrationTest.kt$// "profit save black expose rude feature early rocket alter borrow finish october few duty flush kick spell bean burden enforce bitter theme silent uphold",</ID>
<ID>MaxLineLength:MultiRecipientIntegrationTest.kt$// "renew enlist travel stand trust execute decade surge follow push student school focus woman ripple movie that bitter plug same index wife spread differ"</ID>
<ID>MaxLineLength:MultiRecipientIntegrationTest.kt$// "secret-extended-key-main1qdtp7dwfqqqqpqq3zxegnzc6qtacjp4m6qhyz7typdw9h9smra3rn322dkhyfg8kk26p0fcjuklryw0ed6falf6c7dwqehleca0xf6m6tlnv5zdjx7lqs4xmseqjz0fvk273aczatxxjaqmy3kv8wtzcc6pf6qtrjy5g2mqgs3cj9f8zrhu0ukzf9gn2arr02kzdct0jh5ee3zjch3xscjv34pzkgpueuq0pyl706alssuchqu4jmjm22fcq3htlwxt3f3hdytne7mgacmaq6",</ID>
<ID>MaxLineLength:MultiRecipientIntegrationTest.kt$// "secret-extended-key-main1qv85jn3hqqqqpq9jam3g232ylvvhy8e5vdhp0x9zjppr49sw6awwrm3a3d8l9j9estq9a548lguf0n9fsjs7c96uaymhysuzeek5eg8un0fk8umxszxstm0xfq77x68yjk4t4j7h2xqqjf8nmkx0va3cphnhxpvd0l5dhzgyxryeleayay6m09zpt0dem8hkazlw5jk6gedrakp9z7wzq2ptf6aqkft6z02mtrnq4a5pguwp4m8xkh52wz0r3naeycnqllnvsn8ag5qru36vk",</ID>
<ID>MaxLineLength:MultiRecipientIntegrationTest.kt$// "secret-extended-key-main1qvfmgpzjqqqqpqqnpl2s9n774mrv72zsuw73km9x6ax2s26d0d0ua20nuxvkexa4lzc4n8a3zfvyn2qns37fx00avdtjewghmxz5nc2ey738nrpu4pqqnwysmcls5yek94lf03d5jtsa25nmuln4xjvu6e4g0yrr6xesp9cr6uyj6whwarpcca9exzr7wzltelq5tusn3x3jchjyk6cj09xyctjzykp902w4x23zdsf46d3fn9rtkgm0rmek296c5nhuzf99a2x6umqvf4man",</ID>
<ID>MaxLineLength:MultiRecipientIntegrationTest.kt$// "secret-extended-key-main1qwhel8pxqqqqpqxjl3cqu2z8hu0tqdd5qchkrdtsjuce9egdqlpu7eff2rn3gknm0mdwr9358t3dlcf47vakdwewxy64k7ds7y3k455rfch7s2x8mfesjsxptyfvc9heme3zj08wwdk4l9mwce92lvrl797wmmddt65ygwcqlvvpqs7s2x0cnhemhnwzhx5ccakfgxfym0w8dxglq4h6pwukf2az6lcm38346qc5s9rgx6s988fr0kxnqg0c6g6zlxa2wpc7jh0gz7qx7zl33"</ID>
<ID>MaxLineLength:MultiRecipientIntegrationTest.kt$// "secret-extended-key-main1qws7ryw7qqqqpqq77dmhl9tufzdsgy8hcjq8kxjtgkfwwgqn4a26ahmhmjqueptd2pt49qhm63lt8v93tlqzw7psmkvqqfm6xdnc2qwkflfcenqs7s4sj2yn0c75n982wjrf5k5h37vt3wxwr3pqnjk426lltctrms2uqmqgkl4706c6dmfxg2cmsdlzlt9ykpvacaterq4alljr3efke7k46xcrg4pxc02ezj0txwqjjve23nqqp7t5n5qat4d8569krxgkcd852uqxj5ljt",</ID>
<ID>MaxLineLength:MultiRecipientIntegrationTest.kt$// "unit ice dial annual duty feature smoke expose hard joy globe just accuse inner fog cash neutral forum strategy crash subject hurdle lecture sand",</ID>
<ID>MaxLineLength:MultiRecipientIntegrationTest.kt$// "zxviews1qdtp7dwfqqqqpqq3zxegnzc6qtacjp4m6qhyz7typdw9h9smra3rn322dkhyfg8kktk66k7zaj9tt5j6e58enx89pwry4rxwmcuzqyxlsap965r5gxpt604chmjyuhder6xwu3tx0h608as5sgxapqdqa6v6hy6qzh9fft0ns3cj9f8zrhu0ukzf9gn2arr02kzdct0jh5ee3zjch3xscjv34pzkgpueuq0pyl706alssuchqu4jmjm22fcq3htlwxt3f3hdytne7mgscrz5m",</ID>
<ID>MaxLineLength:MultiRecipientIntegrationTest.kt$// "zxviews1qv85jn3hqqqqpq9jam3g232ylvvhy8e5vdhp0x9zjppr49sw6awwrm3a3d8l9j9es2ed9h29r6ta5tzt53j2y0ex84lzns0thp7n9wzutjapq29chfewqz34q5g6545f8jf0e69jcg9eyv66s8pt3y5dwxg9nrezz8q9j9fwxryeleayay6m09zpt0dem8hkazlw5jk6gedrakp9z7wzq2ptf6aqkft6z02mtrnq4a5pguwp4m8xkh52wz0r3naeycnqllnvsn8ag5q73pqgd",</ID>
<ID>MaxLineLength:MultiRecipientIntegrationTest.kt$// "zxviews1qvfmgpzjqqqqpqqnpl2s9n774mrv72zsuw73km9x6ax2s26d0d0ua20nuxvkexa4lq5fsc6psl8csspyqrlwfeuele5crlwpyjufgkzyy6ffw8hc52hn04jzru6mntms8c2cm255gu200zx4pmz06k3s90jatwehazl465tf6uyj6whwarpcca9exzr7wzltelq5tusn3x3jchjyk6cj09xyctjzykp902w4x23zdsf46d3fn9rtkgm0rmek296c5nhuzf99a2x6umqr804k9",</ID>
<ID>MaxLineLength:MultiRecipientIntegrationTest.kt$// "zxviews1qwhel8pxqqqqpqxjl3cqu2z8hu0tqdd5qchkrdtsjuce9egdqlpu7eff2rn3gknm0msw7ug6qp4ynppscvv6hfm2nkf42lhz8la5et3zsej84xafcn0xdd9ms452hfjp4tljshtffscsl68wgdv3j5nnelxsdcle5rnwkuz6lvvpqs7s2x0cnhemhnwzhx5ccakfgxfym0w8dxglq4h6pwukf2az6lcm38346qc5s9rgx6s988fr0kxnqg0c6g6zlxa2wpc7jh0gz7q4ysx0l"</ID>
<ID>MaxLineLength:MultiRecipientIntegrationTest.kt$// "zxviews1qws7ryw7qqqqpqq77dmhl9tufzdsgy8hcjq8kxjtgkfwwgqn4a26ahmhmjqueptd2pmq3f73pm8uaa25aze5032qw4dppkx4l625xcjcm94d5e65fcq4j2uptnjuqpyu2rvud88dtjwseglgzfe5l4te2xw62yq4tv62d2f6kl4706c6dmfxg2cmsdlzlt9ykpvacaterq4alljr3efke7k46xcrg4pxc02ezj0txwqjjve23nqqp7t5n5qat4d8569krxgkcd852uqg2t2vn",</ID>
<ID>MaxLineLength:MultiRecipientIntegrationTest.kt$// val pong: Service.PingResponse = rustPoc.sendMoney(Service.PingResponse.newBuilder().setEntry(10).setEntry(11).build())</ID>
<ID>MaxLineLength:OutboundTransactionsTest.kt$// private const val blocksUrl = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/before-reorg.txt"</ID>
<ID>MaxLineLength:PagedTransactionRepository.kt$PagedTransactionRepository$// TODO: begin converting these into Data Access API. For now, just collect the desired operations and iterate/refactor, later</ID>
<ID>MaxLineLength:PagedTransactionRepository.kt$PagedTransactionRepository.Companion$// TODO: document why we do this. My guess is to catch database issues early or to trigger migrations--I forget why it was added but there was a good reason?</ID>
<ID>MaxLineLength:PendingTransactionDb.kt$PendingTransactionDao$@Query("UPDATE pending_transactions SET raw = :raw, rawTransactionId = :rawTransactionId, expiryHeight = :expiryHeight WHERE id = :id")</ID>
<ID>MaxLineLength:PersistentTransactionManager.kt$PersistentTransactionManager$private suspend</ID>
<ID>MaxLineLength:PersistentTransactionManager.kt$PersistentTransactionManager$tx.isCancelled() -> twig("Warning: ignoring cancelled transaction with id ${tx.id}. We will not submit it to the network because it has been cancelled.")</ID>
<ID>MaxLineLength:PersistentTransactionManager.kt$PersistentTransactionManager$tx.isFailedEncoding() -> twig("Warning: this transaction will not be submitted because it failed to be encoded.")</ID>
<ID>MaxLineLength:ReorgBasicTest.kt$// private const val blocksUrl = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/before-reorg.txt"</ID>
<ID>MaxLineLength:ReorgBasicTest.kt$// private const val reorgUrl = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/after-small-reorg.txt"</ID>
<ID>MaxLineLength:ReorgLargeTest.kt$// // don't start until after we enter the darkside (otherwise the we find no blocks to begin with and sleep for an interval)</ID>
<ID>MaxLineLength:ReorgLargeTest.kt$// assertTrue("Error: Cannot complete test because the server is disconnected.", it != Synchronizer.Status.DISCONNECTED)</ID>
<ID>MaxLineLength:ReorgLargeTest.kt$// private const val seedPhrase = "still champion voice habit trend flight survey between bitter process artefact blind carbon truly provide dizzy crush flush breeze blouse charge solid fish spread"</ID>
<ID>MaxLineLength:ReorgLargeTest.kt$// validating blocks in range 663172..663202 in db: /data/user/0/cash.z.ecc.android.sdk.test/databases/ReorgTest22_Cache.db</ID>
<ID>MaxLineLength:ReorgLargeTest.kt$// validating blocks in range 663192..663202 in db: /data/user/0/cash.z.ecc.android.sdk.test/databases/ReorgTest22_Cache.db</ID>
<ID>MaxLineLength:ReorgLargeTest.kt$// validating blocks in range 663202..663202 in db: /data/user/0/cash.z.ecc.android.sdk.test/databases/ReorgTest22_Cache.db</ID>
<ID>MaxLineLength:RustBackend.kt$RustBackend$// // serialize the list, send it over to rust and get back a serialized set of results that we parse out and return</ID>
<ID>MaxLineLength:RustBackend.kt$RustBackend$// override fun parseTransactionDataList(tdl: LocalRpcTypes.TransactionDataList): LocalRpcTypes.TransparentTransactionList {</ID>
<ID>MaxLineLength:RustBackend.kt$RustBackend$throw NotImplementedError("TODO: implement this at the zcash_client_sqlite level. But for now, use DerivationTool, instead to derive addresses from seeds")</ID>
<ID>MaxLineLength:RustBackendWelding.kt$RustBackendWelding$suspend fun clearUtxos(tAddress: String, aboveHeightInclusive: BlockHeight = BlockHeight(network.saplingActivationHeight.value)): Boolean</ID>
<ID>MaxLineLength:SanityTest.kt$SanityTest$"${wallet.endpoint} ${wallet.networkName} Lightwalletd is too far behind. Downloader height $downloaderHeight is more than 10 blocks behind block explorer height $expectedHeight"</ID>
<ID>MaxLineLength:SanityTest.kt$SanityTest$assertTrue("$networkName failed to return a proper block. Height was ${block.height} but we expected $height", block.height == height.value)</ID>
<ID>MaxLineLength:SanityTest.kt$SanityTest.Companion$"zxviews1q0hxkupsqqqqpqzsffgrk2smjuccedua7zswf5e3rgtv3ga9nhvhjug670egshd6me53r5n083s2m9mf4va4z7t39ltd3wr7hawnjcw09eu85q0ammsg0tsgx24p4ma0uvr4p8ltx5laum2slh2whc23ctwlnxme9w4dw92kalwk5u4wyem8dynknvvqvs68ktvm8qh7nx9zg22xfc77acv8hk3qqll9k3x4v2fa26puu2939ea7hy4hh60ywma69xtqhcy4037ne8g2sg8sq"</ID>
<ID>MaxLineLength:SanityTest.kt$SanityTest.Companion$"zxviewtestsapling1qv0ue89kqqqqpqqyt4cl5wvssx4wqq30e5m948p07dnwl9x3u75vvnzvjwwpjkrf8yk2gva0kkxk9p8suj4xawlzw9pajuxgap83wykvsuyzfrm33a2p2m4jz2205kgzx0l2lj2kyegtnuph6crkyvyjqmfxut84nu00wxgrstu5fy3eu49nzl8jzr4chmql4ysgg2t8htn9dtvxy8c7wx9rvcerqsjqm6lqln9syk3g8rr3xpy3l4nj0kawenzpcdtnv9qmy98vdhqzaf063"</ID>
<ID>MaxLineLength:SdkSynchronizer.kt$DefaultSynchronizerFactory$// TODO [#242]: Don't hard code page size. It is a workaround for Uncaught Exception: android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views. and is probably related to FlowPagedList</ID>
<ID>MaxLineLength:SdkSynchronizer.kt$SdkSynchronizer$// TODO: turn this section into the data access API. For now, just aggregate all the things that we want to do with the underlying data</ID>
<ID>MaxLineLength:SdkSynchronizer.kt$SdkSynchronizer$twig("[cleanup] FOUND EXPIRED pendingTX (lastScanHeight: $lastScannedHeight expiryHeight: ${it.expiryHeight}): and ${it.id} ${if (result > 0) "successfully removed" else "failed to remove"} it")</ID>
<ID>MaxLineLength:SendFragment.kt$SendFragment$pendingTransaction.isFailedEncoding() -> "ERROR: failed to encode transaction! (id: $id)".also { isSending = false }</ID>
<ID>MaxLineLength:SendFragment.kt$SendFragment$pendingTransaction.isFailedSubmit() -> "ERROR: failed to submit transaction! (id: $id)".also { isSending = false }</ID>
<ID>MaxLineLength:SendFragment.kt$SendFragment$spendingKey = runBlocking { DerivationTool.deriveSpendingKeys(seed, ZcashNetwork.fromResources(requireApplicationContext())).first() }</ID>
<ID>MaxLineLength:SetupTest.kt$SetupTest$val phrase = "still champion voice habit trend flight survey between bitter process artefact blind carbon truly provide dizzy crush flush breeze blouse charge solid fish spread"</ID>
<ID>MaxLineLength:SetupTest.kt$SetupTest.Companion$private const val blocksUrl = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/before-reorg.txt"</ID>
<ID>MaxLineLength:ShieldFundsSample.kt$ShieldFundsSample$val SEED_PHRASE = "still champion voice habit trend flight survey between bitter process artefact blind carbon truly provide dizzy crush flush breeze blouse charge solid fish spread" // \"//\"deputy visa gentle among clean scout farm drive comfort patch skin salt ranch cool ramp warrior drink narrow normal lunch behind salt deal person"//"deputy visa gentle among clean scout farm drive comfort patch skin salt ranch cool ramp warrior drink narrow normal lunch behind salt deal person"</ID>
<ID>MaxLineLength:SmokeTest.kt$SmokeTest$Assert.assertEquals("Invalid CacheDB file", "/data/user/0/cash.z.ecc.android.sdk.test/databases/TestWallet_testnet_Cache.db", wallet.initializer.rustBackend.pathCacheDb)</ID>
<ID>MaxLineLength:SmokeTest.kt$SmokeTest$Assert.assertEquals("Invalid CacheDB params dir", "/data/user/0/cash.z.ecc.android.sdk.test/cache/params", wallet.initializer.rustBackend.pathParamsDir)</ID>
<ID>MaxLineLength:SmokeTest.kt$SmokeTest$Assert.assertEquals("Invalid DataDB file", "/data/user/0/cash.z.ecc.android.sdk.test/databases/TestWallet_testnet_Data.db", wallet.initializer.rustBackend.pathDataDb)</ID>
<ID>MaxLineLength:SmokeTest.kt$SmokeTest$Assert.assertEquals("Invalid extfvk", "zxviewtestsapling1qv0ue89kqqqqpqqyt4cl5wvssx4wqq30e5m948p07dnwl9x3u75vvnzvjwwpjkrf8yk2gva0kkxk9p8suj4xawlzw9pajuxgap83wykvsuyzfrm33a2p2m4jz2205kgzx0l2lj2kyegtnuph6crkyvyjqmfxut84nu00wxgrstu5fy3eu49nzl8jzr4chmql4ysgg2t8htn9dtvxy8c7wx9rvcerqsjqm6lqln9syk3g8rr3xpy3l4nj0kawenzpcdtnv9qmy98vdhqzaf063", wallet.initializer.viewingKeys[0].extfvk)</ID>
<ID>MaxLineLength:SmokeTest.kt$SmokeTest$Assert.assertEquals("Invalid extpub", "0234965f30c8611253d035f44e68d4e2ce82150e8665c95f41ccbaf916b16c69d8", wallet.initializer.viewingKeys[0].extpub)</ID>
<ID>MaxLineLength:TestExtensions.kt$Transactions$"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/0821a89be7f2fc1311792c3fa1dd2171a8cdfb2effd98590cbd5ebcdcfcf491f.txt"</ID>
<ID>MaxLineLength:TestExtensions.kt$Transactions$"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/15a677b6770c5505fb47439361d3d3a7c21238ee1a6874fdedad18ae96850590.txt"</ID>
<ID>MaxLineLength:TestExtensions.kt$Transactions$"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/4dcc95dd0a2f1f51bd64bb9f729b423c6de1690664a1b6614c75925e781662f7.txt"</ID>
<ID>MaxLineLength:TestExtensions.kt$Transactions$"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/71935e29127a7de0b96081f4c8a42a9c11584d83adedfaab414362a6f3d965cf.txt"</ID>
<ID>MaxLineLength:TestExtensions.kt$Transactions$"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/75f2cdd2ff6a94535326abb5d9e663d53cbfa5f31ebb24b4d7e420e9440d41a2.txt"</ID>
<ID>MaxLineLength:TestExtensions.kt$Transactions$"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/7690c8ec740c1be3c50e2aedae8bf907ac81141ae8b6a134c1811706c73f49a6.txt"</ID>
<ID>MaxLineLength:TestExtensions.kt$Transactions$"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/8f064d23c66dc36e32445e5f3b50e0f32ac3ddb78cff21fb521eb6c19c07c99a.txt"</ID>
<ID>MaxLineLength:TestExtensions.kt$Transactions$"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/d2e7be14bbb308f9d4d68de424d622cbf774226d01cd63cc6f155fafd5cd212c.txt"</ID>
<ID>MaxLineLength:TestExtensions.kt$Transactions$"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/e6566be3a4f9a80035dab8e1d97e40832a639e3ea938fb7972ea2f8482ff51ce.txt"</ID>
<ID>MaxLineLength:TestExtensions.kt$Transactions$"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/e9527891b5d43d1ac72f2c0a3ac18a33dc5a0529aec04fa600616ed35f8123f8.txt"</ID>
<ID>MaxLineLength:TestExtensions.kt$Transactions$"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/34e507cab780546f980176f3ff2695cd404917508c7e5ee18cc1d2ff3858cb08.txt"</ID>
<ID>MaxLineLength:TestExtensions.kt$Transactions$"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/4eaa902279f8380914baf5bcc470d8b7c11d84fda809f67f517a7cb48912b87b.txt"</ID>
<ID>MaxLineLength:TestExtensions.kt$Transactions$"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/6edf869063eccff3345676b0fed9f1aa6988fb2524e3d9ca7420a13cfadcd76c.txt"</ID>
<ID>MaxLineLength:TestExtensions.kt$Transactions$"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/72a29d7db511025da969418880b749f7fc0fc910cdb06f52193b5fa5c0401d9d.txt"</ID>
<ID>MaxLineLength:TestExtensions.kt$Transactions$"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/73c5edf8ffba774d99155121ccf07e67fbcf14284458f7e732751fea60d3bcbc.txt"</ID>
<ID>MaxLineLength:TestExtensions.kt$Transactions$"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/c9e35e6ff444b071d63bf9bab6480409d6361760445c8a28d24179adb35c2495.txt"</ID>
<ID>MaxLineLength:TestExtensions.kt$Transactions$"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/de97394ae220c28a33ba78b944e82dabec8cb404a4407650b134b3d5950358c0.txt"</ID>
<ID>MaxLineLength:TestExtensions.kt$Transactions$"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/ff6ea36765dc29793775c7aa71de19fca039c5b5b873a0497866e9c4bc48af01.txt"</ID>
<ID>MaxLineLength:TestExtensions.kt$Transactions$"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/t-shielded-spend.txt"</ID>
<ID>MaxLineLength:TestWallet.kt$TestWallet$suspend</ID>
<ID>MaxLineLength:TestWallet.kt$TestWallet.Backups.ALICE$"quantum whisper lion route fury lunar pelican image job client hundred sauce chimney barely life cliff spirit admit weekend message recipe trumpet impact kitten"</ID>
<ID>MaxLineLength:TestWallet.kt$TestWallet.Backups.BOB$"canvas wine sugar acquire garment spy tongue odor hole cage year habit bullet make label human unit option top calm neutral try vocal arena"</ID>
<ID>MaxLineLength:TestWallet.kt$TestWallet.Backups.DEFAULT$"column rhythm acoustic gym cost fit keen maze fence seed mail medal shrimp tell relief clip cannon foster soldier shallow refuse lunar parrot banana"</ID>
<ID>MaxLineLength:TestWallet.kt$TestWallet.Backups.DEV_WALLET$"still champion voice habit trend flight survey between bitter process artefact blind carbon truly provide dizzy crush flush breeze blouse charge solid fish spread"</ID>
<ID>MaxLineLength:TestWallet.kt$TestWallet.Backups.SAMPLE_WALLET$"input frown warm senior anxiety abuse yard prefer churn reject people glimpse govern glory crumble swallow verb laptop switch trophy inform friend permit purpose"</ID>
<ID>MaxLineLength:TestnetIntegrationTest.kt$TestnetIntegrationTest.Companion$private const val seedPhrase = "still champion voice habit trend flight survey between bitter process artefact blind carbon truly provide dizzy crush flush breeze blouse charge solid fish spread"</ID>
<ID>MaxLineLength:TestnetIntegrationTest.kt$TestnetIntegrationTest.Companion$runBlocking { config.importWallet(seed, BlockHeight.new(ZcashNetwork.Testnet, birthdayHeight), ZcashNetwork.Testnet, lightWalletEndpoint) }</ID>
<ID>MaxLineLength:TransactionViewHolder.kt$TransactionViewHolder$icon.setColorFilter(ContextCompat.getColor(itemView.context, if (isInbound) R.color.tx_inbound else R.color.tx_outbound))</ID>
<ID>MaxLineLength:Transactions.kt$if (latestHeight == null || latestHeight.value &lt; saplingActivationHeight.value || expiryHeight &lt; saplingActivationHeight.value) return false</ID>
<ID>MaxLineLength:TransparentRestoreSample.kt$TransparentRestoreSample$// Assert.assertTrue("Not enough funds to run sample. Expected at least $TX_VALUE Zatoshi but found $value. Try adding funds to $address", value >= TX_VALUE)</ID>
<ID>MaxLineLength:TransparentRestoreSample.kt$TransparentRestoreSample$// twig("FOUND utxo balance of total: ${walletBalance.totalZatoshi} available: ${walletBalance.availableZatoshi}")</ID>
<ID>MaxLineLength:TransparentRestoreSample.kt$TransparentRestoreSample$// walletA.send(TX_VALUE, walletA.transparentAddress, "${TransparentRestoreSample::class.java.simpleName} z->t")</ID>
<ID>MaxLineLength:TransparentRestoreSample.kt$TransparentRestoreSample$Assert.assertTrue("Not enough funds to run sample. Expected some Zatoshi but found ${tbalance.available}. Try adding funds to $address", tbalance.available.value > 0)</ID>
<ID>MaxLineLength:TransparentTest.kt$TransparentTest$assertEquals(expected.tAddr, DerivationTool.deriveTransparentAddressFromPublicKey(uvk.extpub, network = network))</ID>
<ID>MaxLineLength:TransparentTest.kt$TransparentTest.Companion$const val PHRASE = "deputy visa gentle among clean scout farm drive comfort patch skin salt ranch cool ramp warrior drink narrow normal lunch behind salt deal person"</ID>
<ID>MaxLineLength:TransparentTest.kt$TransparentTest.Companion.ExpectedTestnet$override val zAddr = "ztestsapling1wn3tw9w5rs55x5yl586gtk72e8hcfdq8zsnjzcu8p7ghm8lrx54axc74mvm335q7lmy3g0sqje6"</ID>
<ID>MaxLineLength:Twig.kt$inline</ID>
<ID>MaxLineLength:WalletService.kt$var duration = Math.pow(initialDelayMillis.toDouble(), (sequence.toDouble() / 4.0)).toLong() + Random.nextLong(1000L)</ID>
<ID>MaxLineLength:WalletService.kt$var sequence = 0 // count up to the max and then reset to half. So that we don't repeat the max but we also don't repeat too much.</ID>
<ID>MaxLineLength:ZcashSdk.kt$ZcashSdk$*</ID>
<ID>MaxLineLength:build.gradle.kts$// Tricky: fix: By default, the kotlin_module name will not include the version (in classes.jar/META-INF). Instead it has a colon, which breaks compilation on Windows. This is one way to set it explicitly to the proper value. See https://github.com/zcash/zcash-android-wallet-sdk/issues/222 for more info.</ID>
<ID>MaxLineLength:build.gradle.kts$// solves error: Duplicate class com.google.common.util.concurrent.ListenableFuture found in modules jetified-guava-26.0-android.jar (com.google.guava:guava:26.0-android) and listenablefuture-1.0.jar (com.google.guava:listenablefuture:1.0)</ID>
<ID>MaxLineLength:build.gradle.kts$// used by 'ru.gildor.corutines.okhttp.await' (to make simple suspended requests) and breaks on versions higher than 3.8.0</ID>
<ID>MayBeConst:DemoConstants.kt$DemoConstants$// corresponds to address: zs15tzaulx5weua5c7l47l4pku2pw9fzwvvnsp4y80jdpul0y3nwn5zp7tmkcclqaca3mdjqjkl7hx val initialSeedWords: String = "wish puppy smile loan doll curve hole maze file ginger hair nose key relax knife witness cannon grab despair throw review deal slush frame"</ID>
<ID>MayBeConst:DemoConstants.kt$DemoConstants$// corresponds to address: ztestsapling1zhqvuq8zdwa8nsnde7074kcfsat0w25n08jzuvz5skzcs6h9raxu898l48xwr8fmkny3zqqrgd9 val initialSeedWords: String = "wish puppy smile loan doll curve hole maze file ginger hair nose key relax knife witness cannon grab despair throw review deal slush frame"</ID>
<ID>MayBeConst:DemoConstants.kt$DemoConstants$// corresponds to seed: urban kind wise collect social marble riot primary craft lucky head cause syrup odor artist decorate rhythm phone style benefit portion bus truck top val toAddress: String = "zs1lcdmue7rewgvzh3jd09sfvwq3sumu6hkhpk53q94kcneuffjkdg9e3tyxrugkmpza5c3c5e6eqh"</ID>
<ID>MayBeConst:DemoConstants.kt$DemoConstants$// corresponds to seed: urban kind wise collect social marble riot primary craft lucky head cause syrup odor artist decorate rhythm phone style benefit portion bus truck top val toAddress: String = "ztestsapling1ddttvrm6ueug4vwlczs8daqjaul60aur4udnvcz9qdnjt9ekt2tsxheqvv3mn50wvhmzj4ge9rl"</ID>
<ID>MayBeConst:DemoConstants.kt$DemoConstants$val sendAmount: Double = 0.000018</ID>
<ID>MayBeConst:DemoConstants.kt$DemoConstants$val sendAmount: Double = 0.00017</ID>
<ID>MayBeConst:DemoConstants.kt$DemoConstants$val utxoEndHeight: Int = 1075590</ID>
<ID>MayBeConst:TestnetIntegrationTest.kt$TestnetIntegrationTest.Companion$val address = "zs1m30y59wxut4zk9w24d6ujrdnfnl42hpy0ugvhgyhr8s0guszutqhdj05c7j472dndjstulph74m"</ID>
<ID>MayBeConst:TestnetIntegrationTest.kt$TestnetIntegrationTest.Companion$val toAddress = "zs1vp7kvlqr4n9gpehztr76lcn6skkss9p8keqs3nv8avkdtjrcctrvmk9a7u494kluv756jeee5k0"</ID>
<ID>MayBeConst:ZcashSdk.kt$ZcashSdk$/** * Default amount of time, in milliseconds, to poll for new blocks. Typically, this should be about half the average * block time. */ val POLL_INTERVAL = 20_000L</ID>
<ID>MayBeConst:ZcashSdk.kt$ZcashSdk$/** * Default attempts at retrying. */ val RETRIES = 5</ID>
<ID>MayBeConst:ZcashSdk.kt$ZcashSdk$/** * Default number of blocks to rewind when a chain reorg is detected. This should be large enough to recover from the * reorg but smaller than the theoretical max reorg size of 100. */ val REWIND_DISTANCE = 10</ID>
<ID>MayBeConst:ZcashSdk.kt$ZcashSdk$/** * Default size of batches of blocks to scan via librustzcash. The smaller this number the more granular information * can be provided about scan state. Unfortunately, it may also lead to a lot of overhead during scanning. */ val SCAN_BATCH_SIZE = 150</ID>
<ID>MayBeConst:ZcashSdk.kt$ZcashSdk$/** * Estimate of the time between blocks. */ val BLOCK_INTERVAL_MILLIS = 75_000L</ID>
<ID>MayBeConst:ZcashSdk.kt$ZcashSdk$/** * File name for the sapling output params */ val OUTPUT_PARAM_FILE_NAME = "sapling-output.params"</ID>
<ID>MayBeConst:ZcashSdk.kt$ZcashSdk$/** * File name for the sappling spend params */ val SPEND_PARAM_FILE_NAME = "sapling-spend.params"</ID>
<ID>MayBeConst:ZcashSdk.kt$ZcashSdk$/** * The Url that is used by default in zcashd. * We'll want to make this externally configurable, rather than baking it into the SDK but * this will do for now, since we're using a cloudfront URL that already redirects. */ val CLOUD_PARAM_DIR_URL = "https://z.cash/downloads/"</ID>
<ID>MayBeConst:ZcashSdk.kt$ZcashSdk$/** * The amount of blocks ahead of the current height where new transactions are set to expire. This value is controlled * by the rust backend but it is helpful to know what it is set to and should be kept in sync. */ val EXPIRY_OFFSET = 20</ID>
<ID>MayBeConst:ZcashSdk.kt$ZcashSdk$/** * The default maximum amount of time to wait during retry backoff intervals. Failed loops will never wait longer than * this before retyring. */ val MAX_BACKOFF_INTERVAL = 600_000L</ID>
<ID>MayBeConst:ZcashSdk.kt$ZcashSdk$/** * The default memo to use when shielding transparent funds. */ val DEFAULT_SHIELD_FUNDS_MEMO_PREFIX = "shielding:"</ID>
<ID>MayBeConst:ZcashSdk.kt$ZcashSdk$/** * The maximum length of a memo. */ val MAX_MEMO_SIZE = 512</ID>
<ID>MayBeConst:ZcashSdk.kt$ZcashSdk$/** * The theoretical maximum number of blocks in a reorg, due to other bottlenecks in the protocol design. */ val MAX_REORG_SIZE = 100</ID>
<ID>MayBeConst:ZcashSdk.kt$ZcashSdk$val DB_CACHE_NAME = "Cache.db"</ID>
<ID>MayBeConst:ZcashSdk.kt$ZcashSdk$val DB_DATA_NAME = "Data.db"</ID>
<ID>MayBeConst:ZcashSdk.kt$ZcashSdk$val DEFAULT_ALIAS: String = "ZcashSdk"</ID>
<ID>PrintStackTrace:CompactBlockProcessor.kt$CompactBlockProcessor$t</ID>
<ID>PrintStackTrace:PersistentTransactionManager.kt$PersistentTransactionManager$t</ID>
<ID>ReturnCount:CurrencyFormatter.kt$inline fun String?.safelyConvertToBigDecimal(): BigDecimal?</ID>
<ID>ReturnCount:MainActivity.kt$MainActivity$fun getClipboardText(): String?</ID>
<ID>SpreadOperator:Initializer.kt$Initializer.Config$( *DerivationTool.deriveUnifiedViewingKeys( seed, network, numberOfAccounts ) )</ID>
<ID>SpreadOperator:PagedTransactionRepository.kt$PagedTransactionRepository.Companion$(*viewingKeys.toTypedArray())</ID>
<ID>SpreadOperator:RustBackend.kt$RustBackend$(*this)</ID>
<ID>SwallowedException:CompactBlockProcessor.kt$CompactBlockProcessor$e: Throwable</ID>
<ID>SwallowedException:CompactBlockProcessor.kt$CompactBlockProcessor$t: Throwable</ID>
<ID>SwallowedException:CurrencyFormatter.kt$t: Throwable</ID>
<ID>SwallowedException:Ext.kt$t: Throwable</ID>
<ID>SwallowedException:SdkSynchronizer.kt$SdkSynchronizer$t: Throwable</ID>
<ID>SwallowedException:SharedViewModel.kt$SharedViewModel$t: Throwable</ID>
<ID>TooGenericExceptionCaught:CheckpointTool.kt$CheckpointTool$t: Throwable</ID>
<ID>TooGenericExceptionCaught:CompactBlockDownloader.kt$CompactBlockDownloader$t: Throwable</ID>
<ID>TooGenericExceptionCaught:CompactBlockProcessor.kt$CompactBlockProcessor$e: Throwable</ID>
<ID>TooGenericExceptionCaught:CompactBlockProcessor.kt$CompactBlockProcessor$t: Throwable</ID>
<ID>TooGenericExceptionCaught:CurrencyFormatter.kt$t: Throwable</ID>
<ID>TooGenericExceptionCaught:DerivedDataDb.kt$TransactionDao$t: Throwable</ID>
<ID>TooGenericExceptionCaught:Ext.kt$t: Throwable</ID>
<ID>TooGenericExceptionCaught:GetBlockFragment.kt$GetBlockFragment$t: Throwable</ID>
<ID>TooGenericExceptionCaught:GetBlockRangeFragment.kt$GetBlockRangeFragment$t: Throwable</ID>
<ID>TooGenericExceptionCaught:ListUtxosFragment.kt$ListUtxosFragment$t: Throwable</ID>
<ID>TooGenericExceptionCaught:NativeLibraryLoader.kt$NativeLibraryLoader$e: Throwable</ID>
<ID>TooGenericExceptionCaught:PersistentTransactionManager.kt$PersistentTransactionManager$t: Throwable</ID>
<ID>TooGenericExceptionCaught:SaplingParamTool.kt$SaplingParamTool.Companion$e: Throwable</ID>
<ID>TooGenericExceptionCaught:SdkSynchronizer.kt$SdkSynchronizer$t: Throwable</ID>
<ID>TooGenericExceptionCaught:SdkSynchronizer.kt$SdkSynchronizer$tError: Throwable</ID>
<ID>TooGenericExceptionCaught:SdkSynchronizer.kt$SdkSynchronizer$zError: Throwable</ID>
<ID>TooGenericExceptionCaught:SharedViewModel.kt$SharedViewModel$t: Throwable</ID>
<ID>TooGenericExceptionCaught:WalletService.kt$t: Throwable</ID>
<ID>TooGenericExceptionCaught:WalletTransactionEncoder.kt$WalletTransactionEncoder$t: Throwable</ID>
<ID>TooGenericExceptionThrown:DarksideApi.kt$DarksideApi.EmptyResponse$throw RuntimeException("Server responded with an error: $error caused by ${error?.cause}")</ID>
<ID>TooManyFunctions:CompactBlockProcessor.kt$CompactBlockProcessor</ID>
<ID>TooManyFunctions:CurrencyFormatter.kt$cash.z.ecc.android.sdk.ext.CurrencyFormatter.kt</ID>
<ID>TooManyFunctions:DerivationTool.kt$DerivationTool$Companion : Derivation</ID>
<ID>TooManyFunctions:DerivedDataDb.kt$TransactionDao</ID>
<ID>TooManyFunctions:HomeFragment.kt$HomeFragment : BaseDemoFragment</ID>
<ID>TooManyFunctions:Initializer.kt$Initializer$Companion : Erasable</ID>
<ID>TooManyFunctions:Initializer.kt$Initializer$Config</ID>
<ID>TooManyFunctions:ListTransactionsFragment.kt$ListTransactionsFragment : BaseDemoFragment</ID>
<ID>TooManyFunctions:ListUtxosFragment.kt$ListUtxosFragment : BaseDemoFragment</ID>
<ID>TooManyFunctions:MainActivity.kt$MainActivity : AppCompatActivityOnPrimaryClipChangedListenerDrawerListener</ID>
<ID>TooManyFunctions:PagedTransactionRepository.kt$PagedTransactionRepository : TransactionRepository</ID>
<ID>TooManyFunctions:PendingTransactionDb.kt$PendingTransactionDao</ID>
<ID>TooManyFunctions:PersistentTransactionManager.kt$PersistentTransactionManager : OutboundTransactionManager</ID>
<ID>TooManyFunctions:RustBackend.kt$RustBackend : RustBackendWelding</ID>
<ID>TooManyFunctions:RustBackend.kt$RustBackend$Companion</ID>
<ID>TooManyFunctions:RustBackendWelding.kt$RustBackendWelding</ID>
<ID>TooManyFunctions:SdkSynchronizer.kt$SdkSynchronizer : Synchronizer</ID>
<ID>TooManyFunctions:SendFragment.kt$SendFragment : BaseDemoFragment</ID>
<ID>TooManyFunctions:Synchronizer.kt$Synchronizer</ID>
<ID>TooManyFunctions:TransactionManager.kt$OutboundTransactionManager</ID>
<ID>TooManyFunctions:TransactionRepository.kt$TransactionRepository</ID>
<ID>TooManyFunctions:Transactions.kt$cash.z.ecc.android.sdk.db.entity.Transactions.kt</ID>
<ID>UnusedPrivateMember:BalancePrinterUtil.kt$BalancePrinterUtil$private fun downloadNewBlocks(range: IntRange)</ID>
<ID>UnusedPrivateMember:BalancePrinterUtil.kt$BalancePrinterUtil$private suspend fun deleteDb(dbName: String)</ID>
<ID>UnusedPrivateMember:CompactBlockProcessor.kt$CompactBlockProcessor$fastIntervalDesired: Boolean = false</ID>
<ID>UnusedPrivateMember:DarksideTest.kt$DarksideTest$name: String = javaClass.simpleName</ID>
<ID>UnusedPrivateMember:DarksideTestCoordinator.kt$DarksideTestCoordinator.Companion$// Block URLS private const val beforeReorg = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/before-reorg.txt"</ID>
<ID>UnusedPrivateMember:DarksideTestCoordinator.kt$DarksideTestCoordinator.Companion$private const val largeReorg = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/after-large-reorg.txt"</ID>
<ID>UnusedPrivateMember:DarksideTestCoordinator.kt$DarksideTestCoordinator.Companion$private const val smallReorg = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/after-small-reorg.txt"</ID>
<ID>UnusedPrivateMember:DataDbScannerUtil.kt$DataDbScannerUtil$private fun cacheBlocks()</ID>
<ID>UnusedPrivateMember:DataDbScannerUtil.kt$DataDbScannerUtil$private fun deleteDb(dbName: String)</ID>
<ID>UnusedPrivateMember:DataDbScannerUtil.kt$DataDbScannerUtil$private val alias = "ScannerUtil"</ID>
<ID>UnusedPrivateMember:DataDbScannerUtil.kt$DataDbScannerUtil$private val host = "lightd-main.zecwallet.co"</ID>
<ID>UnusedPrivateMember:DataDbScannerUtil.kt$DataDbScannerUtil$private val port = 443</ID>
<ID>UnusedPrivateMember:DerivationTool.kt$DerivationTool.Companion$networkId: Int = ZcashNetwork.Mainnet.id</ID>
<ID>UnusedPrivateMember:DerivationTool.kt$DerivationTool.Companion$viewingKey: UnifiedViewingKey</ID>
<ID>UnusedPrivateMember:GetBlockFragment.kt$GetBlockFragment$_unused: View? = null</ID>
<ID>UnusedPrivateMember:GetBlockRangeFragment.kt$GetBlockRangeFragment$_unused: View</ID>
<ID>UnusedPrivateMember:HomeFragment.kt$HomeFragment$private val homeViewModel: HomeViewModel by viewModels()</ID>
<ID>UnusedPrivateMember:HomeFragment.kt$HomeFragment$unused: View</ID>
<ID>UnusedPrivateMember:InboundTxTests.kt$InboundTxTests$val overwriteBlockCount = 5</ID>
<ID>UnusedPrivateMember:InboundTxTests.kt$InboundTxTests.Companion$private const val lastBlockHash = "2fc7b4682f5ba6ba6f86e170b40f0aa9302e1d3becb2a6ee0db611ff87835e4a"</ID>
<ID>UnusedPrivateMember:InboundTxTests.kt$InboundTxTests.Companion$private const val txIndexReorg = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/tx-index-reorg/t1.txt"</ID>
<ID>UnusedPrivateMember:InboundTxTests.kt$InboundTxTests.Companion$private val txRecv = arrayOf( "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/8f064d23c66dc36e32445e5f3b50e0f32ac3ddb78cff21fb521eb6c19c07c99a.txt", "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/15a677b6770c5505fb47439361d3d3a7c21238ee1a6874fdedad18ae96850590.txt", "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/d2e7be14bbb308f9d4d68de424d622cbf774226d01cd63cc6f155fafd5cd212c.txt", "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/e6566be3a4f9a80035dab8e1d97e40832a639e3ea938fb7972ea2f8482ff51ce.txt", "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/0821a89be7f2fc1311792c3fa1dd2171a8cdfb2effd98590cbd5ebcdcfcf491f.txt", "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/e9527891b5d43d1ac72f2c0a3ac18a33dc5a0529aec04fa600616ed35f8123f8.txt", "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/4dcc95dd0a2f1f51bd64bb9f729b423c6de1690664a1b6614c75925e781662f7.txt", "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/75f2cdd2ff6a94535326abb5d9e663d53cbfa5f31ebb24b4d7e420e9440d41a2.txt", "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/7690c8ec740c1be3c50e2aedae8bf907ac81141ae8b6a134c1811706c73f49a6.txt", "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/71935e29127a7de0b96081f4c8a42a9c11584d83adedfaab414362a6f3d965cf.txt" )</ID>
<ID>UnusedPrivateMember:InboundTxTests.kt$InboundTxTests.Companion$private val txSend = arrayOf( "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/t-shielded-spend.txt", "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/c9e35e6ff444b071d63bf9bab6480409d6361760445c8a28d24179adb35c2495.txt", "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/72a29d7db511025da969418880b749f7fc0fc910cdb06f52193b5fa5c0401d9d.txt", "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/ff6ea36765dc29793775c7aa71de19fca039c5b5b873a0497866e9c4bc48af01.txt", "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/34e507cab780546f980176f3ff2695cd404917508c7e5ee18cc1d2ff3858cb08.txt", "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/6edf869063eccff3345676b0fed9f1aa6988fb2524e3d9ca7420a13cfadcd76c.txt", "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/de97394ae220c28a33ba78b944e82dabec8cb404a4407650b134b3d5950358c0.txt", "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/4eaa902279f8380914baf5bcc470d8b7c11d84fda809f67f517a7cb48912b87b.txt", "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/73c5edf8ffba774d99155121ccf07e67fbcf14284458f7e732751fea60d3bcbc.txt" )</ID>
<ID>UnusedPrivateMember:Initializer.kt$Initializer.Companion$onCriticalErrorHandler: ((Throwable?) -> Boolean)?</ID>
<ID>UnusedPrivateMember:Initializer.kt$Initializer.Companion$private fun onCriticalError(onCriticalErrorHandler: ((Throwable?) -> Boolean)?, error: Throwable)</ID>
<ID>UnusedPrivateMember:MainActivity.kt$MainActivity$view: View</ID>
<ID>UnusedPrivateMember:PersistentTransactionManager.kt$PersistentTransactionManager$priority: Int = 0</ID>
<ID>UnusedPrivateMember:SdkSynchronizer.kt$SdkSynchronizer$private fun onFailedSend(error: Throwable): Boolean</ID>
<ID>UnusedPrivateMember:SdkSynchronizer.kt$SdkSynchronizer$unused: CoroutineContext?</ID>
<ID>UnusedPrivateMember:SendFragment.kt$SendFragment$unused: View</ID>
<ID>UnusedPrivateMember:SetupTest.kt$SetupTest.Companion$private const val blocksUrl = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/before-reorg.txt"</ID>
<ID>UnusedPrivateMember:SetupTest.kt$SetupTest.Companion$private const val firstBlock = 663150</ID>
<ID>UnusedPrivateMember:SetupTest.kt$SetupTest.Companion$private const val lastBlock = 663200</ID>
<ID>UnusedPrivateMember:SetupTest.kt$SetupTest.Companion$private const val lastBlockHash = "2fc7b4682f5ba6ba6f86e170b40f0aa9302e1d3becb2a6ee0db611ff87835e4a"</ID>
<ID>UnusedPrivateMember:TestnetIntegrationTest.kt$TestnetIntegrationTest.Companion$private const val seedPhrase = "still champion voice habit trend flight survey between bitter process artefact blind carbon truly provide dizzy crush flush breeze blouse charge solid fish spread"</ID>
<ID>UnusedPrivateMember:TestnetIntegrationTest.kt$TestnetIntegrationTest.Companion$private const val targetHeight = 663250</ID>
<ID>UnusedPrivateMember:TransactionCounterUtil.kt$TransactionCounterUtil$private val network = ZcashNetwork.Mainnet</ID>
<ID>UseCheckOrError:FlowPagedListBuilder.kt$FlowPagedListBuilder$throw IllegalStateException("Unable to create executor based on dispatcher: $this")</ID>
<ID>UseCheckOrError:Placeholders.kt$SampleSpendingKeyProvider$throw IllegalStateException("This sample provider only supports the dummy seed")</ID>
<ID>UtilityClassWithPublicConstructor:DerivationTool.kt$DerivationTool</ID>
<ID>UtilityClassWithPublicConstructor:Placeholders.kt$SeedGenerator</ID>
<ID>UtilityClassWithPublicConstructor:SaplingParamTool.kt$SaplingParamTool</ID>
<ID>VariableNaming:ShieldFundsSample.kt$ShieldFundsSample$val SEED_PHRASE = "still champion voice habit trend flight survey between bitter process artefact blind carbon truly provide dizzy crush flush breeze blouse charge solid fish spread" // \"//\"deputy visa gentle among clean scout farm drive comfort patch skin salt ranch cool ramp warrior drink narrow normal lunch behind salt deal person"//"deputy visa gentle among clean scout farm drive comfort patch skin salt ranch cool ramp warrior drink narrow normal lunch behind salt deal person"</ID>
<ID>VariableNaming:TransparentRestoreSample.kt$TransparentRestoreSample$val TX_VALUE = Zatoshi(ZcashSdk.MINERS_FEE.value / 2)</ID>
</CurrentIssues>
</SmellBaseline>