[#474] Add type safe BlockHeight API

- Replace int with type safe BlockHeight(Long) object
 - Fix down casting bug, as BlockHeight is uint32 but Java only supports int32 or int64
 - Rename WalletBirthday to Checkpoint and hide from the public API
This commit is contained in:
Carter Jernigan 2022-07-12 08:40:09 -04:00 committed by GitHub
parent f29ffa1895
commit 9b666833b1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
64 changed files with 1266 additions and 885 deletions

View File

@ -4,6 +4,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import cash.z.ecc.android.sdk.darkside.test.DarksideTestCoordinator
import cash.z.ecc.android.sdk.darkside.test.ScopedTest
import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.type.ZcashNetwork
import org.junit.BeforeClass
import org.junit.Test
import org.junit.runner.RunWith
@ -18,12 +20,12 @@ class InboundTxTests : ScopedTest() {
@Test
fun testTargetBlock_scanned() {
validator.validateMinHeightScanned(targetTxBlock - 1)
validator.validateMinHeightScanned(BlockHeight.new(ZcashNetwork.Mainnet, targetTxBlock.value - 1))
}
@Test
fun testLatestHeight() {
validator.validateLatestHeight(targetTxBlock - 1)
validator.validateLatestHeight(BlockHeight.new(ZcashNetwork.Mainnet, targetTxBlock.value - 1))
}
@Test
@ -40,7 +42,7 @@ class InboundTxTests : ScopedTest() {
validator.validateTxCount(2)
}
private fun addTransactions(targetHeight: Int, vararg txs: String) {
private fun addTransactions(targetHeight: BlockHeight, vararg txs: String) {
val overwriteBlockCount = 5
chainMaker
// .stageEmptyBlocks(targetHeight, overwriteBlockCount)
@ -78,8 +80,8 @@ class InboundTxTests : ScopedTest() {
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/71935e29127a7de0b96081f4c8a42a9c11584d83adedfaab414362a6f3d965cf.txt"
)
private const val firstBlock = 663150
private const val targetTxBlock = 663188
private val firstBlock = BlockHeight.new(ZcashNetwork.Mainnet, 663150L)
private val targetTxBlock = BlockHeight.new(ZcashNetwork.Mainnet, 663188L)
private const val lastBlockHash = "2fc7b4682f5ba6ba6f86e170b40f0aa9302e1d3becb2a6ee0db611ff87835e4a"
private val sithLord = DarksideTestCoordinator()
private val validator = sithLord.validator
@ -93,7 +95,7 @@ class InboundTxTests : ScopedTest() {
chainMaker
.resetBlocks(blocksUrl, startHeight = firstBlock, tipHeight = targetTxBlock)
.stageEmptyBlocks(firstBlock + 1, 100)
.applyTipHeight(targetTxBlock - 1)
.applyTipHeight(BlockHeight.new(ZcashNetwork.Mainnet, targetTxBlock.value - 1))
sithLord.synchronizer.start(classScope)
sithLord.await()

View File

@ -3,6 +3,8 @@ package cash.z.ecc.android.sdk.darkside.reorgs
import androidx.test.ext.junit.runners.AndroidJUnit4
import cash.z.ecc.android.sdk.darkside.test.DarksideTestCoordinator
import cash.z.ecc.android.sdk.darkside.test.ScopedTest
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.type.ZcashNetwork
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Test
@ -11,8 +13,8 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class ReorgSetupTest : ScopedTest() {
private val birthdayHeight = 663150
private val targetHeight = 663250
private val birthdayHeight = BlockHeight.new(ZcashNetwork.Mainnet, 663150)
private val targetHeight = BlockHeight.new(ZcashNetwork.Mainnet, 663250)
@Before
fun setup() {

View File

@ -4,6 +4,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import cash.z.ecc.android.sdk.darkside.test.DarksideTestCoordinator
import cash.z.ecc.android.sdk.darkside.test.ScopedTest
import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.type.ZcashNetwork
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.BeforeClass
@ -13,7 +15,10 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class ReorgSmallTest : ScopedTest() {
private val targetHeight = 663250
private val targetHeight = BlockHeight.new(
ZcashNetwork.Mainnet,
663250
)
private val hashBeforeReorg = "09ec0d5de30d290bc5a2318fbf6a2427a81c7db4790ce0e341a96aeac77108b9"
private val hashAfterReorg = "tbd"

View File

@ -4,6 +4,7 @@ import android.content.Context
import cash.z.ecc.android.sdk.R
import cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService
import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.type.ZcashNetwork
import cash.z.wallet.sdk.rpc.Darkside
import cash.z.wallet.sdk.rpc.Darkside.DarksideTransactionsURL
@ -41,7 +42,7 @@ class DarksideApi(
//
fun reset(
saplingActivationHeight: Int = 419200,
saplingActivationHeight: BlockHeight = ZcashNetwork.Mainnet.saplingActivationHeight,
branchId: String = "e9ff75a6", // Canopy,
chainName: String = "darkside${ZcashNetwork.Mainnet.networkName}"
) = apply {
@ -49,7 +50,7 @@ class DarksideApi(
Darkside.DarksideMetaState.newBuilder()
.setBranchID(branchId)
.setChainName(chainName)
.setSaplingActivation(saplingActivationHeight)
.setSaplingActivation(saplingActivationHeight.value.toInt())
.build().let { request ->
createStub().reset(request)
}
@ -60,21 +61,21 @@ class DarksideApi(
createStub().stageBlocks(url.toUrl())
}
fun stageTransactions(url: String, targetHeight: Int) = apply {
fun stageTransactions(url: String, targetHeight: BlockHeight) = apply {
twig("staging transaction at height=$targetHeight from url=$url")
createStub().stageTransactions(
DarksideTransactionsURL.newBuilder().setHeight(targetHeight).setUrl(url).build()
DarksideTransactionsURL.newBuilder().setHeight(targetHeight.value).setUrl(url).build()
)
}
fun stageEmptyBlocks(startHeight: Int, count: Int = 10, nonce: Int = Random.nextInt()) = apply {
fun stageEmptyBlocks(startHeight: BlockHeight, count: Int = 10, nonce: Int = Random.nextInt()) = apply {
twig("staging $count empty blocks starting at $startHeight with nonce $nonce")
createStub().stageBlocksCreate(
Darkside.DarksideEmptyBlocks.newBuilder().setHeight(startHeight).setCount(count).setNonce(nonce).build()
Darkside.DarksideEmptyBlocks.newBuilder().setHeight(startHeight.value).setCount(count).setNonce(nonce).build()
)
}
fun stageTransactions(txs: Iterator<Service.RawTransaction>?, tipHeight: Int) {
fun stageTransactions(txs: Iterator<Service.RawTransaction>?, tipHeight: BlockHeight) {
if (txs == null) {
twig("no transactions to stage")
return
@ -84,7 +85,7 @@ class DarksideApi(
createStreamingStub().stageTransactionsStream(response).apply {
txs.forEach {
twig("stageTransactions: onNext calling!!!")
onNext(it.newBuilderForType().setData(it.data).setHeight(tipHeight.toLong()).build()) // apply the tipHeight because the passed in txs might not know their destination height (if they were created via SendTransaction)
onNext(it.newBuilderForType().setData(it.data).setHeight(tipHeight.value).build()) // apply the tipHeight because the passed in txs might not know their destination height (if they were created via SendTransaction)
twig("stageTransactions: onNext called")
}
twig("stageTransactions: onCompleted calling!!!")
@ -94,7 +95,7 @@ class DarksideApi(
response.await()
}
fun applyBlocks(tipHeight: Int) {
fun applyBlocks(tipHeight: BlockHeight) {
twig("applying blocks up to tipHeight=$tipHeight")
createStub().applyStaged(tipHeight.toHeight())
}
@ -146,7 +147,7 @@ class DarksideApi(
.withDeadlineAfter(singleRequestTimeoutSec, TimeUnit.SECONDS)
private fun String.toUrl() = Darkside.DarksideBlocksURL.newBuilder().setUrl(this).build()
private fun Int.toHeight() = Darkside.DarksideHeight.newBuilder().setHeight(this).build()
private fun BlockHeight.toHeight() = Darkside.DarksideHeight.newBuilder().setHeight(this.value).build()
class EmptyResponse : StreamObserver<Service.Empty> {
var completed = false

View File

@ -4,6 +4,7 @@ import androidx.test.platform.app.InstrumentationRegistry
import cash.z.ecc.android.sdk.SdkSynchronizer
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.type.ZcashNetwork
import io.grpc.StatusRuntimeException
import kotlinx.coroutines.delay
@ -14,19 +15,20 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking
import org.junit.Assert
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
class DarksideTestCoordinator(val wallet: TestWallet) {
constructor(
alias: String = "DarksideTestCoordinator",
seedPhrase: String = DEFAULT_SEED_PHRASE,
startHeight: Int = DEFAULT_START_HEIGHT,
startHeight: BlockHeight = DEFAULT_START_HEIGHT,
host: String = COMPUTER_LOCALHOST,
network: ZcashNetwork = ZcashNetwork.Mainnet,
port: Int = network.defaultPort
) : this(TestWallet(seedPhrase, alias, network, host, startHeight = startHeight, port = port))
private val targetHeight = 663250
private val targetHeight = BlockHeight.new(wallet.network, 663250)
private val context = InstrumentationRegistry.getInstrumentation().context
// dependencies: private
@ -91,20 +93,20 @@ class DarksideTestCoordinator(val wallet: TestWallet) {
* Waits for, at most, the given amount of time for the synchronizer to download and scan blocks
* and reach a 'SYNCED' status.
*/
fun await(timeout: Long = 60_000L, targetHeight: Int = -1) = runBlocking {
fun await(timeout: Long = 60_000L, targetHeight: BlockHeight? = null) = runBlocking {
ScopedTest.timeoutWith(this, timeout) {
twig("*** Waiting up to ${timeout / 1_000}s for sync ***")
synchronizer.status.onEach {
twig("got processor status $it")
if (it == Synchronizer.Status.DISCONNECTED) {
twig("waiting a bit before giving up on connection...")
} else if (targetHeight != -1 && (synchronizer as SdkSynchronizer).processor.getLastScannedHeight() < targetHeight) {
} else if (targetHeight != null && (synchronizer as SdkSynchronizer).processor.getLastScannedHeight() < targetHeight) {
twig("awaiting new blocks from server...")
}
}.map {
// whenever we're waiting for a target height, for simplicity, if we're sleeping,
// and in between polls, then consider it that we're not synced
if (targetHeight != -1 && (synchronizer as SdkSynchronizer).processor.getLastScannedHeight() < targetHeight) {
if (targetHeight != null && (synchronizer as SdkSynchronizer).processor.getLastScannedHeight() < targetHeight) {
twig("switching status to DOWNLOADING because we're still waiting for height $targetHeight")
Synchronizer.Status.DOWNLOADING
} else {
@ -140,14 +142,14 @@ class DarksideTestCoordinator(val wallet: TestWallet) {
inner class DarksideTestValidator {
fun validateHasBlock(height: Int) {
fun validateHasBlock(height: BlockHeight) {
runBlocking {
assertTrue((synchronizer as SdkSynchronizer).findBlockHashAsHex(height) != null)
assertTrue((synchronizer as SdkSynchronizer).findBlockHash(height)?.size ?: 0 > 0)
}
}
fun validateLatestHeight(height: Int) = runBlocking<Unit> {
fun validateLatestHeight(height: BlockHeight) = runBlocking<Unit> {
val info = synchronizer.processorInfo.first()
val networkBlockHeight = info.networkBlockHeight
assertTrue(
@ -157,41 +159,44 @@ class DarksideTestCoordinator(val wallet: TestWallet) {
)
}
fun validateMinHeightDownloaded(minHeight: Int) = runBlocking<Unit> {
fun validateMinHeightDownloaded(minHeight: BlockHeight) = runBlocking<Unit> {
val info = synchronizer.processorInfo.first()
val lastDownloadedHeight = info.lastDownloadedHeight
assertNotNull(lastDownloadedHeight)
assertTrue(
"Expected to have at least downloaded $minHeight but the last downloaded block was" +
" $lastDownloadedHeight! Full details: $info",
lastDownloadedHeight >= minHeight
lastDownloadedHeight!! >= minHeight
)
}
fun validateMinHeightScanned(minHeight: Int) = runBlocking<Unit> {
fun validateMinHeightScanned(minHeight: BlockHeight) = runBlocking<Unit> {
val info = synchronizer.processorInfo.first()
val lastScannedHeight = info.lastScannedHeight
assertNotNull(lastScannedHeight)
assertTrue(
"Expected to have at least scanned $minHeight but the last scanned block was" +
" $lastScannedHeight! Full details: $info",
lastScannedHeight >= minHeight
lastScannedHeight!! >= minHeight
)
}
fun validateMaxHeightScanned(maxHeight: Int) = runBlocking<Unit> {
fun validateMaxHeightScanned(maxHeight: BlockHeight) = runBlocking<Unit> {
val lastDownloadedHeight = synchronizer.processorInfo.first().lastScannedHeight
assertNotNull(lastDownloadedHeight)
assertTrue(
"Did not expect to be synced beyond $maxHeight but we are synced to" +
" $lastDownloadedHeight",
lastDownloadedHeight <= maxHeight
lastDownloadedHeight!! <= maxHeight
)
}
fun validateBlockHash(height: Int, expectedHash: String) {
fun validateBlockHash(height: BlockHeight, expectedHash: String) {
val hash = runBlocking { (synchronizer as SdkSynchronizer).findBlockHashAsHex(height) }
assertEquals(expectedHash, hash)
}
fun onReorg(callback: (errorHeight: Int, rewindHeight: Int) -> Unit) {
fun onReorg(callback: (errorHeight: BlockHeight, rewindHeight: BlockHeight) -> Unit) {
synchronizer.onChainErrorHandler = callback
}
@ -225,7 +230,7 @@ class DarksideTestCoordinator(val wallet: TestWallet) {
//
inner class DarksideChainMaker {
var lastTipHeight = -1
var lastTipHeight: BlockHeight? = null
/**
* Resets the darksidelightwalletd server, stages the blocks represented by the given URL, then
@ -233,8 +238,8 @@ class DarksideTestCoordinator(val wallet: TestWallet) {
*/
fun resetBlocks(
blocksUrl: String,
startHeight: Int = DEFAULT_START_HEIGHT,
tipHeight: Int = startHeight + 100
startHeight: BlockHeight = DEFAULT_START_HEIGHT,
tipHeight: BlockHeight = startHeight + 100
): DarksideChainMaker = apply {
darkside
.reset(startHeight)
@ -242,23 +247,23 @@ class DarksideTestCoordinator(val wallet: TestWallet) {
applyTipHeight(tipHeight)
}
fun stageTransaction(url: String, targetHeight: Int): DarksideChainMaker = apply {
fun stageTransaction(url: String, targetHeight: BlockHeight): DarksideChainMaker = apply {
darkside.stageTransactions(url, targetHeight)
}
fun stageTransactions(targetHeight: Int, vararg urls: String): DarksideChainMaker = apply {
fun stageTransactions(targetHeight: BlockHeight, vararg urls: String): DarksideChainMaker = apply {
urls.forEach {
darkside.stageTransactions(it, targetHeight)
}
}
fun stageEmptyBlocks(startHeight: Int, count: Int = 10): DarksideChainMaker = apply {
fun stageEmptyBlocks(startHeight: BlockHeight, count: Int = 10): DarksideChainMaker = apply {
darkside.stageEmptyBlocks(startHeight, count)
}
fun stageEmptyBlock() = stageEmptyBlocks(lastTipHeight + 1, 1)
fun stageEmptyBlock() = stageEmptyBlocks(lastTipHeight!! + 1, 1)
fun applyTipHeight(tipHeight: Int): DarksideChainMaker = apply {
fun applyTipHeight(tipHeight: BlockHeight): DarksideChainMaker = apply {
twig("applying tip height of $tipHeight")
darkside.applyBlocks(tipHeight)
lastTipHeight = tipHeight
@ -277,14 +282,14 @@ class DarksideTestCoordinator(val wallet: TestWallet) {
}
fun advanceBy(numEmptyBlocks: Int) {
val nextBlock = lastTipHeight + 1
val nextBlock = lastTipHeight!! + 1
twig("adding $numEmptyBlocks empty blocks to the chain starting at $nextBlock")
darkside.stageEmptyBlocks(nextBlock, numEmptyBlocks)
applyTipHeight(nextBlock + numEmptyBlocks)
}
fun applyPendingTransactions(targetHeight: Int = lastTipHeight + 1) {
stageEmptyBlocks(lastTipHeight + 1, targetHeight - lastTipHeight)
fun applyPendingTransactions(targetHeight: BlockHeight = lastTipHeight!! + 1) {
stageEmptyBlocks(lastTipHeight!! + 1, (targetHeight.value - lastTipHeight!!.value).toInt())
darkside.stageTransactions(darkside.getSentTransactions()?.iterator(), targetHeight)
applyTipHeight(targetHeight)
}
@ -304,7 +309,7 @@ class DarksideTestCoordinator(val wallet: TestWallet) {
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/after-small-reorg.txt"
private const val largeReorg =
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/after-large-reorg.txt"
private const val DEFAULT_START_HEIGHT = 663150
private val DEFAULT_START_HEIGHT = BlockHeight.new(ZcashNetwork.Mainnet, 663150)
private const val DEFAULT_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"
}

View File

@ -10,6 +10,7 @@ import cash.z.ecc.android.sdk.db.entity.isPending
import cash.z.ecc.android.sdk.internal.Twig
import cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService
import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.WalletBalance
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.tool.DerivationTool
@ -36,7 +37,7 @@ class TestWallet(
val alias: String = "TestWallet",
val network: ZcashNetwork = ZcashNetwork.Testnet,
val host: String = network.defaultHost,
startHeight: Int? = null,
startHeight: BlockHeight? = null,
val port: Int = network.defaultPort
) {
constructor(
@ -109,7 +110,7 @@ class TestWallet(
suspend fun send(address: String = transparentAddress, memo: String = "", amount: Zatoshi = Zatoshi(500L), fromAccountIndex: Int = 0): TestWallet {
Twig.sprout("$alias sending")
synchronizer.sendToAddress(shieldedSpendingKey, amount, address, memo, fromAccountIndex)
.takeWhile { it.isPending() }
.takeWhile { it.isPending(null) }
.collect {
twig("Updated transaction: $it")
}
@ -117,14 +118,14 @@ class TestWallet(
return this
}
suspend fun rewindToHeight(height: Int): TestWallet {
suspend fun rewindToHeight(height: BlockHeight): TestWallet {
synchronizer.rewindToNearestHeight(height, false)
return this
}
suspend fun shieldFunds(): TestWallet {
twig("checking $transparentAddress for transactions!")
synchronizer.refreshUtxos(transparentAddress, 935000).let { count ->
synchronizer.refreshUtxos(transparentAddress, BlockHeight.new(ZcashNetwork.Mainnet, 935000)).let { count ->
twig("FOUND $count new UTXOs")
}
@ -163,13 +164,13 @@ class TestWallet(
}
}
enum class Backups(val seedPhrase: String, val testnetBirthday: Int, val mainnetBirthday: Int) {
enum class Backups(val seedPhrase: String, val testnetBirthday: BlockHeight, val mainnetBirthday: BlockHeight) {
// TODO: get the proper birthday values for these wallets
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", 1_355_928, 1_000_000),
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", 1_330_190, 1_000_000),
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", 1_000_000, 991645),
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", 1_330_190, 1_000_000),
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", 1_330_190, 1_000_000),
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", BlockHeight.new(ZcashNetwork.Testnet, 1_355_928), BlockHeight.new(ZcashNetwork.Mainnet, 1_000_000)),
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", BlockHeight.new(ZcashNetwork.Testnet, 1_330_190), BlockHeight.new(ZcashNetwork.Mainnet, 1_000_000)),
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", BlockHeight.new(ZcashNetwork.Testnet, 1_000_000), BlockHeight.new(ZcashNetwork.Mainnet, 991645)),
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", BlockHeight.new(ZcashNetwork.Testnet, 1_330_190), BlockHeight.new(ZcashNetwork.Mainnet, 1_000_000)),
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", BlockHeight.new(ZcashNetwork.Testnet, 1_330_190), BlockHeight.new(ZcashNetwork.Mainnet, 1_000_000)),
;
}
}

View File

@ -10,6 +10,7 @@ import cash.z.ecc.android.sdk.internal.TroubleshootingTwig
import cash.z.ecc.android.sdk.internal.Twig
import cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService
import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.sdk.type.ZcashNetwork
import kotlinx.coroutines.flow.collect
@ -93,10 +94,10 @@ class SampleCodeTest {
// ///////////////////////////////////////////////////
// Download compact block range
@Test fun getBlockRange() {
val blockRange = 500_000..500_009
val blockRange = BlockHeight.new(ZcashNetwork.Mainnet, 500_000)..BlockHeight.new(ZcashNetwork.Mainnet, 500_009)
val lightwalletService = LightWalletGrpcService(context, lightwalletdHost)
val blocks = lightwalletService.getBlockRange(blockRange)
assertEquals(blockRange.count(), blocks.size)
assertEquals(blockRange.endInclusive.value - blockRange.start.value, blocks.size)
blocks.forEachIndexed { i, block ->
log("Block #$i: height:${block.height} hash:${block.hash.toByteArray().toHex()}")

View File

@ -51,7 +51,7 @@ class GetBalanceFragment : BaseDemoFragment<FragmentGetBalanceBinding>() {
runBlocking {
Initializer.new(requireApplicationContext(), null) {
it.setNetwork(ZcashNetwork.fromResources(requireApplicationContext()))
it.importWallet(viewingKey, network = ZcashNetwork.fromResources(requireApplicationContext()))
it.newWallet(viewingKey, network = ZcashNetwork.fromResources(requireApplicationContext()))
}
}.let { initializer ->
synchronizer = Synchronizer.newBlocking(initializer)
@ -81,7 +81,7 @@ class GetBalanceFragment : BaseDemoFragment<FragmentGetBalanceBinding>() {
private fun onStatus(status: Synchronizer.Status) {
binding.textStatus.text = "Status: $status"
val balance = synchronizer.saplingBalances.value
val balance: WalletBalance? = synchronizer.saplingBalances.value
if (null == balance) {
binding.textBalance.text = "Calculating balance..."
} else {

View File

@ -7,11 +7,15 @@ import android.view.View
import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentGetBlockBinding
import cash.z.ecc.android.sdk.demoapp.ext.requireApplicationContext
import cash.z.ecc.android.sdk.demoapp.util.fromResources
import cash.z.ecc.android.sdk.demoapp.util.mainActivity
import cash.z.ecc.android.sdk.demoapp.util.toHtml
import cash.z.ecc.android.sdk.demoapp.util.toRelativeTime
import cash.z.ecc.android.sdk.demoapp.util.withCommas
import cash.z.ecc.android.sdk.ext.toHex
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.type.ZcashNetwork
import kotlin.math.min
/**
* Retrieves a compact block from the lightwalletd service and displays basic information about it.
@ -20,7 +24,7 @@ import cash.z.ecc.android.sdk.ext.toHex
*/
class GetBlockFragment : BaseDemoFragment<FragmentGetBlockBinding>() {
private fun setBlockHeight(blockHeight: Int) {
private fun setBlockHeight(blockHeight: BlockHeight) {
val blocks =
lightwalletService?.getBlockRange(blockHeight..blockHeight)
val block = blocks?.firstOrNull()
@ -38,8 +42,11 @@ class GetBlockFragment : BaseDemoFragment<FragmentGetBlockBinding>() {
}
private fun onApply(_unused: View? = null) {
val network = ZcashNetwork.fromResources(requireApplicationContext())
val newHeight = min(binding.textBlockHeight.text.toString().toLongOrNull() ?: network.saplingActivationHeight.value, network.saplingActivationHeight.value)
try {
setBlockHeight(binding.textBlockHeight.text.toString().toInt())
setBlockHeight(BlockHeight.new(network, newHeight))
} catch (t: Throwable) {
toast("Error: $t")
}

View File

@ -8,9 +8,13 @@ import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment
import cash.z.ecc.android.sdk.demoapp.R
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentGetBlockRangeBinding
import cash.z.ecc.android.sdk.demoapp.ext.requireApplicationContext
import cash.z.ecc.android.sdk.demoapp.util.fromResources
import cash.z.ecc.android.sdk.demoapp.util.mainActivity
import cash.z.ecc.android.sdk.demoapp.util.toRelativeTime
import cash.z.ecc.android.sdk.demoapp.util.withCommas
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.type.ZcashNetwork
import kotlin.math.max
/**
* Retrieves a range of compact block from the lightwalletd service and displays basic information
@ -20,7 +24,7 @@ import cash.z.ecc.android.sdk.demoapp.util.withCommas
*/
class GetBlockRangeFragment : BaseDemoFragment<FragmentGetBlockRangeBinding>() {
private fun setBlockRange(blockRange: IntRange) {
private fun setBlockRange(blockRange: ClosedRange<BlockHeight>) {
val start = System.currentTimeMillis()
val blocks =
lightwalletService?.getBlockRange(blockRange)
@ -69,8 +73,9 @@ class GetBlockRangeFragment : BaseDemoFragment<FragmentGetBlockRangeBinding>() {
}
private fun onApply(_unused: View) {
val start = binding.textStartHeight.text.toString().toInt()
val end = binding.textEndHeight.text.toString().toInt()
val network = ZcashNetwork.fromResources(requireApplicationContext())
val start = max(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) {
try {
with(binding.buttonApply) {
@ -78,7 +83,7 @@ class GetBlockRangeFragment : BaseDemoFragment<FragmentGetBlockRangeBinding>() {
setText(R.string.loading)
binding.textInfo.setText(R.string.loading)
post {
setBlockRange(start..end)
setBlockRange(BlockHeight.new(network, start)..BlockHeight.new(network, end))
isEnabled = true
setText(R.string.apply)
}

View File

@ -50,7 +50,7 @@ class ListTransactionsFragment : BaseDemoFragment<FragmentListTransactionsBindin
initializer = runBlocking {
Initializer.new(requireApplicationContext()) {
runBlocking { it.importWallet(seed, network = ZcashNetwork.fromResources(requireApplicationContext())) }
runBlocking { it.newWallet(seed, network = ZcashNetwork.fromResources(requireApplicationContext())) }
it.setNetwork(ZcashNetwork.fromResources(requireApplicationContext()))
}
}

View File

@ -1,5 +1,6 @@
package cash.z.ecc.android.sdk.demoapp.demos.listutxos
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@ -13,13 +14,13 @@ import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.block.CompactBlockProcessor
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment
import cash.z.ecc.android.sdk.demoapp.DemoConstants
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentListUtxosBinding
import cash.z.ecc.android.sdk.demoapp.ext.requireApplicationContext
import cash.z.ecc.android.sdk.demoapp.util.fromResources
import cash.z.ecc.android.sdk.demoapp.util.mainActivity
import cash.z.ecc.android.sdk.ext.collectWith
import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.sdk.type.ZcashNetwork
import kotlinx.coroutines.Dispatchers
@ -27,6 +28,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlin.math.max
/**
* ===============================================================================================
@ -63,7 +65,7 @@ class ListUtxosFragment : BaseDemoFragment<FragmentListUtxosBinding>() {
seed = Mnemonics.MnemonicCode(sharedViewModel.seedPhrase.value).toSeed()
initializer = runBlocking {
Initializer.new(requireApplicationContext()) {
runBlocking { it.importWallet(seed, network = ZcashNetwork.fromResources(requireApplicationContext())) }
runBlocking { it.newWallet(seed, network = ZcashNetwork.fromResources(requireApplicationContext())) }
it.alias = "Demo_Utxos"
}
}
@ -78,7 +80,7 @@ class ListUtxosFragment : BaseDemoFragment<FragmentListUtxosBinding>() {
fun initUi() {
binding.inputAddress.setText(address)
binding.inputRangeStart.setText(ZcashNetwork.fromResources(requireApplicationContext()).saplingActivationHeight.toString())
binding.inputRangeEnd.setText(DemoConstants.utxoEndHeight.toString())
binding.inputRangeEnd.setText(getUxtoEndHeight(requireApplicationContext()).value.toString())
binding.buttonLoad.setOnClickListener {
mainActivity()?.hideKeyboard()
@ -91,24 +93,28 @@ class ListUtxosFragment : BaseDemoFragment<FragmentListUtxosBinding>() {
fun downloadTransactions() {
binding.textStatus.text = "loading..."
binding.textStatus.post {
val network = ZcashNetwork.fromResources(requireApplicationContext())
binding.textStatus.requestFocus()
val addressToUse = binding.inputAddress.text.toString()
val startToUse = binding.inputRangeStart.text.toString().toIntOrNull() ?: ZcashNetwork.fromResources(requireApplicationContext()).saplingActivationHeight
val endToUse = binding.inputRangeEnd.text.toString().toIntOrNull() ?: DemoConstants.utxoEndHeight
val startToUse = max(binding.inputRangeStart.text.toString().toLongOrNull() ?: network.saplingActivationHeight.value, network.saplingActivationHeight.value)
val endToUse = binding.inputRangeEnd.text.toString().toLongOrNull()
?: getUxtoEndHeight(requireApplicationContext()).value
var allStart = now
twig("loading transactions in range $startToUse..$endToUse")
val txids = lightwalletService?.getTAddressTransactions(addressToUse, startToUse..endToUse)
val txids = lightwalletService?.getTAddressTransactions(addressToUse, BlockHeight.new(network, startToUse)..BlockHeight.new(network, endToUse))
var delta = now - allStart
updateStatus("found ${txids?.size} transactions in ${delta}ms.", false)
txids?.map {
it.data.apply {
try {
runBlocking { initializer.rustBackend.decryptAndStoreTransaction(toByteArray()) }
} catch (t: Throwable) {
twig("failed to decrypt and store transaction due to: $t")
}
}
// Disabled during migration to newer SDK version; this appears to have been
// leveraging non-public APIs in the SDK so perhaps should be removed
// it.data.apply {
// try {
// runBlocking { initializer.rustBackend.decryptAndStoreTransaction(toByteArray()) }
// } catch (t: Throwable) {
// twig("failed to decrypt and store transaction due to: $t")
// }
// }
}?.let { txData ->
// Disabled during migration to newer SDK version; this appears to have been
// leveraging non-public APIs in the SDK so perhaps should be removed
@ -246,4 +252,9 @@ class ListUtxosFragment : BaseDemoFragment<FragmentListUtxosBinding>() {
}
}
}
@Suppress("MagicNumber")
private fun getUxtoEndHeight(context: Context): BlockHeight {
return BlockHeight.new(ZcashNetwork.fromResources(context), 968085L)
}
}

View File

@ -66,7 +66,7 @@ class SendFragment : BaseDemoFragment<FragmentSendBinding>() {
runBlocking {
Initializer.new(requireApplicationContext()) {
runBlocking { it.importWallet(seed, network = ZcashNetwork.fromResources(requireApplicationContext())) }
runBlocking { it.newWallet(seed, network = ZcashNetwork.fromResources(requireApplicationContext())) }
it.setNetwork(ZcashNetwork.fromResources(requireApplicationContext()))
}
}.let { initializer ->

View File

@ -1,7 +1,7 @@
package cash.z.ecc.android.sdk.demoapp
object DemoConstants {
val utxoEndHeight: Int = 968085
val sendAmount: Double = 0.000018
// corresponds to address: zs15tzaulx5weua5c7l47l4pku2pw9fzwvvnsp4y80jdpul0y3nwn5zp7tmkcclqaca3mdjqjkl7hx

View File

@ -3,7 +3,7 @@ package cash.z.ecc.android.sdk
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.filters.SmallTest
import cash.z.ecc.android.sdk.tool.WalletBirthdayTool
import cash.z.ecc.android.sdk.tool.CheckpointTool
import cash.z.ecc.android.sdk.type.ZcashNetwork
import kotlinx.coroutines.runBlocking
import org.json.JSONObject
@ -57,7 +57,7 @@ class AssetTest {
private fun assertFileContents(network: ZcashNetwork, files: Array<String>?) {
files?.map { filename ->
val filePath = "${WalletBirthdayTool.birthdayDirectory(network)}/$filename"
val filePath = "${CheckpointTool.checkpointDirectory(network)}/$filename"
ApplicationProvider.getApplicationContext<Context>().assets.open(filePath)
.use { inputSteam ->
inputSteam.bufferedReader().use { bufferedReader ->
@ -82,7 +82,7 @@ class AssetTest {
assertEquals(
"File: ${it.filename}",
WalletBirthdayTool.birthdayHeight(it.filename),
CheckpointTool.checkpointHeightFromFilename(network, it.filename),
jsonObject.getInt("height")
)
@ -94,9 +94,9 @@ class AssetTest {
companion object {
fun listAssets(network: ZcashNetwork) = runBlocking {
WalletBirthdayTool.listBirthdayDirectoryContents(
CheckpointTool.listCheckpointDirectoryContents(
ApplicationProvider.getApplicationContext<Context>(),
WalletBirthdayTool.birthdayDirectory(network)
CheckpointTool.checkpointDirectory(network)
)
}
}

View File

@ -14,14 +14,14 @@ fun Initializer.Config.seedPhrase(seedPhrase: String, network: ZcashNetwork) {
}
object BlockExplorer {
suspend fun fetchLatestHeight(): Int {
suspend fun fetchLatestHeight(): Long {
val client = OkHttpClient()
val request = Request.Builder()
.url("https://api.blockchair.com/zcash/blocks?limit=1")
.build()
val result = client.newCall(request).await()
val body = result.body?.string()
return JSONObject(body).getJSONArray("data").getJSONObject(0).getInt("id")
return JSONObject(body).getJSONArray("data").getJSONObject(0).getLong("id")
}
}

View File

@ -3,6 +3,7 @@ package cash.z.ecc.android.sdk.integration
import cash.z.ecc.android.sdk.annotation.MaintainedTest
import cash.z.ecc.android.sdk.annotation.TestPurpose
import cash.z.ecc.android.sdk.ext.BlockExplorer
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.type.ZcashNetwork
import cash.z.ecc.android.sdk.util.TestWallet
import kotlinx.coroutines.runBlocking
@ -61,7 +62,7 @@ class SanityTest(
assertEquals(
"$name has invalid birthday height",
birthday,
wallet.initializer.birthday.height
wallet.initializer.checkpoint.height
)
}
@ -97,7 +98,7 @@ class SanityTest(
val info = wallet.connectionInfo
assertTrue(
"$info\n ${wallet.networkName} Lightwalletd is too far behind. Downloader height $downloaderHeight is more than 10 blocks behind block explorer height $expectedHeight",
expectedHeight - 10 < downloaderHeight
expectedHeight - 10 < downloaderHeight.value
)
}
}
@ -105,9 +106,9 @@ class SanityTest(
@Test
fun testSingleBlockDownload() = runBlocking {
// fetch block directly because the synchronizer hasn't started, yet
val height = 1_000_000
val height = BlockHeight.new(wallet.network, 1_000_000)
val block = wallet.service.getBlockRange(height..height)[0]
assertTrue("$networkName failed to return a proper block. Height was ${block.height} but we expected $height", block.height.toInt() == height)
assertTrue("$networkName failed to return a proper block. Height was ${block.height} but we expected $height", block.height == height.value)
}
companion object {

View File

@ -38,7 +38,7 @@ class SmokeTest {
@Test
fun testBirthday() {
Assert.assertEquals("Invalid birthday height", 1_320_000, wallet.initializer.birthday.height)
Assert.assertEquals("Invalid birthday height", 1_320_000, wallet.initializer.checkpoint.height)
}
@Test

View File

@ -12,10 +12,11 @@ import cash.z.ecc.android.sdk.internal.TroubleshootingTwig
import cash.z.ecc.android.sdk.internal.Twig
import cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService
import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.test.ScopedTest
import cash.z.ecc.android.sdk.tool.CheckpointTool
import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.sdk.tool.WalletBirthdayTool
import cash.z.ecc.android.sdk.type.ZcashNetwork
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.filter
@ -49,7 +50,7 @@ class TestnetIntegrationTest : ScopedTest() {
@Test
fun testLoadBirthday() {
val (height, hash, time, tree) = runBlocking {
WalletBirthdayTool.loadNearest(
CheckpointTool.loadNearest(
context,
synchronizer.network,
saplingActivation + 1
@ -118,7 +119,7 @@ class TestnetIntegrationTest : ScopedTest() {
init { Twig.plant(TroubleshootingTwig()) }
const val host = "lightwalletd.testnet.z.cash"
private const val birthdayHeight = 963150
private const val birthdayHeight = 963150L
private const val targetHeight = 663250
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"
val seed = "cash.z.ecc.android.sdk.integration.IntegrationTest.seed.value.64bytes".toByteArray()
@ -129,7 +130,7 @@ class TestnetIntegrationTest : ScopedTest() {
private val initializer = runBlocking {
Initializer.new(context) { config ->
config.setNetwork(ZcashNetwork.Testnet, host)
runBlocking { config.importWallet(seed, birthdayHeight, ZcashNetwork.Testnet) }
runBlocking { config.importWallet(seed, BlockHeight.new(ZcashNetwork.Testnet, birthdayHeight), ZcashNetwork.Testnet) }
}
}
private lateinit var synchronizer: Synchronizer

View File

@ -11,6 +11,7 @@ import cash.z.ecc.android.sdk.internal.block.CompactBlockStore
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.twig
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.test.ScopedTest
import cash.z.ecc.android.sdk.type.ZcashNetwork
import kotlinx.coroutines.delay
@ -70,7 +71,7 @@ class ChangeServiceTest : ScopedTest() {
@Test
fun testCleanSwitch() = runBlocking {
downloader.changeService(otherService)
val result = downloader.downloadBlockRange(900_000..901_000)
val result = downloader.downloadBlockRange(BlockHeight.new(network, 900_000)..BlockHeight.new(network, 901_000))
assertEquals(1_001, result)
}
@ -81,7 +82,7 @@ class ChangeServiceTest : ScopedTest() {
@Test
@Ignore("This test is broken")
fun testSwitchWhileActive() = runBlocking {
val start = 900_000
val start = BlockHeight.new(ZcashNetwork.Mainnet, 900_000)
val count = 5
val differentiators = mutableListOf<String>()
var initialValue = downloader.getServerInfo().buildUser

View File

@ -0,0 +1,47 @@
package cash.z.ecc.android.sdk.internal
import androidx.test.filters.SmallTest
import cash.z.ecc.android.sdk.internal.model.Checkpoint
import cash.z.ecc.fixture.CheckpointFixture
import cash.z.ecc.fixture.toJson
import org.json.JSONObject
import org.junit.Assert.assertEquals
import org.junit.Test
class CheckpointTest {
@Test
@SmallTest
fun deserialize() {
val fixtureCheckpoint = CheckpointFixture.new()
val deserialized = Checkpoint.from(CheckpointFixture.NETWORK, fixtureCheckpoint.toJson())
assertEquals(fixtureCheckpoint, deserialized)
}
@Test
@SmallTest
fun epoch_seconds_as_long_that_would_overflow_int() {
val jsonString = CheckpointFixture.new(time = Long.MAX_VALUE).toJson()
Checkpoint.from(CheckpointFixture.NETWORK, jsonString).also {
assertEquals(Long.MAX_VALUE, it.epochSeconds)
}
}
@Test
@SmallTest
fun parse_height_as_long_that_would_overflow_int() {
val jsonString = JSONObject().apply {
put(Checkpoint.KEY_VERSION, Checkpoint.VERSION_1)
put(Checkpoint.KEY_HEIGHT, UInt.MAX_VALUE.toLong())
put(Checkpoint.KEY_HASH, CheckpointFixture.HASH)
put(Checkpoint.KEY_EPOCH_SECONDS, CheckpointFixture.EPOCH_SECONDS)
put(Checkpoint.KEY_TREE, CheckpointFixture.TREE)
}.toString()
Checkpoint.from(CheckpointFixture.NETWORK, jsonString).also {
assertEquals(UInt.MAX_VALUE.toLong(), it.height.value)
}
}
}

View File

@ -1,30 +0,0 @@
package cash.z.ecc.android.sdk.internal
import androidx.test.filters.SmallTest
import cash.z.ecc.android.sdk.type.WalletBirthday
import cash.z.ecc.fixture.WalletBirthdayFixture
import cash.z.ecc.fixture.toJson
import org.junit.Assert.assertEquals
import org.junit.Test
class WalletBirthdayTest {
@Test
@SmallTest
fun deserialize() {
val fixtureBirthday = WalletBirthdayFixture.new()
val deserialized = WalletBirthday.from(fixtureBirthday.toJson())
assertEquals(fixtureBirthday, deserialized)
}
@Test
@SmallTest
fun epoch_seconds_as_long_that_would_overflow_int() {
val jsonString = WalletBirthdayFixture.new(time = Long.MAX_VALUE).toJson()
WalletBirthday.from(jsonString).also {
assertEquals(Long.MAX_VALUE, it.time)
}
}
}

View File

@ -2,6 +2,7 @@ package cash.z.ecc.android.sdk.jni
import cash.z.ecc.android.sdk.annotation.MaintainedTest
import cash.z.ecc.android.sdk.annotation.TestPurpose
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.type.ZcashNetwork
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
@ -14,9 +15,9 @@ import org.junit.runners.Parameterized
*/
@MaintainedTest(TestPurpose.REGRESSION)
@RunWith(Parameterized::class)
class BranchIdTest(
class BranchIdTest internal constructor(
private val networkName: String,
private val height: Int,
private val height: BlockHeight,
private val branchId: Long,
private val branchHex: String,
private val rustBackend: RustBackendWelding
@ -44,8 +45,8 @@ class BranchIdTest(
// is an abnormal use of the SDK because this really should run at the rust level
// However, due to quirks on certain devices, we created this test at the Android level,
// as a sanity check
val testnetBackend = runBlocking { RustBackend.init("", "", "", ZcashNetwork.Testnet) }
val mainnetBackend = runBlocking { RustBackend.init("", "", "", ZcashNetwork.Mainnet) }
val testnetBackend = runBlocking { RustBackend.init("", "", "", ZcashNetwork.Testnet, ZcashNetwork.Testnet.saplingActivationHeight) }
val mainnetBackend = runBlocking { RustBackend.init("", "", "", ZcashNetwork.Mainnet, ZcashNetwork.Mainnet.saplingActivationHeight) }
return listOf(
// Mainnet Cases
arrayOf("Sapling", 419_200, 1991772603L, "76b809bb", mainnetBackend),

View File

@ -3,7 +3,9 @@ package cash.z.ecc.android.sdk.sample
import androidx.test.filters.LargeTest
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.type.ZcashNetwork
import cash.z.ecc.android.sdk.type.ZcashNetwork.Testnet
import cash.z.ecc.android.sdk.util.TestWallet
import kotlinx.coroutines.delay
@ -73,7 +75,7 @@ class TransparentRestoreSample {
// wallet.rewindToHeight(1343500).join(45_000)
val wallet = TestWallet(TestWallet.Backups.SAMPLE_WALLET, alias = "WalletC")
// wallet.sync().rewindToHeight(1339178).join(10000)
wallet.sync().rewindToHeight(1339178).send(
wallet.sync().rewindToHeight(BlockHeight.new(ZcashNetwork.Testnet, 1339178)).send(
"ztestsapling17zazsl8rryl8kjaqxnr2r29rw9d9a2mud37ugapm0s8gmyv0ue43h9lqwmhdsp3nu9dazeqfs6l",
"is send broken?"
).join(5)
@ -85,7 +87,15 @@ class TransparentRestoreSample {
@LargeTest
@Ignore("This test is extremely slow")
fun kris() = runBlocking<Unit> {
val wallet0 = TestWallet(TestWallet.Backups.SAMPLE_WALLET.seedPhrase, "tmpabc", Testnet, startHeight = 1330190)
val wallet0 = TestWallet(
TestWallet.Backups.SAMPLE_WALLET.seedPhrase,
"tmpabc",
Testnet,
startHeight = BlockHeight.new(
ZcashNetwork.Testnet,
1330190
)
)
// val wallet1 = SimpleWallet(WALLET0_PHRASE, "Wallet1")
wallet0.sync() // .shieldFunds()
@ -107,7 +117,7 @@ class TransparentRestoreSample {
*/
// @Test
fun hasFunds() = runBlocking<Unit> {
val walletSandbox = TestWallet(TestWallet.Backups.SAMPLE_WALLET.seedPhrase, "WalletC", Testnet, startHeight = 1330190)
val walletSandbox = TestWallet(TestWallet.Backups.SAMPLE_WALLET.seedPhrase, "WalletC", Testnet, startHeight = BlockHeight.new(ZcashNetwork.Testnet, 1330190))
// val job = walletA.walletScope.launch {
// twig("Syncing WalletA")
// walletA.sync()
@ -125,7 +135,7 @@ class TransparentRestoreSample {
// send z->t
// walletA.send(TX_VALUE, walletA.transparentAddress, "${TransparentRestoreSample::class.java.simpleName} z->t")
walletSandbox.rewindToHeight(1339178)
walletSandbox.rewindToHeight(BlockHeight.new(ZcashNetwork.Testnet, 1339178))
twig("Done REWINDING!")
twig("T-ADDR (for the win!): ${walletSandbox.transparentAddress}")
delay(500)

View File

@ -4,18 +4,19 @@ import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import cash.z.ecc.android.sdk.tool.WalletBirthdayTool.IS_FALLBACK_ON_FAILURE
import cash.z.ecc.android.sdk.tool.CheckpointTool.IS_FALLBACK_ON_FAILURE
import cash.z.ecc.android.sdk.type.ZcashNetwork
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class WalletBirthdayToolTest {
class CheckpointToolTest {
@Test
@SmallTest
fun birthday_height_from_filename() {
assertEquals(123, WalletBirthdayTool.birthdayHeight("123.json"))
assertEquals(123, CheckpointTool.checkpointHeightFromFilename(ZcashNetwork.Mainnet, "123.json"))
}
@Test
@ -27,13 +28,14 @@ class WalletBirthdayToolTest {
val context = ApplicationProvider.getApplicationContext<Context>()
val birthday = runBlocking {
WalletBirthdayTool.getFirstValidWalletBirthday(
CheckpointTool.getFirstValidWalletBirthday(
context,
ZcashNetwork.Mainnet,
directory,
listOf("1300000.json", "1290000.json")
)
}
assertEquals(1300000, birthday.height)
assertEquals(1300000, birthday.height.value)
}
@Test
@ -46,12 +48,13 @@ class WalletBirthdayToolTest {
val directory = "co.electriccoin.zcash/checkpoint/badnet"
val context = ApplicationProvider.getApplicationContext<Context>()
val birthday = runBlocking {
WalletBirthdayTool.getFirstValidWalletBirthday(
CheckpointTool.getFirstValidWalletBirthday(
context,
ZcashNetwork.Mainnet,
directory,
listOf("1300000.json", "1290000.json")
)
}
assertEquals(1290000, birthday.height)
assertEquals(1290000, birthday.height.value)
}
}

View File

@ -6,9 +6,10 @@ import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.internal.TroubleshootingTwig
import cash.z.ecc.android.sdk.internal.Twig
import cash.z.ecc.android.sdk.internal.ext.deleteSuspend
import cash.z.ecc.android.sdk.internal.model.Checkpoint
import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.tool.WalletBirthdayTool
import cash.z.ecc.android.sdk.type.WalletBirthday
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.tool.CheckpointTool
import cash.z.ecc.android.sdk.type.ZcashNetwork
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flow
@ -30,7 +31,7 @@ class BalancePrinterUtil {
private val network = ZcashNetwork.Mainnet
private val downloadBatchSize = 9_000
private val birthdayHeight = 523240
private val birthdayHeight = BlockHeight.new(network, 523240)
private val mnemonics = SimpleMnemonics()
private val context = InstrumentationRegistry.getInstrumentation().context
@ -46,14 +47,14 @@ class BalancePrinterUtil {
// private val rustBackend = RustBackend.init(context, cacheDbName, dataDbName)
private lateinit var birthday: WalletBirthday
private lateinit var birthday: Checkpoint
private var synchronizer: Synchronizer? = null
@Before
fun setup() {
Twig.plant(TroubleshootingTwig())
cacheBlocks()
birthday = runBlocking { WalletBirthdayTool.loadNearest(context, network, birthdayHeight) }
birthday = runBlocking { CheckpointTool.loadNearest(context, network, birthdayHeight) }
}
private fun cacheBlocks() = runBlocking {

View File

@ -6,6 +6,8 @@ import cash.z.ecc.android.sdk.SdkSynchronizer
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.internal.TroubleshootingTwig
import cash.z.ecc.android.sdk.internal.Twig
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.type.ZcashNetwork
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
@ -36,7 +38,7 @@ class DataDbScannerUtil {
// private val rustBackend = RustBackend.init(context, cacheDbName, dataDbName)
private val birthdayHeight = 600_000
private val birthdayHeight = 600_000L
private lateinit var synchronizer: Synchronizer
@Before
@ -67,7 +69,10 @@ class DataDbScannerUtil {
val initializer = runBlocking {
Initializer.new(context) {
it.setBirthdayHeight(
birthdayHeight
BlockHeight.new(
ZcashNetwork.Mainnet,
birthdayHeight
)
)
}
}

View File

@ -10,6 +10,7 @@ import cash.z.ecc.android.sdk.db.entity.isPending
import cash.z.ecc.android.sdk.internal.Twig
import cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService
import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.WalletBalance
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.tool.DerivationTool
@ -36,7 +37,7 @@ class TestWallet(
val alias: String = "TestWallet",
val network: ZcashNetwork = ZcashNetwork.Testnet,
val host: String = network.defaultHost,
startHeight: Int? = null,
startHeight: BlockHeight? = null,
val port: Int = network.defaultPort
) {
constructor(
@ -109,7 +110,7 @@ class TestWallet(
suspend fun send(address: String = transparentAddress, memo: String = "", amount: Zatoshi = Zatoshi(500L), fromAccountIndex: Int = 0): TestWallet {
Twig.sprout("$alias sending")
synchronizer.sendToAddress(shieldedSpendingKey, amount, address, memo, fromAccountIndex)
.takeWhile { it.isPending() }
.takeWhile { it.isPending(null) }
.collect {
twig("Updated transaction: $it")
}
@ -117,14 +118,14 @@ class TestWallet(
return this
}
suspend fun rewindToHeight(height: Int): TestWallet {
suspend fun rewindToHeight(height: BlockHeight): TestWallet {
synchronizer.rewindToNearestHeight(height, false)
return this
}
suspend fun shieldFunds(): TestWallet {
twig("checking $transparentAddress for transactions!")
synchronizer.refreshUtxos(transparentAddress, 935000).let { count ->
synchronizer.refreshUtxos(transparentAddress, BlockHeight.new(ZcashNetwork.Mainnet, 935000)).let { count ->
twig("FOUND $count new UTXOs")
}
@ -163,13 +164,13 @@ class TestWallet(
}
}
enum class Backups(val seedPhrase: String, val testnetBirthday: Int, val mainnetBirthday: Int) {
enum class Backups(val seedPhrase: String, val testnetBirthday: BlockHeight, val mainnetBirthday: BlockHeight) {
// TODO: get the proper birthday values for these wallets
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", 1_355_928, 1_000_000),
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", 1_330_190, 1_000_000),
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", 1_000_000, 991645),
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", 1_330_190, 1_000_000),
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", 1_330_190, 1_000_000),
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", BlockHeight.new(ZcashNetwork.Testnet, 1_355_928), BlockHeight.new(ZcashNetwork.Mainnet, 1_000_000)),
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", BlockHeight.new(ZcashNetwork.Testnet, 1_330_190), BlockHeight.new(ZcashNetwork.Mainnet, 1_000_000)),
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", BlockHeight.new(ZcashNetwork.Testnet, 1_000_000), BlockHeight.new(ZcashNetwork.Mainnet, 991645)),
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", BlockHeight.new(ZcashNetwork.Testnet, 1_330_190), BlockHeight.new(ZcashNetwork.Mainnet, 1_000_000)),
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", BlockHeight.new(ZcashNetwork.Testnet, 1_330_190), BlockHeight.new(ZcashNetwork.Mainnet, 1_000_000)),
;
}
}

View File

@ -5,6 +5,7 @@ import cash.z.ecc.android.sdk.internal.TroubleshootingTwig
import cash.z.ecc.android.sdk.internal.Twig
import cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService
import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.type.ZcashNetwork
import org.junit.Ignore
import org.junit.Test
@ -23,7 +24,12 @@ class TransactionCounterUtil {
@Ignore("This test is broken")
fun testBlockSize() {
val sizes = mutableMapOf<Int, Int>()
service.getBlockRange(900_000..910_000).forEach { b ->
service.getBlockRange(
BlockHeight.new(ZcashNetwork.Mainnet, 900_000)..BlockHeight.new(
ZcashNetwork.Mainnet,
910_000
)
).forEach { b ->
twig("h: ${b.header.size()}")
val s = b.serializedSize
sizes[s] = (sizes[s] ?: 0) + 1
@ -38,7 +44,12 @@ class TransactionCounterUtil {
val outputCounts = mutableMapOf<Int, Int>()
var totalOutputs = 0
var totalTxs = 0
service.getBlockRange(900_000..950_000).forEach { b ->
service.getBlockRange(
BlockHeight.new(ZcashNetwork.Mainnet, 900_000)..BlockHeight.new(
ZcashNetwork.Mainnet,
950_000
)
).forEach { b ->
b.header.size()
b.vtxList.map { it.outputsCount }.forEach { oCount ->
outputCounts[oCount] = (outputCounts[oCount] ?: 0) + oCount.coerceAtLeast(1)

View File

@ -6,31 +6,34 @@ import cash.z.ecc.android.sdk.internal.KEY_HEIGHT
import cash.z.ecc.android.sdk.internal.KEY_TREE
import cash.z.ecc.android.sdk.internal.KEY_VERSION
import cash.z.ecc.android.sdk.internal.VERSION_1
import cash.z.ecc.android.sdk.type.WalletBirthday
import cash.z.ecc.android.sdk.internal.model.Checkpoint
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.type.ZcashNetwork
import org.json.JSONObject
object WalletBirthdayFixture {
object CheckpointFixture {
val NETWORK = ZcashNetwork.Mainnet
// These came from the mainnet 1500000.json file
const val HEIGHT = 1500000
val HEIGHT = BlockHeight.new(ZcashNetwork.Mainnet, 1500000L)
const val HASH = "00000000019e5b25a95c7607e7789eb326fddd69736970ebbe1c7d00247ef902"
const val EPOCH_SECONDS = 1639913234L
@Suppress("MaxLineLength")
const val TREE = "01ce183032b16ed87fcc5052a42d908376526126346567773f55bc58a63e4480160013000001bae5112769a07772345dd402039f2949c457478fe9327363ff631ea9d78fb80d0177c0b6c21aa9664dc255336ed450914088108c38a9171c85875b4e53d31b3e140171add6f9129e124651ca894aa842a3c71b1738f3ee2b7ba829106524ef51e62101f9cebe2141ee9d0a3f3a3e28bce07fa6b6e1c7b42c01cc4fe611269e9d52da540001d0adff06de48569129bd2a211e3253716362da97270d3504d9c1b694689ebe3c0122aaaea90a7fa2773b8166937310f79a4278b25d759128adf3138d052da3725b0137fb2cbc176075a45db2a3c32d3f78e669ff2258fd974e99ec9fb314d7fd90180165aaee3332ea432d13a9398c4863b38b8a7a491877a5c46b0802dcd88f7e324301a9a262f8b92efc2e0e3e4bd1207486a79d62e87b4ab9cc41814d62a23c4e28040001e3c4ee998682df5c5e230d6968e947f83d0c03682f0cfc85f1e6ec8e8552c95a000155989fed7a8cc7a0d479498d6881ca3bafbe05c7095110f85c64442d6a06c25c0185cd8c141e620eda0ca0516f42240aedfabdf9189c8c6ac834b7bdebc171331d01ecceb776c043662617d62646ee60985521b61c0b860f3a9731e66ef74ed8fb320118f64df255c9c43db708255e7bf6bffd481e5c2f38fe9ed8f3d189f7f9cf2644"
fun new(
height: Int = HEIGHT,
internal fun new(
height: BlockHeight = HEIGHT,
hash: String = HASH,
time: Long = EPOCH_SECONDS,
tree: String = TREE
) = WalletBirthday(height = height, hash = hash, time = time, tree = tree)
) = Checkpoint(height = height, hash = hash, epochSeconds = time, tree = tree)
}
fun WalletBirthday.toJson() = JSONObject().apply {
put(WalletBirthday.KEY_VERSION, WalletBirthday.VERSION_1)
put(WalletBirthday.KEY_HEIGHT, height)
put(WalletBirthday.KEY_HASH, hash)
put(WalletBirthday.KEY_EPOCH_SECONDS, time)
put(WalletBirthday.KEY_TREE, tree)
internal fun Checkpoint.toJson() = JSONObject().apply {
put(Checkpoint.KEY_VERSION, Checkpoint.VERSION_1)
put(Checkpoint.KEY_HEIGHT, height.value)
put(Checkpoint.KEY_HASH, hash)
put(Checkpoint.KEY_EPOCH_SECONDS, epochSeconds)
put(Checkpoint.KEY_TREE, tree)
}.toString()

View File

@ -6,12 +6,13 @@ import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.internal.SdkDispatchers
import cash.z.ecc.android.sdk.internal.ext.getCacheDirSuspend
import cash.z.ecc.android.sdk.internal.ext.getDatabasePathSuspend
import cash.z.ecc.android.sdk.internal.model.Checkpoint
import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.jni.RustBackend
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.tool.CheckpointTool
import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.sdk.tool.WalletBirthdayTool
import cash.z.ecc.android.sdk.type.UnifiedViewingKey
import cash.z.ecc.android.sdk.type.WalletBirthday
import cash.z.ecc.android.sdk.type.ZcashNetwork
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
@ -22,14 +23,14 @@ import java.io.File
*/
class Initializer private constructor(
val context: Context,
val rustBackend: RustBackend,
internal val rustBackend: RustBackend,
val network: ZcashNetwork,
val alias: String,
val host: String,
val port: Int,
val viewingKeys: List<UnifiedViewingKey>,
val overwriteVks: Boolean,
val birthday: WalletBirthday
internal val checkpoint: Checkpoint
) {
suspend fun erase() = erase(context, network, alias)
@ -38,7 +39,7 @@ class Initializer private constructor(
val viewingKeys: MutableList<UnifiedViewingKey> = mutableListOf(),
var alias: String = ZcashSdk.DEFAULT_ALIAS
) {
var birthdayHeight: Int? = null
var birthdayHeight: BlockHeight? = null
private set
lateinit var network: ZcashNetwork
@ -86,7 +87,7 @@ class Initializer private constructor(
* transactions. Again, this value is only considered when [height] is null.
*
*/
fun setBirthdayHeight(height: Int?, defaultToOldestHeight: Boolean = false): Config =
fun setBirthdayHeight(height: BlockHeight?, defaultToOldestHeight: Boolean = false): Config =
apply {
this.birthdayHeight = height
this.defaultToOldestHeight = defaultToOldestHeight
@ -105,7 +106,7 @@ class Initializer private constructor(
* importing a pre-existing wallet. It is the same as calling
* `birthdayHeight = importedHeight`.
*/
fun importedWalletBirthday(importedHeight: Int?): Config = apply {
fun importedWalletBirthday(importedHeight: BlockHeight?): Config = apply {
birthdayHeight = importedHeight
defaultToOldestHeight = true
}
@ -172,7 +173,7 @@ class Initializer private constructor(
*/
suspend fun importWallet(
seed: ByteArray,
birthdayHeight: Int? = null,
birthday: BlockHeight?,
network: ZcashNetwork,
host: String = network.defaultHost,
port: Int = network.defaultPort,
@ -180,7 +181,7 @@ class Initializer private constructor(
): Config =
importWallet(
DerivationTool.deriveUnifiedViewingKeys(seed, network = network)[0],
birthdayHeight,
birthday,
network,
host,
port,
@ -192,7 +193,7 @@ class Initializer private constructor(
*/
fun importWallet(
viewingKey: UnifiedViewingKey,
birthdayHeight: Int? = null,
birthday: BlockHeight?,
network: ZcashNetwork,
host: String = network.defaultHost,
port: Int = network.defaultPort,
@ -200,7 +201,7 @@ class Initializer private constructor(
): Config = apply {
setViewingKeys(viewingKey)
setNetwork(network, host, port)
importedWalletBirthday(birthdayHeight)
importedWalletBirthday(birthday)
this.alias = alias
}
@ -284,8 +285,8 @@ class Initializer private constructor(
}
// allow either null or a value greater than the activation height
if (
(birthdayHeight ?: network.saplingActivationHeight)
< network.saplingActivationHeight
(birthdayHeight?.value ?: network.saplingActivationHeight.value)
< network.saplingActivationHeight.value
) {
throw InitializerException.InvalidBirthdayHeightException(birthdayHeight, network)
}
@ -331,9 +332,9 @@ class Initializer private constructor(
val heightToUse = config.birthdayHeight
?: (if (config.defaultToOldestHeight == true) config.network.saplingActivationHeight else null)
val loadedBirthday =
WalletBirthdayTool.loadNearest(context, config.network, heightToUse)
CheckpointTool.loadNearest(context, config.network, heightToUse)
val rustBackend = initRustBackend(context, config.network, config.alias, loadedBirthday)
val rustBackend = initRustBackend(context, config.network, config.alias, loadedBirthday.height)
return Initializer(
context.applicationContext,
@ -375,14 +376,14 @@ class Initializer private constructor(
context: Context,
network: ZcashNetwork,
alias: String,
birthday: WalletBirthday
blockHeight: BlockHeight
): RustBackend {
return RustBackend.init(
cacheDbPath(context, network, alias),
dataDbPath(context, network, alias),
File(context.getCacheDirSuspend(), "params").absolutePath,
network,
birthday.height
blockHeight
)
}

View File

@ -36,6 +36,7 @@ import cash.z.ecc.android.sdk.internal.block.CompactBlockDownloader
import cash.z.ecc.android.sdk.internal.block.CompactBlockStore
import cash.z.ecc.android.sdk.internal.ext.toHexReversed
import cash.z.ecc.android.sdk.internal.ext.tryNull
import cash.z.ecc.android.sdk.internal.isEmpty
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.transaction.OutboundTransactionManager
@ -46,6 +47,7 @@ import cash.z.ecc.android.sdk.internal.transaction.TransactionRepository
import cash.z.ecc.android.sdk.internal.transaction.WalletTransactionEncoder
import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.internal.twigTask
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.WalletBalance
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.tool.DerivationTool
@ -188,7 +190,7 @@ class SdkSynchronizer internal constructor(
* The latest height seen on the network while processing blocks. This may differ from the
* latest height scanned and is useful for determining block confirmations and expiration.
*/
override val networkHeight: StateFlow<Int> = processor.networkHeight
override val networkHeight: StateFlow<BlockHeight?> = processor.networkHeight
//
// Error Handling
@ -230,7 +232,7 @@ class SdkSynchronizer internal constructor(
* A callback to invoke whenever a chain error is encountered. These occur whenever the
* processor detects a missing or non-chain-sequential block (i.e. a reorg).
*/
override var onChainErrorHandler: ((errorHeight: Int, rewindHeight: Int) -> Any)? = null
override var onChainErrorHandler: ((errorHeight: BlockHeight, rewindHeight: BlockHeight) -> Any)? = null
//
// Public API
@ -242,9 +244,11 @@ class SdkSynchronizer internal constructor(
* this, a wallet will more likely want to consume the flow of processor info using
* [processorInfo].
*/
override val latestHeight: Int get() = processor.currentInfo.networkBlockHeight
override val latestHeight
get() = processor.currentInfo.networkBlockHeight
override val latestBirthdayHeight: Int get() = processor.birthdayHeight
override val latestBirthdayHeight
get() = processor.birthdayHeight
override suspend fun prepare(): Synchronizer = apply {
// Do nothing; this could likely be removed
@ -318,10 +322,10 @@ class SdkSynchronizer internal constructor(
)
}
override suspend fun getNearestRewindHeight(height: Int): Int =
override suspend fun getNearestRewindHeight(height: BlockHeight): BlockHeight =
processor.getNearestRewindHeight(height)
override suspend fun rewindToNearestHeight(height: Int, alsoClearBlockCache: Boolean) {
override suspend fun rewindToNearestHeight(height: BlockHeight, alsoClearBlockCache: Boolean) {
processor.rewindToNearestHeight(height, alsoClearBlockCache)
}
@ -335,11 +339,11 @@ class SdkSynchronizer internal constructor(
// 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
suspend fun findBlockHash(height: Int): ByteArray? {
suspend fun findBlockHash(height: BlockHeight): ByteArray? {
return (storage as? PagedTransactionRepository)?.findBlockHash(height)
}
suspend fun findBlockHashAsHex(height: Int): String? {
suspend fun findBlockHashAsHex(height: BlockHeight): String? {
return findBlockHash(height)?.toHexReversed()
}
@ -479,7 +483,7 @@ class SdkSynchronizer internal constructor(
return onSetupErrorHandler?.invoke(error) == true
}
private fun onChainError(errorHeight: Int, rewindHeight: Int) {
private fun onChainError(errorHeight: BlockHeight, rewindHeight: BlockHeight) {
twig("Chain error detected at height: $errorHeight. Rewinding to: $rewindHeight")
if (onChainErrorHandler == null) {
twig(
@ -494,7 +498,7 @@ class SdkSynchronizer internal constructor(
/**
* @param elapsedMillis the amount of time that passed since the last scan
*/
private suspend fun onScanComplete(scannedRange: IntRange, elapsedMillis: Long) {
private suspend fun onScanComplete(scannedRange: ClosedRange<BlockHeight>?, elapsedMillis: Long) {
// We don't need to update anything if there have been no blocks
// refresh anyway if:
// - if it's the first time we finished scanning
@ -694,7 +698,7 @@ class SdkSynchronizer internal constructor(
txManager.monitorById(it.id)
}.distinctUntilChanged()
override suspend fun refreshUtxos(address: String, startHeight: Int): Int? {
override suspend fun refreshUtxos(address: String, startHeight: BlockHeight): Int? {
return processor.refreshUtxos(address, startHeight)
}
@ -784,15 +788,16 @@ object DefaultSynchronizerFactory {
suspend fun defaultTransactionRepository(initializer: Initializer): TransactionRepository =
PagedTransactionRepository.new(
initializer.context,
initializer.network,
DEFAULT_PAGE_SIZE,
initializer.rustBackend,
initializer.birthday,
initializer.checkpoint,
initializer.viewingKeys,
initializer.overwriteVks
)
fun defaultBlockStore(initializer: Initializer): CompactBlockStore =
CompactBlockDbStore.new(initializer.context, initializer.rustBackend.pathCacheDb)
CompactBlockDbStore.new(initializer.context, initializer.network, initializer.rustBackend.pathCacheDb)
fun defaultService(initializer: Initializer): LightWalletService =
LightWalletGrpcService(initializer.context, initializer.host, initializer.port)

View File

@ -4,6 +4,7 @@ import cash.z.ecc.android.sdk.block.CompactBlockProcessor
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
import cash.z.ecc.android.sdk.db.entity.PendingTransaction
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.WalletBalance
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.type.AddressType
@ -98,7 +99,7 @@ interface Synchronizer {
* latest downloaded height or scanned height. Although this is present in [processorInfo], it
* is such a frequently used value that it is convenient to have the real-time value by itself.
*/
val networkHeight: StateFlow<Int>
val networkHeight: StateFlow<BlockHeight?>
/**
* A stream of balance values for the orchard pool. Includes the available and total balance.
@ -145,13 +146,13 @@ interface Synchronizer {
/**
* An in-memory reference to the latest height seen on the network.
*/
val latestHeight: Int
val latestHeight: BlockHeight?
/**
* An in-memory reference to the best known birthday height, which can change if the first
* transaction has not yet occurred.
*/
val latestBirthdayHeight: Int
val latestBirthdayHeight: BlockHeight?
//
// Operations
@ -302,7 +303,7 @@ interface Synchronizer {
*/
suspend fun refreshUtxos(
tAddr: String,
sinceHeight: Int = network.saplingActivationHeight
since: BlockHeight = network.saplingActivationHeight
): Int?
/**
@ -310,7 +311,7 @@ interface Synchronizer {
*/
suspend fun getTransparentBalance(tAddr: String): WalletBalance
suspend fun getNearestRewindHeight(height: Int): Int
suspend fun getNearestRewindHeight(height: BlockHeight): BlockHeight
/**
* Returns the safest height to which we can rewind, given a desire to rewind to the height
@ -318,7 +319,7 @@ interface Synchronizer {
* arbitrary height. This handles all that complexity yet remains flexible in the future as
* improvements are made.
*/
suspend fun rewindToNearestHeight(height: Int, alsoClearBlockCache: Boolean = false)
suspend fun rewindToNearestHeight(height: BlockHeight, alsoClearBlockCache: Boolean = false)
suspend fun quickRewind()
@ -372,7 +373,7 @@ interface Synchronizer {
* best to log these errors because they are the most common source of bugs and unexpected
* behavior in wallets, due to the chain data mutating and wallets becoming out of sync.
*/
var onChainErrorHandler: ((Int, Int) -> Any)?
var onChainErrorHandler: ((BlockHeight, BlockHeight) -> Any)?
/**
* Represents the status of this Synchronizer, which is useful for communicating to the user.

View File

@ -33,12 +33,14 @@ import cash.z.ecc.android.sdk.internal.block.CompactBlockDownloader
import cash.z.ecc.android.sdk.internal.ext.retryUpTo
import cash.z.ecc.android.sdk.internal.ext.retryWithBackoff
import cash.z.ecc.android.sdk.internal.ext.toHexReversed
import cash.z.ecc.android.sdk.internal.isEmpty
import cash.z.ecc.android.sdk.internal.transaction.PagedTransactionRepository
import cash.z.ecc.android.sdk.internal.transaction.TransactionRepository
import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.internal.twigTask
import cash.z.ecc.android.sdk.jni.RustBackend
import cash.z.ecc.android.sdk.jni.RustBackendWelding
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.WalletBalance
import cash.z.wallet.sdk.rpc.Service
import io.grpc.StatusRuntimeException
@ -75,11 +77,11 @@ import kotlin.math.roundToInt
* of the current wallet--the height before which we do not need to scan for transactions.
*/
@OpenForTesting
class CompactBlockProcessor(
class CompactBlockProcessor internal constructor(
val downloader: CompactBlockDownloader,
private val repository: TransactionRepository,
private val rustBackend: RustBackendWelding,
minimumHeight: Int = rustBackend.network.saplingActivationHeight
minimumHeight: BlockHeight = rustBackend.network.saplingActivationHeight
) {
/**
* Callback for any non-trivial errors that occur while processing compact blocks.
@ -93,7 +95,7 @@ class CompactBlockProcessor(
* Callback for reorgs. This callback is invoked when validation fails with the height at which
* an error was found and the lower bound to which the data will rewind, at most.
*/
var onChainErrorListener: ((errorHeight: Int, rewindHeight: Int) -> Any)? = null
var onChainErrorListener: ((errorHeight: BlockHeight, rewindHeight: BlockHeight) -> Any)? = null
/**
* Callback for setup errors that occur prior to processing compact blocks. Can be used to
@ -117,12 +119,18 @@ class CompactBlockProcessor(
var onScanMetricCompleteListener: ((BatchMetrics, Boolean) -> Unit)? = null
private val consecutiveChainErrors = AtomicInteger(0)
private val lowerBoundHeight: Int = max(rustBackend.network.saplingActivationHeight, minimumHeight - MAX_REORG_SIZE)
private val lowerBoundHeight: BlockHeight = BlockHeight(
max(
rustBackend.network.saplingActivationHeight.value,
minimumHeight.value - MAX_REORG_SIZE
)
)
private val _state: ConflatedBroadcastChannel<State> = ConflatedBroadcastChannel(Initialized)
private val _progress = ConflatedBroadcastChannel(0)
private val _processorInfo = ConflatedBroadcastChannel(ProcessorInfo())
private val _networkHeight = MutableStateFlow(-1)
private val _processorInfo =
ConflatedBroadcastChannel(ProcessorInfo(null, null, null, null, null))
private val _networkHeight = MutableStateFlow<BlockHeight?>(null)
private val processingMutex = Mutex()
/**
@ -139,7 +147,7 @@ class CompactBlockProcessor(
* sequentially, due to the way sqlite works so it is okay for this not to be threadsafe or
* coroutine safe because processing cannot be concurrent.
*/
internal var currentInfo = ProcessorInfo()
internal var currentInfo = ProcessorInfo(null, null, null, null, null)
/**
* The zcash network that is being processed. Either Testnet or Mainnet.
@ -193,25 +201,38 @@ class CompactBlockProcessor(
processNewBlocks()
}
// immediately process again after failures in order to download new blocks right away
if (result == ERROR_CODE_RECONNECT) {
val napTime = calculatePollInterval(true)
twig("Unable to process new blocks because we are disconnected! Attempting to reconnect in ${napTime}ms")
delay(napTime)
} else if (result == ERROR_CODE_NONE || result == ERROR_CODE_FAILED_ENHANCE) {
val noWorkDone = currentInfo.lastDownloadRange.isEmpty() && currentInfo.lastScanRange.isEmpty()
val summary = if (noWorkDone) "Nothing to process: no new blocks to download or scan" else "Done processing blocks"
consecutiveChainErrors.set(0)
val napTime = calculatePollInterval()
twig("$summary${if (result == ERROR_CODE_FAILED_ENHANCE) " (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)
} else {
if (consecutiveChainErrors.get() >= RETRIES) {
val errorMessage = "ERROR: unable to resolve reorg at height $result after ${consecutiveChainErrors.get()} correction attempts!"
fail(CompactBlockProcessorException.FailedReorgRepair(errorMessage))
} else {
handleChainError(result)
when (result) {
BlockProcessingResult.Reconnecting -> {
val napTime = calculatePollInterval(true)
twig("Unable to process new blocks because we are disconnected! Attempting to reconnect in ${napTime}ms")
delay(napTime)
}
BlockProcessingResult.NoBlocksToProcess, BlockProcessingResult.FailedEnhance -> {
val noWorkDone =
currentInfo.lastDownloadRange?.isEmpty() ?: true && currentInfo.lastScanRange?.isEmpty() ?: true
val summary = if (noWorkDone) {
"Nothing to process: no new blocks to download or scan"
} else {
"Done processing blocks"
}
consecutiveChainErrors.set(0)
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}).")
delay(napTime)
}
is BlockProcessingResult.Error -> {
if (consecutiveChainErrors.get() >= RETRIES) {
val errorMessage =
"ERROR: unable to resolve reorg at height $result after ${consecutiveChainErrors.get()} correction attempts!"
fail(CompactBlockProcessorException.FailedReorgRepair(errorMessage))
} else {
handleChainError(result.failedAtHeight)
}
consecutiveChainErrors.getAndIncrement()
}
is BlockProcessingResult.Success -> {
// Do nothing. We are done.
}
consecutiveChainErrors.getAndIncrement()
}
}
} while (isActive && !_state.isClosedForSend && _state.value !is Stopped)
@ -238,32 +259,37 @@ class CompactBlockProcessor(
throw error
}
/**
* Process new blocks returning false whenever an error was found.
*
* @return -1 when processing was successful and did not encounter errors during validation or scanning. Otherwise
* return the block height where an error was found.
*/
private suspend fun processNewBlocks(): Int = withContext(IO) {
private suspend fun processNewBlocks(): BlockProcessingResult = withContext(IO) {
twig("beginning to process new blocks (with lower bound: $lowerBoundHeight)...", -1)
if (!updateRanges()) {
twig("Disconnection detected! Attempting to reconnect!")
setState(Disconnected)
downloader.lightWalletService.reconnect()
ERROR_CODE_RECONNECT
BlockProcessingResult.Reconnecting
} else if (currentInfo.lastDownloadRange.isEmpty() && currentInfo.lastScanRange.isEmpty()) {
setState(Scanned(currentInfo.lastScanRange))
ERROR_CODE_NONE
BlockProcessingResult.NoBlocksToProcess
} else {
downloadNewBlocks(currentInfo.lastDownloadRange)
val error = validateAndScanNewBlocks(currentInfo.lastScanRange)
if (error != ERROR_CODE_NONE) error else {
enhanceTransactionDetails(currentInfo.lastScanRange)
if (error != BlockProcessingResult.Success) {
error
} else {
currentInfo.lastScanRange?.let { enhanceTransactionDetails(it) }
?: BlockProcessingResult.NoBlocksToProcess
}
}
}
sealed class BlockProcessingResult {
object NoBlocksToProcess : BlockProcessingResult()
object Success : BlockProcessingResult()
object Reconnecting : BlockProcessingResult()
object FailedEnhance : BlockProcessingResult()
data class Error(val failedAtHeight: BlockHeight) : BlockProcessingResult()
}
/**
* Gets the latest range info and then uses that initialInfo to update (and transmit)
* the scan/download ranges that require processing.
@ -278,19 +304,36 @@ class CompactBlockProcessor(
ProcessorInfo(
networkBlockHeight = downloader.getLatestBlockHeight(),
lastScannedHeight = getLastScannedHeight(),
lastDownloadedHeight = max(getLastDownloadedHeight(), lowerBoundHeight - 1)
lastDownloadedHeight = BlockHeight.new(
network,
max(
getLastDownloadedHeight().value,
lowerBoundHeight.value - 1
)
),
lastDownloadRange = null,
lastScanRange = null
).let { initialInfo ->
updateProgress(
networkBlockHeight = initialInfo.networkBlockHeight,
lastScannedHeight = initialInfo.lastScannedHeight,
lastDownloadedHeight = initialInfo.lastDownloadedHeight,
lastScanRange = (initialInfo.lastScannedHeight + 1)..initialInfo.networkBlockHeight,
lastDownloadRange = (
max(
initialInfo.lastDownloadedHeight,
initialInfo.lastScannedHeight
) + 1
lastScanRange = if (initialInfo.lastScannedHeight != null && initialInfo.networkBlockHeight != null) {
initialInfo.lastScannedHeight + 1..initialInfo.networkBlockHeight
} else {
null
},
lastDownloadRange = if (initialInfo.lastDownloadedHeight != null && initialInfo.lastScannedHeight != null && initialInfo.networkBlockHeight != null) {
BlockHeight.new(
network,
max(
initialInfo.lastDownloadedHeight.value,
initialInfo.lastScannedHeight.value
) + 1
)..initialInfo.networkBlockHeight
} else {
null
}
)
}
true
@ -306,35 +349,34 @@ class CompactBlockProcessor(
* prevHash value matches the preceding block in the chain.
*
* @param lastScanRange the range to be validated and scanned.
*
* @return error code or [ERROR_CODE_NONE] when there is no error.
*/
private suspend fun validateAndScanNewBlocks(lastScanRange: IntRange): Int = withContext(IO) {
setState(Validating)
var error = validateNewBlocks(lastScanRange)
if (error == ERROR_CODE_NONE) {
// in theory, a scan should not fail after validation succeeds but maybe consider
// changing the rust layer to return the failed block height whenever scan does fail
// rather than a boolean
setState(Scanning)
val success = scanNewBlocks(lastScanRange)
if (!success) throw CompactBlockProcessorException.FailedScan()
else {
setState(Scanned(lastScanRange))
private suspend fun validateAndScanNewBlocks(lastScanRange: ClosedRange<BlockHeight>?): BlockProcessingResult =
withContext(IO) {
setState(Validating)
val result = validateNewBlocks(lastScanRange)
if (result == BlockProcessingResult.Success) {
// in theory, a scan should not fail after validation succeeds but maybe consider
// changing the rust layer to return the failed block height whenever scan does fail
// rather than a boolean
setState(Scanning)
val success = scanNewBlocks(lastScanRange)
if (!success) {
throw CompactBlockProcessorException.FailedScan()
} else {
setState(Scanned(lastScanRange))
}
}
ERROR_CODE_NONE
} else {
error
}
}
private suspend fun enhanceTransactionDetails(lastScanRange: IntRange): Int {
result
}
private suspend fun enhanceTransactionDetails(lastScanRange: ClosedRange<BlockHeight>): BlockProcessingResult {
Twig.sprout("enhancing")
twig("Enhancing transaction details for blocks $lastScanRange")
setState(Enhancing)
return try {
val newTxs = repository.findNewTransactions(lastScanRange)
if (newTxs == null) {
if (newTxs.isEmpty()) {
twig("no new transactions found in $lastScanRange")
} else {
twig("enhancing ${newTxs.size} transaction(s)!")
@ -346,15 +388,18 @@ class CompactBlockProcessor(
}
newTxs?.onEach { newTransaction ->
if (newTransaction == null) twig("somehow, new transaction was null!!!")
else enhance(newTransaction)
if (newTransaction == null) {
twig("somehow, new transaction was null!!!")
} else {
enhance(newTransaction)
}
}
twig("Done enhancing transaction details")
ERROR_CODE_NONE
BlockProcessingResult.Success
} catch (t: Throwable) {
twig("Failed to enhance due to $t")
t.printStackTrace()
ERROR_CODE_FAILED_ENHANCE
BlockProcessingResult.FailedEnhance
} finally {
Twig.clip("enhancing")
}
@ -374,8 +419,11 @@ class CompactBlockProcessor(
} catch (t: Throwable) {
twig("Warning: failure on transaction: error: $t\ttransaction: $transaction")
onProcessorError(
if (downloaded) EnhanceTxDecryptError(transaction.minedHeight, t)
else EnhanceTxDownloadError(transaction.minedHeight, t)
if (downloaded) {
EnhanceTxDecryptError(transaction.minedBlockHeight, t)
} else {
EnhanceTxDownloadError(transaction.minedBlockHeight, t)
}
)
}
}
@ -391,11 +439,19 @@ class CompactBlockProcessor(
else -> {
// verify that the server is correct
downloader.getServerInfo().let { info ->
val clientBranch = "%x".format(rustBackend.getBranchIdForHeight(info.blockHeight.toInt()))
val clientBranch =
"%x".format(rustBackend.getBranchIdForHeight(BlockHeight(info.blockHeight)))
val network = rustBackend.network.networkName
when {
!info.matchingNetwork(network) -> MismatchedNetwork(clientNetwork = network, serverNetwork = info.chainName)
!info.matchingConsensusBranchId(clientBranch) -> MismatchedBranch(clientBranch = clientBranch, serverBranch = info.consensusBranchId, networkName = network)
!info.matchingNetwork(network) -> MismatchedNetwork(
clientNetwork = network,
serverNetwork = info.chainName
)
!info.matchingConsensusBranchId(clientBranch) -> MismatchedBranch(
clientBranch = clientBranch,
serverBranch = info.consensusBranchId,
networkName = network
)
else -> null
}
}
@ -426,30 +482,35 @@ class CompactBlockProcessor(
}
var failedUtxoFetches = 0
internal suspend fun refreshUtxos(tAddress: String, startHeight: Int): Int? = withContext(IO) {
var count: Int? = null
// todo: 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
// probably disabled on ligthtwalletd, so then stop trying until the next app launch.
if (failedUtxoFetches < 9) { // there are 3 attempts per block
try {
retryUpTo(3) {
val result = downloader.lightWalletService.fetchUtxos(tAddress, startHeight)
count = processUtxoResult(result, tAddress, startHeight)
internal suspend fun refreshUtxos(tAddress: String, startHeight: BlockHeight): Int? =
withContext(IO) {
var count: Int? = null
// todo: 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
// probably disabled on ligthtwalletd, so then stop trying until the next app launch.
if (failedUtxoFetches < 9) { // there are 3 attempts per block
try {
retryUpTo(3) {
val result = downloader.lightWalletService.fetchUtxos(tAddress, startHeight)
count = processUtxoResult(result, tAddress, startHeight)
}
} catch (e: Throwable) {
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.")
}
} catch (e: Throwable) {
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.")
} else {
twig("Warning: gave up on fetching UTXOs for this session. It seems to unavailable on lightwalletd.")
}
} else {
twig("Warning: gave up on fetching UTXOs for this session. It seems to unavailable on lightwalletd.")
count
}
count
}
internal suspend fun processUtxoResult(result: List<Service.GetAddressUtxosReply>, tAddress: String, startHeight: Int): Int = withContext(IO) {
internal suspend fun processUtxoResult(
result: List<Service.GetAddressUtxosReply>,
tAddress: String,
startHeight: BlockHeight
): Int = withContext(IO) {
var skipped = 0
val aboveHeight = startHeight - 1
val aboveHeight = startHeight
twig("Clearing utxos above height $aboveHeight", -1)
rustBackend.clearUtxos(tAddress, aboveHeight)
twig("Checking for UTXOs above height $aboveHeight")
@ -462,7 +523,7 @@ class CompactBlockProcessor(
utxo.index,
utxo.script.toByteArray(),
utxo.valueZat,
utxo.height.toInt()
BlockHeight(utxo.height)
)
} catch (t: Throwable) {
// TODO: more accurately track the utxos that were skipped (in theory, this could fail for other reasons)
@ -480,75 +541,81 @@ class CompactBlockProcessor(
* @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
internal suspend fun downloadNewBlocks(range: IntRange) = withContext<Unit>(IO) {
if (range.isEmpty()) {
twig("no blocks to download")
} else {
_state.send(Downloading)
Twig.sprout("downloading")
twig("downloading blocks in range $range", -1)
internal suspend fun downloadNewBlocks(range: ClosedRange<BlockHeight>?) =
withContext<Unit>(IO) {
if (null == range || range.isEmpty()) {
twig("no blocks to download")
} else {
_state.send(Downloading)
Twig.sprout("downloading")
twig("downloading blocks in range $range", -1)
var downloadedBlockHeight = range.first
val missingBlockCount = range.last - range.first + 1
val batches = (
missingBlockCount / DOWNLOAD_BATCH_SIZE +
(if (missingBlockCount.rem(DOWNLOAD_BATCH_SIZE) == 0) 0 else 1)
)
var progress: Int
twig("found $missingBlockCount missing blocks, downloading in $batches batches of $DOWNLOAD_BATCH_SIZE...")
for (i in 1..batches) {
retryUpTo(RETRIES, { CompactBlockProcessorException.FailedDownload(it) }) {
val end = min((range.first + (i * DOWNLOAD_BATCH_SIZE)) - 1, range.last) // subtract 1 on the first value because the range is inclusive
var count = 0
twig("downloaded $downloadedBlockHeight..$end (batch $i of $batches) [${downloadedBlockHeight..end}]") {
count = downloader.downloadBlockRange(downloadedBlockHeight..end)
var downloadedBlockHeight = range.start
val missingBlockCount = range.endInclusive.value - range.start.value + 1
val batches = (
missingBlockCount / DOWNLOAD_BATCH_SIZE +
(if (missingBlockCount.rem(DOWNLOAD_BATCH_SIZE) == 0L) 0 else 1)
)
var progress: Int
twig("found $missingBlockCount missing blocks, downloading in $batches batches of $DOWNLOAD_BATCH_SIZE...")
for (i in 1..batches) {
retryUpTo(RETRIES, { CompactBlockProcessorException.FailedDownload(it) }) {
val end = BlockHeight(
min(
(range.start.value + (i * DOWNLOAD_BATCH_SIZE)) - 1,
range.endInclusive.value
)
) // subtract 1 on the first value because the range is inclusive
var count = 0
twig("downloaded $downloadedBlockHeight..$end (batch $i of $batches) [${downloadedBlockHeight..end}]") {
count = downloader.downloadBlockRange(downloadedBlockHeight..end)
}
twig("downloaded $count blocks!")
progress = (i / batches.toFloat() * 100).roundToInt()
_progress.send(progress)
val lastDownloadedHeight = downloader.getLastDownloadedHeight()
.takeUnless { it < network.saplingActivationHeight }
updateProgress(lastDownloadedHeight = lastDownloadedHeight)
downloadedBlockHeight = end
}
twig("downloaded $count blocks!")
progress = (i / batches.toFloat() * 100).roundToInt()
_progress.send(progress)
val lastDownloadedHeight = downloader.getLastDownloadedHeight().takeUnless { it < network.saplingActivationHeight } ?: -1
updateProgress(lastDownloadedHeight = lastDownloadedHeight)
downloadedBlockHeight = end
}
Twig.clip("downloading")
}
Twig.clip("downloading")
_progress.send(100)
}
_progress.send(100)
}
/**
* Validate all blocks in the given range, ensuring that the blocks are in ascending order, with
* no gaps and are also chain-sequential. This means every block's prevHash value matches the
* preceding block in the chain.
* preceding block in the chain. Validation starts at the back of the chain and works toward the tip.
*
* @param range the range of blocks to validate.
*
* @return [ERROR_CODE_NONE] when there is no problem. Otherwise, return the lowest height where an error was
* found. In other words, validation starts at the back of the chain and works toward the tip.
*/
private suspend fun validateNewBlocks(range: IntRange?): Int {
if (range?.isEmpty() != false) {
private suspend fun validateNewBlocks(range: ClosedRange<BlockHeight>?): BlockProcessingResult {
if (null == range || range.isEmpty()) {
twig("no blocks to validate: $range")
return ERROR_CODE_NONE
return BlockProcessingResult.NoBlocksToProcess
}
Twig.sprout("validating")
twig("validating blocks in range $range in db: ${(rustBackend as RustBackend).pathCacheDb}")
val result = rustBackend.validateCombinedChain()
Twig.clip("validating")
return result
return if (null == result) {
BlockProcessingResult.Success
} else {
BlockProcessingResult.Error(result)
}
}
/**
* Scan all blocks in the given range, decrypting and persisting anything that matches our
* wallet.
* wallet. Scanning starts at the back of the chain and works toward the tip.
*
* @param range the range of blocks to scan.
*
* @return [ERROR_CODE_NONE] when there is no problem. Otherwise, return the lowest height where an error was
* found. In other words, scanning starts at the back of the chain and works toward the tip.
*/
private suspend fun scanNewBlocks(range: IntRange?): Boolean = withContext(IO) {
if (range?.isEmpty() != false) {
private suspend fun scanNewBlocks(range: ClosedRange<BlockHeight>?): Boolean = withContext(IO) {
if (null == range || range.isEmpty()) {
twig("no blocks to scan for range $range")
true
} else {
@ -565,16 +632,17 @@ class CompactBlockProcessor(
metrics.beginBatch()
result = rustBackend.scanBlocks(SCAN_BATCH_SIZE)
metrics.endBatch()
val lastScannedHeight = range.start + metrics.cumulativeItems - 1
val percentValue = (lastScannedHeight - range.first) / (range.last - range.first + 1).toFloat() * 100.0f
val lastScannedHeight = BlockHeight.new(network, range.start.value + metrics.cumulativeItems - 1)
val percentValue =
(lastScannedHeight.value - range.start.value) / (range.endInclusive.value - range.start.value + 1).toFloat() * 100.0f
val percent = "%.0f".format(percentValue.coerceAtMost(100f).coerceAtLeast(0f))
twig("batch scanned ($percent%): $lastScannedHeight/${range.last} | ${metrics.batchTime}ms, ${metrics.batchItems}blks, ${metrics.batchIps.format()}bps")
twig("batch scanned ($percent%): $lastScannedHeight/${range.endInclusive} | ${metrics.batchTime}ms, ${metrics.batchItems}blks, ${metrics.batchIps.format()}bps")
if (currentInfo.lastScannedHeight != lastScannedHeight) {
scannedNewBlocks = true
updateProgress(lastScannedHeight = lastScannedHeight)
}
// if we made progress toward our scan, then keep trying
} while (result && scannedNewBlocks && lastScannedHeight < range.last)
} 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.clip("scanning")
@ -600,11 +668,11 @@ class CompactBlockProcessor(
* blocks that we don't yet have.
*/
private suspend fun updateProgress(
networkBlockHeight: Int = currentInfo.networkBlockHeight,
lastScannedHeight: Int = currentInfo.lastScannedHeight,
lastDownloadedHeight: Int = currentInfo.lastDownloadedHeight,
lastScanRange: IntRange = currentInfo.lastScanRange,
lastDownloadRange: IntRange = currentInfo.lastDownloadRange
networkBlockHeight: BlockHeight? = currentInfo.networkBlockHeight,
lastScannedHeight: BlockHeight? = currentInfo.lastScannedHeight,
lastDownloadedHeight: BlockHeight? = currentInfo.lastDownloadedHeight,
lastScanRange: ClosedRange<BlockHeight>? = currentInfo.lastScanRange,
lastDownloadRange: ClosedRange<BlockHeight>? = currentInfo.lastDownloadRange
): Unit = withContext(IO) {
currentInfo = currentInfo.copy(
networkBlockHeight = networkBlockHeight,
@ -617,7 +685,7 @@ class CompactBlockProcessor(
_processorInfo.send(currentInfo)
}
private suspend fun handleChainError(errorHeight: Int) {
private suspend fun handleChainError(errorHeight: BlockHeight) {
// TODO consider an error object containing hash information
printValidationErrorInfo(errorHeight)
determineLowerBound(errorHeight).let { lowerBound ->
@ -627,14 +695,17 @@ class CompactBlockProcessor(
}
}
suspend fun getNearestRewindHeight(height: Int): Int {
suspend fun getNearestRewindHeight(height: BlockHeight): BlockHeight {
// TODO: add a concept of original checkpoint height to the processor. For now, derive it
val originalCheckpoint = lowerBoundHeight + MAX_REORG_SIZE + 2 // add one because we already have the checkpoint. Add one again because we delete ABOVE the block
val originalCheckpoint =
lowerBoundHeight + MAX_REORG_SIZE + 2 // add one because we already have the checkpoint. Add one again because we delete ABOVE the block
return if (height < originalCheckpoint) {
originalCheckpoint
} else {
// tricky: subtract one because we delete ABOVE this block
rustBackend.getNearestRewindHeight(height) - 1
// This could create an invalid height if if height was saplingActivationHeight
val rewindHeight = BlockHeight(height.value - 1)
rustBackend.getNearestRewindHeight(rewindHeight)
}
}
@ -644,7 +715,10 @@ class CompactBlockProcessor(
suspend fun quickRewind() {
val height = max(currentInfo.lastScannedHeight, repository.lastScannedHeight())
val blocksPerDay = 60 * 60 * 24 * 1000 / ZcashSdk.BLOCK_INTERVAL_MILLIS.toInt()
val twoWeeksBack = (height - blocksPerDay * 14).coerceAtLeast(lowerBoundHeight)
val twoWeeksBack = BlockHeight.new(
network,
(height.value - blocksPerDay * 14).coerceAtLeast(lowerBoundHeight.value)
)
rewindToNearestHeight(twoWeeksBack, false)
}
@ -652,45 +726,73 @@ class CompactBlockProcessor(
* @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.
*/
suspend fun rewindToNearestHeight(height: Int, alsoClearBlockCache: Boolean = false) = withContext(IO) {
processingMutex.withLockLogged("rewindToHeight") {
val lastScannedHeight = currentInfo.lastScannedHeight
val lastLocalBlock = repository.lastScannedHeight()
val targetHeight = getNearestRewindHeight(height)
twig("Rewinding from $lastScannedHeight to requested height: $height using target height: $targetHeight with last local block: $lastLocalBlock")
if (targetHeight < lastScannedHeight || (lastScannedHeight == -1 && (targetHeight < lastLocalBlock))) {
rustBackend.rewindToHeight(targetHeight)
} 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")
}
suspend fun rewindToNearestHeight(
height: BlockHeight,
alsoClearBlockCache: Boolean = false
) =
withContext(IO) {
processingMutex.withLockLogged("rewindToHeight") {
val lastScannedHeight = currentInfo.lastScannedHeight
val lastLocalBlock = repository.lastScannedHeight()
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)) {
rustBackend.rewindToHeight(targetHeight)
} 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")
}
if (alsoClearBlockCache) {
twig("Also clearing block cache back to $targetHeight. These rewound blocks will download in the next scheduled scan")
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
setState(Downloading)
updateProgress(
lastScannedHeight = targetHeight,
lastDownloadedHeight = targetHeight,
lastScanRange = (targetHeight + 1)..currentInfo.networkBlockHeight,
lastDownloadRange = (targetHeight + 1)..currentInfo.networkBlockHeight
)
_progress.send(0)
} else {
updateProgress(
lastScannedHeight = targetHeight,
lastScanRange = (targetHeight + 1)..currentInfo.networkBlockHeight
)
_progress.send(0)
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.first}..${range.last}")
if (validateAndScanNewBlocks(range) == ERROR_CODE_NONE) enhanceTransactionDetails(range)
val currentNetworkBlockHeight = currentInfo.networkBlockHeight
if (alsoClearBlockCache) {
twig("Also clearing block cache back to $targetHeight. These rewound blocks will download in the next scheduled scan")
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
setState(Downloading)
if (null == currentNetworkBlockHeight) {
updateProgress(
lastScannedHeight = targetHeight,
lastDownloadedHeight = targetHeight,
lastScanRange = null,
lastDownloadRange = null
)
} else {
updateProgress(
lastScannedHeight = targetHeight,
lastDownloadedHeight = targetHeight,
lastScanRange = (targetHeight + 1)..currentNetworkBlockHeight,
lastDownloadRange = (targetHeight + 1)..currentNetworkBlockHeight
)
}
_progress.send(0)
} else {
if (null == currentNetworkBlockHeight) {
updateProgress(
lastScannedHeight = targetHeight,
lastScanRange = null
)
} else {
updateProgress(
lastScannedHeight = targetHeight,
lastScanRange = (targetHeight + 1)..currentNetworkBlockHeight
)
}
_progress.send(0)
if (null != 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}")
if (validateAndScanNewBlocks(range) == BlockProcessingResult.Success) {
enhanceTransactionDetails(range)
}
}
}
}
}
}
/** insightful function for debugging these critical errors */
private suspend fun printValidationErrorInfo(errorHeight: Int, 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
if (!BuildConfig.DEBUG) return
@ -699,19 +801,25 @@ class CompactBlockProcessor(
errorInfo = fetchValidationErrorInfo(errorHeight + 1)
twig("The next block block: ${errorInfo.errorHeight} which had hash ${errorInfo.actualPrevHash} but the expected hash was ${errorInfo.expectedPrevHash}")
twig("=================== BLOCKS [$errorHeight..${errorHeight + count - 1}]: START ========")
twig("=================== BLOCKS [$errorHeight..${errorHeight.value + count - 1}]: START ========")
repeat(count) { i ->
val height = errorHeight + i
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.
val hash = block?.hash?.toByteArray() ?: (repository as PagedTransactionRepository).findBlockHash(height)
twig("block: $height\thash=${hash?.toHexReversed()} \tprevHash=${block?.prevHash?.toByteArray()?.toHexReversed()}")
val hash = block?.hash?.toByteArray()
?: (repository as PagedTransactionRepository).findBlockHash(height)
twig(
"block: $height\thash=${hash?.toHexReversed()} \tprevHash=${
block?.prevHash?.toByteArray()?.toHexReversed()
}"
)
}
twig("=================== BLOCKS [$errorHeight..${errorHeight + count - 1}]: END ========")
twig("=================== BLOCKS [$errorHeight..${errorHeight.value + count - 1}]: END ========")
}
private suspend fun fetchValidationErrorInfo(errorHeight: Int): ValidationErrorInfo {
val hash = (repository as PagedTransactionRepository).findBlockHash(errorHeight + 1)?.toHexReversed()
private suspend fun fetchValidationErrorInfo(errorHeight: BlockHeight): ValidationErrorInfo {
val hash = (repository as PagedTransactionRepository).findBlockHash(errorHeight + 1)
?.toHexReversed()
val prevHash = repository.findBlockHash(errorHeight)?.toHexReversed()
val compactBlock = downloader.compactBlockStore.findCompactBlock(errorHeight + 1)
@ -729,9 +837,9 @@ class CompactBlockProcessor(
return onProcessorErrorListener?.invoke(throwable) ?: true
}
private fun determineLowerBound(errorHeight: Int): Int {
val offset = Math.min(MAX_REORG_SIZE, REWIND_DISTANCE * (consecutiveChainErrors.get() + 1))
return Math.max(errorHeight - offset, lowerBoundHeight).also {
private fun determineLowerBound(errorHeight: BlockHeight): BlockHeight {
val offset = min(MAX_REORG_SIZE, REWIND_DISTANCE * (consecutiveChainErrors.get() + 1))
return BlockHeight(max(errorHeight.value - offset, lowerBoundHeight.value)).also {
twig("offset = min($MAX_REORG_SIZE, $REWIND_DISTANCE * (${consecutiveChainErrors.get() + 1})) = $offset")
twig("lowerBound = max($errorHeight - $offset, $lowerBoundHeight) = $it")
}
@ -754,19 +862,28 @@ class CompactBlockProcessor(
return deltaToNextInteral
}
suspend fun calculateBirthdayHeight(): Int {
var oldestTransactionHeight = 0
suspend fun calculateBirthdayHeight(): BlockHeight {
var oldestTransactionHeight: BlockHeight? = null
try {
oldestTransactionHeight = repository.receivedTransactions.first().lastOrNull()?.minedHeight ?: lowerBoundHeight
val tempOldestTransactionHeight = repository.receivedTransactions
.first()
.lastOrNull()
?.minedBlockHeight
?: lowerBoundHeight
// 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
oldestTransactionHeight = ZcashSdk.MAX_REORG_SIZE.let { boundary ->
oldestTransactionHeight.let { it - it.rem(boundary) - boundary }
}
oldestTransactionHeight = BlockHeight.new(
network,
tempOldestTransactionHeight.value - tempOldestTransactionHeight.value.rem(ZcashSdk.MAX_REORG_SIZE) - ZcashSdk.MAX_REORG_SIZE.toLong()
)
} catch (t: Throwable) {
twig("failed to calculate birthday due to: $t")
}
return maxOf(lowerBoundHeight, oldestTransactionHeight, rustBackend.network.saplingActivationHeight)
return buildList<BlockHeight> {
add(lowerBoundHeight)
add(rustBackend.network.saplingActivationHeight)
oldestTransactionHeight?.let { add(it) }
}.maxOf { it }
}
/**
@ -819,7 +936,8 @@ class CompactBlockProcessor(
}
}
suspend fun getUtxoCacheBalance(address: String): WalletBalance = rustBackend.getDownloadedUtxoBalance(address)
suspend fun getUtxoCacheBalance(address: String): WalletBalance =
rustBackend.getDownloadedUtxoBalance(address)
/**
* Transmits the given state for this processor.
@ -864,7 +982,7 @@ class CompactBlockProcessor(
/**
* [State] for when we are done decrypting blocks, for now.
*/
class Scanned(val scannedRange: IntRange) : Connected, Syncing, State()
class Scanned(val scannedRange: ClosedRange<BlockHeight>?) : Connected, Syncing, State()
/**
* [State] for when transaction details are being retrieved. This typically means the wallet
@ -907,11 +1025,11 @@ class CompactBlockProcessor(
* @param lastScanRange inclusive range to scan.
*/
data class ProcessorInfo(
val networkBlockHeight: Int = -1,
val lastScannedHeight: Int = -1,
val lastDownloadedHeight: Int = -1,
val lastDownloadRange: IntRange = 0..-1, // empty range
val lastScanRange: IntRange = 0..-1 // empty range
val networkBlockHeight: BlockHeight?,
val lastScannedHeight: BlockHeight?,
val lastDownloadedHeight: BlockHeight?,
val lastDownloadRange: ClosedRange<BlockHeight>?,
val lastScanRange: ClosedRange<BlockHeight>?
) {
/**
@ -919,19 +1037,24 @@ class CompactBlockProcessor(
*
* @return false when all values match their defaults.
*/
val hasData get() = networkBlockHeight != -1 ||
lastScannedHeight != -1 ||
lastDownloadedHeight != -1 ||
lastDownloadRange != 0..-1 ||
lastScanRange != 0..-1
val hasData
get() = networkBlockHeight != null ||
lastScannedHeight != null ||
lastDownloadedHeight != null ||
lastDownloadRange != null ||
lastScanRange != null
/**
* Determines whether this instance is actively downloading compact blocks.
*
* @return true when there are more than zero blocks remaining to download.
*/
val isDownloading: Boolean get() = !lastDownloadRange.isEmpty() &&
lastDownloadedHeight < lastDownloadRange.last
val isDownloading: Boolean
get() =
lastDownloadedHeight != null &&
lastDownloadRange != null &&
!lastDownloadRange.isEmpty() &&
lastDownloadedHeight < lastDownloadRange.endInclusive
/**
* Determines whether this instance is actively scanning or validating compact blocks.
@ -939,32 +1062,39 @@ class CompactBlockProcessor(
* @return true when downloading has completed and there are more than zero blocks remaining
* to be scanned.
*/
val isScanning: Boolean get() = !isDownloading &&
!lastScanRange.isEmpty() &&
lastScannedHeight < lastScanRange.last
val isScanning: Boolean
get() =
!isDownloading &&
lastScannedHeight != null &&
lastScanRange != null &&
!lastScanRange.isEmpty() &&
lastScannedHeight < lastScanRange.endInclusive
/**
* The amount of scan progress from 0 to 100.
*/
val scanProgress get() = when {
lastScannedHeight <= -1 -> 0
lastScanRange.isEmpty() -> 100
lastScannedHeight >= lastScanRange.last -> 100
else -> {
// when lastScannedHeight == lastScanRange.first, we have scanned one block, thus the offsets
val blocksScanned = (lastScannedHeight - lastScanRange.first + 1).coerceAtLeast(0)
// we scan the range inclusively so 100..100 is one block to scan, thus the offset
val numberOfBlocks = lastScanRange.last - lastScanRange.first + 1
// take the percentage then convert and round
((blocksScanned.toFloat() / numberOfBlocks) * 100.0f).let { percent ->
percent.coerceAtMost(100.0f).roundToInt()
val scanProgress
get() = when {
lastScannedHeight == null -> 0
lastScanRange == null -> 100
lastScannedHeight >= lastScanRange.endInclusive -> 100
else -> {
// when lastScannedHeight == lastScanRange.first, we have scanned one block, thus the offsets
val blocksScanned =
(lastScannedHeight.value - lastScanRange.start.value + 1).coerceAtLeast(0)
// we scan the range inclusively so 100..100 is one block to scan, thus the offset
val numberOfBlocks =
lastScanRange.endInclusive.value - lastScanRange.start.value + 1
// take the percentage then convert and round
((blocksScanned.toFloat() / numberOfBlocks) * 100.0f).let { percent ->
percent.coerceAtMost(100.0f).roundToInt()
}
}
}
}
}
data class ValidationErrorInfo(
val errorHeight: Int,
val errorHeight: BlockHeight,
val hash: String?,
val expectedPrevHash: String?,
val actualPrevHash: String?
@ -1002,10 +1132,12 @@ class CompactBlockProcessor(
}
twig("$name MUTEX: withLock complete", -1)
}
companion object {
const val ERROR_CODE_NONE = -1
const val ERROR_CODE_RECONNECT = 20
const val ERROR_CODE_FAILED_ENHANCE = 40
}
}
private fun max(a: BlockHeight?, b: BlockHeight) = if (null == a) {
b
} else if (a.value > b.value) {
a
} else {
b
}

View File

@ -5,6 +5,7 @@ import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.Zatoshi
//
@ -81,8 +82,8 @@ data class PendingTransactionEntity(
override val value: Long = -1,
override val memo: ByteArray? = byteArrayOf(),
override val accountIndex: Int,
override val minedHeight: Int = -1,
override val expiryHeight: Int = -1,
override val minedHeight: Long = -1,
override val expiryHeight: Long = -1,
override val cancelled: Int = 0,
override val encodeAttempts: Int = -1,
@ -135,8 +136,8 @@ data class PendingTransactionEntity(
result = 31 * result + value.hashCode()
result = 31 * result + (memo?.contentHashCode() ?: 0)
result = 31 * result + accountIndex
result = 31 * result + minedHeight
result = 31 * result + expiryHeight
result = 31 * result + minedHeight.hashCode()
result = 31 * result + expiryHeight.hashCode()
result = 31 * result + cancelled
result = 31 * result + encodeAttempts
result = 31 * result + submitAttempts
@ -163,7 +164,7 @@ data class ConfirmedTransaction(
override val memo: ByteArray? = ByteArray(0),
override val noteId: Long = 0L,
override val blockTimeInSeconds: Long = 0L,
override val minedHeight: Int = -1,
override val minedHeight: Long = -1,
override val transactionIndex: Int,
override val rawTransactionId: ByteArray = ByteArray(0),
@ -173,6 +174,13 @@ data class ConfirmedTransaction(
override val raw: ByteArray? = byteArrayOf()
) : MinedTransaction, SignedTransaction {
val minedBlockHeight
get() = if (minedHeight == -1L) {
null
} else {
BlockHeight(minedHeight)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is ConfirmedTransaction) return false
@ -204,7 +212,7 @@ data class ConfirmedTransaction(
result = 31 * result + (memo?.contentHashCode() ?: 0)
result = 31 * result + noteId.hashCode()
result = 31 * result + blockTimeInSeconds.hashCode()
result = 31 * result + minedHeight
result = 31 * result + minedHeight.hashCode()
result = 31 * result + transactionIndex
result = 31 * result + rawTransactionId.contentHashCode()
result = 31 * result + (toAddress?.hashCode() ?: 0)
@ -217,8 +225,12 @@ data class ConfirmedTransaction(
val ConfirmedTransaction.valueInZatoshi
get() = Zatoshi(value)
data class EncodedTransaction(val txId: ByteArray, override val raw: ByteArray, val expiryHeight: Int?) :
data class EncodedTransaction(val txId: ByteArray, override val raw: ByteArray, val expiryHeight: Long?) :
SignedTransaction {
val expiryBlockHeight
get() = expiryHeight?.let { BlockHeight(it) }
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is EncodedTransaction) return false
@ -233,7 +245,7 @@ data class EncodedTransaction(val txId: ByteArray, override val raw: ByteArray,
override fun hashCode(): Int {
var result = txId.contentHashCode()
result = 31 * result + raw.contentHashCode()
result = 31 * result + (expiryHeight ?: 0)
result = 31 * result + (expiryHeight?.hashCode() ?: 0)
return result
}
}
@ -264,7 +276,7 @@ interface SignedTransaction {
* one list for things like history. A mined tx should have all properties, except possibly a memo.
*/
interface MinedTransaction : Transaction {
val minedHeight: Int
val minedHeight: Long
val noteId: Long
val blockTimeInSeconds: Long
val transactionIndex: Int
@ -277,8 +289,8 @@ interface PendingTransaction : SignedTransaction, Transaction {
override val memo: ByteArray?
val toAddress: String
val accountIndex: Int
val minedHeight: Int
val expiryHeight: Int
val minedHeight: Long // apparently this can be -1 as an uninitialized value
val expiryHeight: Long // apparently this can be -1 as an uninitialized value
val cancelled: Int
val encodeAttempts: Int
val submitAttempts: Int
@ -337,16 +349,16 @@ fun PendingTransaction.isSubmitted(): Boolean {
return submitAttempts > 0
}
fun PendingTransaction.isExpired(latestHeight: Int?, saplingActivationHeight: Int): Boolean {
fun PendingTransaction.isExpired(latestHeight: BlockHeight?, saplingActivationHeight: BlockHeight): Boolean {
// TODO: test for off-by-one error here. Should we use <= or <
if (latestHeight == null || latestHeight < saplingActivationHeight || expiryHeight < saplingActivationHeight) return false
return expiryHeight < latestHeight
if (latestHeight == null || latestHeight.value < saplingActivationHeight.value || expiryHeight < saplingActivationHeight.value) return false
return expiryHeight < latestHeight.value
}
// if we don't have info on a pendingtx after 100 blocks then it's probably safe to stop polling!
fun PendingTransaction.isLongExpired(latestHeight: Int?, saplingActivationHeight: Int): Boolean {
if (latestHeight == null || latestHeight < saplingActivationHeight || expiryHeight < saplingActivationHeight) return false
return (latestHeight - expiryHeight) > 100
fun PendingTransaction.isLongExpired(latestHeight: BlockHeight?, saplingActivationHeight: BlockHeight): Boolean {
if (latestHeight == null || latestHeight.value < saplingActivationHeight.value || expiryHeight < saplingActivationHeight.value) return false
return (latestHeight.value - expiryHeight) > 100
}
fun PendingTransaction.isMarkedForDeletion(): Boolean {
@ -373,10 +385,10 @@ fun PendingTransaction.isSafeToDiscard(): Boolean {
}
}
fun PendingTransaction.isPending(currentHeight: Int = -1): Boolean {
fun PendingTransaction.isPending(currentHeight: BlockHeight?): Boolean {
// not mined and not expired and successfully created
return !isSubmitSuccess() && minedHeight == -1 &&
(expiryHeight == -1 || expiryHeight > currentHeight) && raw != null
return !isSubmitSuccess() && minedHeight == -1L &&
(expiryHeight == -1L || expiryHeight > (currentHeight?.value ?: 0L)) && raw != null
}
fun PendingTransaction.isSubmitSuccess(): Boolean {

View File

@ -1,5 +1,7 @@
package cash.z.ecc.android.sdk.exception
import cash.z.ecc.android.sdk.internal.model.Checkpoint
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.type.ZcashNetwork
import cash.z.wallet.sdk.rpc.Service
import io.grpc.Status
@ -15,8 +17,7 @@ open class SdkException(message: String, cause: Throwable?) : RuntimeException(m
* Exceptions thrown in the Rust layer of the SDK. We may not always be able to surface details about this
* exception so it's important for the SDK to provide helpful messages whenever these errors are encountered.
*/
sealed class RustLayerException(message: String, cause: Throwable? = null) :
SdkException(message, cause) {
sealed class RustLayerException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
class BalanceException(cause: Throwable) : RustLayerException(
"Error while requesting the current balance over " +
"JNI. This might mean that the database has been corrupted and needs to be rebuilt. Verify that " +
@ -28,13 +29,11 @@ sealed class RustLayerException(message: String, cause: Throwable? = null) :
/**
* User-facing exceptions thrown by the transaction repository.
*/
sealed class RepositoryException(message: String, cause: Throwable? = null) :
SdkException(message, cause) {
sealed class RepositoryException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
object FalseStart : RepositoryException(
"The channel is closed. Note that once a repository has stopped it " +
"cannot be restarted. Verify that the repository is not being restarted."
)
object Unprepared : RepositoryException(
"Unprepared repository: Data cannot be accessed before the repository is prepared." +
" Ensure that things have been properly initialized. If you see this error it most" +
@ -49,13 +48,11 @@ sealed class RepositoryException(message: String, cause: Throwable? = null) :
* High-level exceptions thrown by the synchronizer, which do not fall within the umbrella of a
* child component.
*/
sealed class SynchronizerException(message: String, cause: Throwable? = null) :
SdkException(message, cause) {
sealed class SynchronizerException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
object FalseStart : SynchronizerException(
"This synchronizer was already started. Multiple calls to start are not" +
"allowed and once a synchronizer has stopped it cannot be restarted."
)
object NotYetStarted : SynchronizerException(
"The synchronizer has not yet started. Verify that" +
" start has been called prior to this operation and that the coroutineScope is not" +
@ -66,16 +63,12 @@ sealed class SynchronizerException(message: String, cause: Throwable? = null) :
/**
* Potentially user-facing exceptions that occur while processing compact blocks.
*/
sealed class CompactBlockProcessorException(message: String, cause: Throwable? = null) :
SdkException(message, cause) {
sealed class CompactBlockProcessorException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
class DataDbMissing(path: String) : CompactBlockProcessorException(
"No data db file found at path $path. Verify " +
"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(
"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" +
@ -83,137 +76,100 @@ sealed class CompactBlockProcessorException(message: String, cause: Throwable? =
" So pass in context.getDatabasePath(dbFileName).absolutePath instead of just dbFileName alone.",
null
)
class FailedReorgRepair(message: String) : CompactBlockProcessorException(message)
class FailedDownload(cause: Throwable? = null) : CompactBlockProcessorException(
"Error while downloading blocks. This most " +
"likely means the server is down or slow to respond. See logs for details.",
cause
)
class FailedScan(cause: Throwable? = null) : CompactBlockProcessorException(
"Error while scanning blocks. This most " +
"likely means a block was missed or a reorg was mishandled. See logs for details.",
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(
"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" +
" can be fixed by re-importing the wallet."
)
object NoAccount : CompactBlockProcessorException(
"Attempting to scan without an account. This is probably a setup error or a race condition."
)
open class EnhanceTransactionError(message: String, val height: Int, cause: Throwable) :
CompactBlockProcessorException(message, cause) {
class EnhanceTxDownloadError(height: Int, cause: Throwable) : EnhanceTransactionError(
"Error while attempting to download a transaction to enhance",
height,
cause
)
class EnhanceTxDecryptError(height: Int, cause: Throwable) : EnhanceTransactionError(
"Error while attempting to decrypt and store a transaction to enhance",
height,
cause
)
open class EnhanceTransactionError(message: String, 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(
"Incompatible server: this client expects a server using $clientNetwork but it was $serverNetwork! Try updating the client or switching servers."
)
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."
)
class MismatchedBranch(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."
)
class MismatchedBranch(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."
)
}
/**
* Exceptions related to the wallet's birthday.
*/
sealed class BirthdayException(message: String, cause: Throwable? = null) :
SdkException(message, cause) {
sealed class BirthdayException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
object UninitializedBirthdayException : BirthdayException(
"Error the birthday cannot be" +
" accessed before it is initialized. Verify that the new, import or open functions" +
" have been called on the initializer."
)
class MissingBirthdayFilesException(directory: String) : BirthdayException(
"Cannot initialize wallet because no birthday files were found in the $directory directory."
)
class ExactBirthdayNotFoundException(height: Int, nearestMatch: Int? = null) :
BirthdayException(
"Unable to find birthday that exactly matches $height.${
if (nearestMatch != null) {
" An exact match was request but the nearest match found was $nearestMatch."
} else {
""
}
}"
)
class BirthdayFileNotFoundException(directory: String, height: Int?) : BirthdayException(
class ExactBirthdayNotFoundException internal constructor(birthday: BlockHeight, nearestMatch: Checkpoint? = null) : BirthdayException(
"Unable to find birthday that exactly matches $birthday.${
if (nearestMatch != null) {
" An exact match was request but the nearest match found was ${nearestMatch.height}."
} else ""
}"
)
class BirthdayFileNotFoundException(directory: String, height: BlockHeight?) : BirthdayException(
"Unable to find birthday file for $height verify that $directory/$height.json exists."
)
class MalformattedBirthdayFilesException(directory: String, file: String, cause: Throwable?) :
BirthdayException(
"Failed to parse file $directory/$file verify that it is formatted as #####.json, " +
"where the first portion is an Int representing the height of the tree contained in the file",
cause
)
class MalformattedBirthdayFilesException(directory: String, file: String, cause: Throwable?) : BirthdayException(
"Failed to parse file $directory/$file verify that it is formatted as #####.json, " +
"where the first portion is an Int representing the height of the tree contained in the file",
cause
)
}
/**
* Exceptions thrown by the initializer.
*/
sealed class InitializerException(message: String, cause: Throwable? = null) :
SdkException(message, cause) {
class FalseStart(cause: Throwable?) :
InitializerException("Failed to initialize accounts due to: $cause", cause)
sealed class InitializerException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
class FalseStart(cause: Throwable?) : InitializerException("Failed to initialize accounts due to: $cause", cause)
class AlreadyInitializedException(cause: Throwable, dbPath: String) : InitializerException(
"Failed to initialize the blocks table" +
" because it already exists in $dbPath",
cause
)
object MissingBirthdayException : InitializerException(
"Expected a birthday for this wallet but failed to find one. This usually means that " +
"wallet setup did not happen correctly. A workaround might be to interpret the " +
"birthday, based on the contents of the wallet data but it is probably better " +
"not to mask this error because the root issue should be addressed."
)
object MissingViewingKeyException : InitializerException(
"Expected a unified viewingKey for this wallet but failed to find one. This usually means" +
" that wallet setup happened incorrectly. A workaround might be to derive the" +
" unified viewingKey from the seed or seedPhrase, if they exist, but it is probably" +
" better not to mask this error because the root issue should be addressed."
)
class MissingAddressException(description: String, cause: Throwable? = null) :
InitializerException(
"Expected a $description address for this wallet but failed to find one. This usually" +
" means that wallet setup happened incorrectly. If this problem persists, a" +
" workaround might be to go to settings and WIPE the wallet and rescan. Doing so" +
" will restore any missing address information. Meanwhile, please report that" +
" this happened so that the root issue can be uncovered and corrected." +
if (cause != null) "\nCaused by: $cause" else ""
)
class MissingAddressException(description: String, cause: Throwable? = null) : InitializerException(
"Expected a $description address for this wallet but failed to find one. This usually" +
" means that wallet setup happened incorrectly. If this problem persists, a" +
" workaround might be to go to settings and WIPE the wallet and rescan. Doing so" +
" will restore any missing address information. Meanwhile, please report that" +
" this happened so that the root issue can be uncovered and corrected." +
if (cause != null) "\nCaused by: $cause" else ""
)
object DatabasePathException :
InitializerException(
"Critical failure to locate path for storing databases. Perhaps this device prevents" +
@ -221,11 +177,10 @@ sealed class InitializerException(message: String, cause: Throwable? = null) :
" data."
)
class InvalidBirthdayHeightException(height: Int?, network: ZcashNetwork) :
InitializerException(
"Invalid birthday height of $height. The birthday height must be at least the height of" +
" Sapling activation on ${network.networkName} (${network.saplingActivationHeight})."
)
class InvalidBirthdayHeightException(birthday: BlockHeight?, network: ZcashNetwork) : InitializerException(
"Invalid birthday height of ${birthday?.value}. The birthday height must be at least the height of" +
" Sapling activation on ${network.networkName} (${network.saplingActivationHeight})."
)
object MissingDefaultBirthdayException : InitializerException(
"The birthday height is missing and it is unclear which value to use as a default."
@ -235,15 +190,13 @@ sealed class InitializerException(message: String, cause: Throwable? = null) :
/**
* Exceptions thrown while interacting with lightwalletd.
*/
sealed class LightWalletException(message: String, cause: Throwable? = null) :
SdkException(message, cause) {
sealed class LightWalletException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
object InsecureConnection : LightWalletException(
"Error: attempted to connect to lightwalletd" +
" with an insecure connection! Plaintext connections are only allowed when the" +
" resource value for 'R.bool.lightwalletd_allow_very_insecure_connections' is true" +
" because this choice should be explicit."
)
class ConsensusBranchException(sdkBranch: String, lwdBranch: String) :
LightWalletException(
"Error: the lightwalletd server is using a consensus branch" +
@ -253,18 +206,11 @@ sealed class LightWalletException(message: String, cause: Throwable? = null) :
" update the SDK to match lightwalletd or use a lightwalletd that matches the SDK."
)
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(
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(
"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) {
companion object {
private fun Status.toMessage(): String {
return when (this.code) {
@ -282,29 +228,23 @@ sealed class LightWalletException(message: String, cause: Throwable? = null) :
/**
* Potentially user-facing exceptions thrown while encoding transactions.
*/
sealed class TransactionEncoderException(message: String, cause: Throwable? = null) :
SdkException(message, cause) {
class FetchParamsException(message: String) :
TransactionEncoderException("Failed to fetch params due to: $message")
sealed class TransactionEncoderException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
class FetchParamsException(message: String) : TransactionEncoderException("Failed to fetch params due to: $message")
object MissingParamsException : TransactionEncoderException(
"Cannot send funds due to missing spend or output params and attempting to download them failed."
)
class TransactionNotFoundException(transactionId: Long) : TransactionEncoderException(
"Unable to find transactionId " +
"$transactionId in the repository. This means the wallet created a transaction and then returned a row ID " +
"that does not actually exist. This is a scenario where the wallet should have thrown an exception but failed " +
"to do so."
)
class TransactionNotEncodedException(transactionId: Long) : TransactionEncoderException(
"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" +
" an exception but failed to do so."
)
class IncompleteScanException(lastScannedHeight: Int) : TransactionEncoderException(
class IncompleteScanException(lastScannedHeight: BlockHeight) : TransactionEncoderException(
"Cannot" +
" create spending transaction because scanning is incomplete. We must scan up to the" +
" latest height to know which consensus rules to apply. However, the last scanned" +

View File

@ -1,23 +1,24 @@
package cash.z.ecc.android.sdk.ext
import cash.z.ecc.android.sdk.model.BlockHeight
import kotlin.math.max
import kotlin.math.min
class BatchMetrics(val range: IntRange, 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 rangeStartTime = 0L
private var batchStartTime = 0L
private var batchEndTime = 0L
private var rangeSize = range.last - range.first + 1
private var rangeSize = range.endInclusive.value - range.start.value + 1
private inline fun now() = System.currentTimeMillis()
private inline fun ips(blocks: Int, time: Long) = 1000.0f * blocks / time
private inline fun ips(blocks: Long, time: Long) = 1000.0f * blocks / time
val isComplete get() = completedBatches * batchSize >= rangeSize
val isBatchComplete get() = batchEndTime > batchStartTime
val cumulativeItems get() = min(completedBatches * batchSize, rangeSize)
val cumulativeItems get() = min(completedBatches * batchSize.toLong(), rangeSize)
val cumulativeTime get() = (if (isComplete) batchEndTime else now()) - rangeStartTime
val batchTime get() = max(batchEndTime - batchStartTime, now() - batchStartTime)
val batchItems get() = min(batchSize, batchSize - (completedBatches * batchSize - rangeSize))
val batchItems get() = min(batchSize.toLong(), batchSize - (completedBatches * batchSize - rangeSize))
val batchIps get() = ips(batchItems, batchTime)
val cumulativeIps get() = ips(cumulativeItems, cumulativeTime)

View File

@ -0,0 +1,46 @@
package cash.z.ecc.android.sdk.internal
import cash.z.ecc.android.sdk.internal.model.Checkpoint
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.type.ZcashNetwork
import org.json.JSONObject
// Version is not returned from the server, so version 1 is implied. A version is declared here
// to structure the parsing to be version-aware in the future.
internal val Checkpoint.Companion.VERSION_1
get() = 1
internal val Checkpoint.Companion.KEY_VERSION
get() = "version"
internal val Checkpoint.Companion.KEY_HEIGHT
get() = "height"
internal val Checkpoint.Companion.KEY_HASH
get() = "hash"
internal val Checkpoint.Companion.KEY_EPOCH_SECONDS
get() = "time"
internal val Checkpoint.Companion.KEY_TREE
get() = "saplingTree"
internal fun Checkpoint.Companion.from(zcashNetwork: ZcashNetwork, jsonString: String) =
from(zcashNetwork, JSONObject(jsonString))
private fun Checkpoint.Companion.from(
zcashNetwork: ZcashNetwork,
jsonObject: JSONObject
): Checkpoint {
when (val version = jsonObject.optInt(Checkpoint.KEY_VERSION, Checkpoint.VERSION_1)) {
Checkpoint.VERSION_1 -> {
val height = run {
val heightLong = jsonObject.getLong(Checkpoint.KEY_HEIGHT)
BlockHeight.new(zcashNetwork, heightLong)
}
val hash = jsonObject.getString(Checkpoint.KEY_HASH)
val epochSeconds = jsonObject.getLong(Checkpoint.KEY_EPOCH_SECONDS)
val tree = jsonObject.getString(Checkpoint.KEY_TREE)
return Checkpoint(height, hash, epochSeconds, tree)
}
else -> {
throw IllegalArgumentException("Unsupported version $version")
}
}
}

View File

@ -0,0 +1,5 @@
package cash.z.ecc.android.sdk.internal
import cash.z.ecc.android.sdk.model.BlockHeight
internal fun ClosedRange<BlockHeight>?.isEmpty() = this?.isEmpty() ?: true

View File

@ -1,37 +0,0 @@
package cash.z.ecc.android.sdk.internal
import cash.z.ecc.android.sdk.type.WalletBirthday
import org.json.JSONObject
// Version is not returned from the server, so version 1 is implied. A version is declared here
// to structure the parsing to be version-aware in the future.
internal val WalletBirthday.Companion.VERSION_1
get() = 1
internal val WalletBirthday.Companion.KEY_VERSION
get() = "version"
internal val WalletBirthday.Companion.KEY_HEIGHT
get() = "height"
internal val WalletBirthday.Companion.KEY_HASH
get() = "hash"
internal val WalletBirthday.Companion.KEY_EPOCH_SECONDS
get() = "time"
internal val WalletBirthday.Companion.KEY_TREE
get() = "saplingTree"
fun WalletBirthday.Companion.from(jsonString: String) = from(JSONObject(jsonString))
private fun WalletBirthday.Companion.from(jsonObject: JSONObject): WalletBirthday {
when (val version = jsonObject.optInt(WalletBirthday.KEY_VERSION, WalletBirthday.VERSION_1)) {
WalletBirthday.VERSION_1 -> {
val height = jsonObject.getInt(WalletBirthday.KEY_HEIGHT)
val hash = jsonObject.getString(WalletBirthday.KEY_HASH)
val epochSeconds = jsonObject.getLong(WalletBirthday.KEY_EPOCH_SECONDS)
val tree = jsonObject.getString(WalletBirthday.KEY_TREE)
return WalletBirthday(height, hash, epochSeconds, tree)
}
else -> {
throw IllegalArgumentException("Unsupported version $version")
}
}
}

View File

@ -7,6 +7,8 @@ import cash.z.ecc.android.sdk.db.entity.CompactBlockEntity
import cash.z.ecc.android.sdk.internal.SdkDispatchers
import cash.z.ecc.android.sdk.internal.SdkExecutors
import cash.z.ecc.android.sdk.internal.db.CompactBlockDb
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.type.ZcashNetwork
import cash.z.wallet.sdk.rpc.CompactFormats
import kotlinx.coroutines.withContext
import kotlin.math.max
@ -16,21 +18,22 @@ import kotlin.math.max
* path. This represents the "cache db" or local cache of compact blocks waiting to be scanned.
*/
class CompactBlockDbStore private constructor(
private val network: ZcashNetwork,
private val cacheDb: CompactBlockDb
) : CompactBlockStore {
private val cacheDao = cacheDb.compactBlockDao()
override suspend fun getLatestHeight(): Int = max(0, cacheDao.latestBlockHeight())
override suspend fun getLatestHeight(): BlockHeight = BlockHeight.new(network, max(0L, cacheDao.latestBlockHeight()))
override suspend fun findCompactBlock(height: Int): CompactFormats.CompactBlock? =
cacheDao.findCompactBlock(height)?.let { CompactFormats.CompactBlock.parseFrom(it) }
override suspend fun findCompactBlock(height: BlockHeight): CompactFormats.CompactBlock? =
cacheDao.findCompactBlock(height.value)?.let { CompactFormats.CompactBlock.parseFrom(it) }
override suspend fun write(result: List<CompactFormats.CompactBlock>) =
cacheDao.insert(result.map { CompactBlockEntity(it.height.toInt(), it.toByteArray()) })
override suspend fun rewindTo(height: Int) =
cacheDao.rewindTo(height)
override suspend fun rewindTo(height: BlockHeight) =
cacheDao.rewindTo(height.value)
override suspend fun close() {
withContext(SdkDispatchers.DATABASE_IO) {
@ -43,10 +46,10 @@ class CompactBlockDbStore private constructor(
* @param appContext the application context. This is used for creating the database.
* @property dbPath the absolute path to the database.
*/
fun new(appContext: Context, dbPath: String): CompactBlockDbStore {
fun new(appContext: Context, zcashNetwork: ZcashNetwork, dbPath: String): CompactBlockDbStore {
val cacheDb = createCompactBlockCacheDb(appContext.applicationContext, dbPath)
return CompactBlockDbStore(cacheDb)
return CompactBlockDbStore(zcashNetwork, cacheDb)
}
private fun createCompactBlockCacheDb(

View File

@ -4,6 +4,7 @@ import cash.z.ecc.android.sdk.internal.ext.retryUpTo
import cash.z.ecc.android.sdk.internal.ext.tryWarn
import cash.z.ecc.android.sdk.internal.service.LightWalletService
import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.wallet.sdk.rpc.Service
import io.grpc.StatusRuntimeException
import kotlinx.coroutines.CoroutineScope
@ -43,7 +44,7 @@ open class CompactBlockDownloader private constructor(val compactBlockStore: Com
*
* @return the number of blocks that were returned in the results from the lightwalletService.
*/
suspend fun downloadBlockRange(heightRange: IntRange): Int = withContext(IO) {
suspend fun downloadBlockRange(heightRange: ClosedRange<BlockHeight>): Int = withContext(IO) {
val result = lightWalletService.getBlockRange(heightRange)
compactBlockStore.write(result)
result.size
@ -55,7 +56,7 @@ open class CompactBlockDownloader private constructor(val compactBlockStore: Com
*
* @param height the height to which the data will rewind.
*/
suspend fun rewindToHeight(height: Int) =
suspend fun rewindToHeight(height: BlockHeight) =
// TODO: cancel anything in flight
compactBlockStore.rewindTo(height)

View File

@ -1,5 +1,6 @@
package cash.z.ecc.android.sdk.internal.block
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.wallet.sdk.rpc.CompactFormats
/**
@ -11,14 +12,14 @@ interface CompactBlockStore {
*
* @return the latest block height.
*/
suspend fun getLatestHeight(): Int
suspend fun getLatestHeight(): BlockHeight
/**
* Fetch the compact block for the given height, if it exists.
*
* @return the compact block or null when it did not exist.
*/
suspend fun findCompactBlock(height: Int): CompactFormats.CompactBlock?
suspend fun findCompactBlock(height: BlockHeight): CompactFormats.CompactBlock?
/**
* Write the given blocks to this store, which may be anything from an in-memory cache to a DB.
@ -32,7 +33,7 @@ interface CompactBlockStore {
*
* @param height the target height to which to rewind.
*/
suspend fun rewindTo(height: Int)
suspend fun rewindTo(height: BlockHeight)
/**
* Close any connections to the block store.

View File

@ -43,11 +43,11 @@ interface CompactBlockDao {
suspend fun insert(block: List<CompactBlockEntity>)
@Query("DELETE FROM compactblocks WHERE height > :height")
suspend fun rewindTo(height: Int)
suspend fun rewindTo(height: Long)
@Query("SELECT MAX(height) FROM compactblocks")
suspend fun latestBlockHeight(): Int
suspend fun latestBlockHeight(): Long
@Query("SELECT data FROM compactblocks WHERE height = :height")
suspend fun findCompactBlock(height: Int): ByteArray?
suspend fun findCompactBlock(height: Long): ByteArray?
}

View File

@ -198,13 +198,13 @@ interface BlockDao {
suspend fun count(): Int
@Query("SELECT MAX(height) FROM blocks")
suspend fun lastScannedHeight(): Int
suspend fun lastScannedHeight(): Long
@Query("SELECT MIN(height) FROM blocks")
suspend fun firstScannedHeight(): Int
suspend fun firstScannedHeight(): Long
@Query("SELECT hash FROM BLOCKS WHERE height = :height")
suspend fun findHashByHeight(height: Int): ByteArray?
suspend fun findHashByHeight(height: Long): ByteArray?
}
/**
@ -273,7 +273,7 @@ interface TransactionDao {
LIMIT 1
"""
)
suspend fun findMinedHeight(rawTransactionId: ByteArray): Int?
suspend fun findMinedHeight(rawTransactionId: ByteArray): Long?
/**
* Query sent transactions that have been mined, sorted so the newest data is at the top.
@ -418,7 +418,7 @@ interface TransactionDao {
LIMIT :limit
"""
)
suspend fun findAllTransactionsByRange(blockRangeStart: Int, blockRangeEnd: Int = 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
// This should probably be a rust call but there's not a lot of bandwidth for this
@ -474,7 +474,7 @@ interface TransactionDao {
}
@Transaction
suspend fun deleteExpired(lastHeight: Int): Int {
suspend fun deleteExpired(lastHeight: Long): Int {
var count = 0
findExpiredTxs(lastHeight).forEach { transactionId ->
if (removeInvalidOutboundTransaction(transactionId)) count++
@ -537,5 +537,5 @@ interface TransactionDao {
AND expiry_height < :lastheight
"""
)
suspend fun findExpiredTxs(lastheight: Int): List<Long>
suspend fun findExpiredTxs(lastheight: Long): List<Long>
}

View File

@ -70,10 +70,10 @@ interface PendingTransactionDao {
suspend fun removeRawTransactionId(id: Long)
@Query("UPDATE pending_transactions SET minedHeight = :minedHeight WHERE id = :id")
suspend fun updateMinedHeight(id: Long, minedHeight: Int)
suspend fun updateMinedHeight(id: Long, minedHeight: Long)
@Query("UPDATE pending_transactions SET raw = :raw, rawTransactionId = :rawTransactionId, expiryHeight = :expiryHeight WHERE id = :id")
suspend fun updateEncoding(id: Long, raw: ByteArray, rawTransactionId: ByteArray, expiryHeight: Int?)
suspend fun updateEncoding(id: Long, raw: ByteArray, rawTransactionId: ByteArray, expiryHeight: Long?)
@Query("UPDATE pending_transactions SET errorMessage = :errorMessage, errorCode = :errorCode WHERE id = :id")
suspend fun updateError(id: Long, errorMessage: String?, errorCode: Int?)

View File

@ -0,0 +1,22 @@
package cash.z.ecc.android.sdk.internal.model
import cash.z.ecc.android.sdk.model.BlockHeight
/**
* Represents a checkpoint, which is used to speed sync times.
*
* @param height the height of the checkpoint.
* @param hash the hash of the block at [height].
* @param epochSeconds the time of the block at [height].
* @param tree the sapling tree corresponding to [height].
*/
internal data class Checkpoint(
val height: BlockHeight,
val hash: String,
// Note: this field does NOT match the name of the JSON, so will break with field-based JSON parsing
val epochSeconds: Long,
// Note: this field does NOT match the name of the JSON, so will break with field-based JSON parsing
val tree: String
) {
internal companion object
}

View File

@ -5,6 +5,7 @@ import cash.z.ecc.android.sdk.R
import cash.z.ecc.android.sdk.annotation.OpenForTesting
import cash.z.ecc.android.sdk.exception.LightWalletException
import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.type.ZcashNetwork
import cash.z.wallet.sdk.rpc.CompactFormats
import cash.z.wallet.sdk.rpc.CompactTxStreamerGrpc
@ -15,22 +16,24 @@ import io.grpc.ConnectivityState
import io.grpc.ManagedChannel
import io.grpc.android.AndroidChannelBuilder
import java.util.concurrent.TimeUnit
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
/**
* Implementation of LightwalletService using gRPC for requests to lightwalletd.
*
* @property channel the channel to use for communicating with the lightwalletd server.
* @property singleRequestTimeoutSec the timeout to use for non-streaming requests. When a new stub
* @property singleRequestTimeout the timeout to use for non-streaming requests. When a new stub
* is created, it will use a deadline that is after the given duration from now.
* @property streamingRequestTimeoutSec the timeout to use for streaming requests. When a new stub
* @property streamingRequestTimeout the timeout to use for streaming requests. When a new stub
* is created for streaming requests, it will use a deadline that is after the given duration from
* now.
*/
@OpenForTesting
class LightWalletGrpcService private constructor(
var channel: ManagedChannel,
private val singleRequestTimeoutSec: Long = 10L,
private val streamingRequestTimeoutSec: Long = 90L
private val singleRequestTimeout: Duration = 10.seconds,
private val streamingRequestTimeout: Duration = 90.seconds
) : LightWalletService {
lateinit var connectionInfo: ConnectionInfo
@ -64,20 +67,22 @@ class LightWalletGrpcService private constructor(
/* LightWalletService implementation */
override fun getBlockRange(heightRange: IntRange): List<CompactFormats.CompactBlock> {
override fun getBlockRange(heightRange: ClosedRange<BlockHeight>): List<CompactFormats.CompactBlock> {
if (heightRange.isEmpty()) return listOf()
return requireChannel().createStub(streamingRequestTimeoutSec)
return requireChannel().createStub(streamingRequestTimeout)
.getBlockRange(heightRange.toBlockRange()).toList()
}
override fun getLatestBlockHeight(): Int {
return requireChannel().createStub(singleRequestTimeoutSec)
.getLatestBlock(Service.ChainSpec.newBuilder().build()).height.toInt()
override fun getLatestBlockHeight(): BlockHeight {
return BlockHeight(
requireChannel().createStub(singleRequestTimeout)
.getLatestBlock(Service.ChainSpec.newBuilder().build()).height
)
}
override fun getServerInfo(): Service.LightdInfo {
return requireChannel().createStub(singleRequestTimeoutSec)
return requireChannel().createStub(singleRequestTimeout)
.getLightdInfo(Service.Empty.newBuilder().build())
}
@ -111,18 +116,18 @@ class LightWalletGrpcService private constructor(
override fun fetchUtxos(
tAddress: String,
startHeight: Int
startHeight: BlockHeight
): List<Service.GetAddressUtxosReply> {
val result = requireChannel().createStub().getAddressUtxos(
Service.GetAddressUtxosArg.newBuilder().setAddress(tAddress)
.setStartHeight(startHeight.toLong()).build()
.setStartHeight(startHeight.value).build()
)
return result.addressUtxosList
}
override fun getTAddressTransactions(
tAddress: String,
blockHeightRange: IntRange
blockHeightRange: ClosedRange<BlockHeight>
): List<Service.RawTransaction> {
if (blockHeightRange.isEmpty() || tAddress.isBlank()) return listOf()
@ -164,18 +169,9 @@ class LightWalletGrpcService private constructor(
// Utilities
//
private fun Channel.createStub(timeoutSec: Long = 60L) = CompactTxStreamerGrpc
private fun Channel.createStub(timeoutSec: Duration = 60.seconds) = CompactTxStreamerGrpc
.newBlockingStub(this)
.withDeadlineAfter(timeoutSec, TimeUnit.SECONDS)
private inline fun Int.toBlockHeight(): Service.BlockID =
Service.BlockID.newBuilder().setHeight(this.toLong()).build()
private inline fun IntRange.toBlockRange(): Service.BlockRange =
Service.BlockRange.newBuilder()
.setStart(first.toBlockHeight())
.setEnd(last.toBlockHeight())
.build()
.withDeadlineAfter(timeoutSec.inWholeSeconds, TimeUnit.SECONDS)
/**
* This function effectively parses streaming responses. Each call to next(), on the iterators
@ -231,3 +227,12 @@ class LightWalletGrpcService private constructor(
}
}
}
private fun BlockHeight.toBlockHeight(): Service.BlockID =
Service.BlockID.newBuilder().setHeight(value).build()
private fun ClosedRange<BlockHeight>.toBlockRange(): Service.BlockRange =
Service.BlockRange.newBuilder()
.setStart(start.toBlockHeight())
.setEnd(endInclusive.toBlockHeight())
.build()

View File

@ -1,5 +1,6 @@
package cash.z.ecc.android.sdk.internal.service
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.wallet.sdk.rpc.CompactFormats
import cash.z.wallet.sdk.rpc.Service
@ -24,7 +25,7 @@ interface LightWalletService {
*
* @return the UTXOs for the given address from the startHeight.
*/
fun fetchUtxos(tAddress: String, startHeight: Int): List<Service.GetAddressUtxosReply>
fun fetchUtxos(tAddress: String, startHeight: BlockHeight): List<Service.GetAddressUtxosReply>
/**
* Return the given range of blocks.
@ -35,14 +36,14 @@ interface LightWalletService {
* @return a list of compact blocks for the given range
*
*/
fun getBlockRange(heightRange: IntRange): List<CompactFormats.CompactBlock>
fun getBlockRange(heightRange: ClosedRange<BlockHeight>): List<CompactFormats.CompactBlock>
/**
* Return the latest block height known to the service.
*
* @return the latest block height known to the service.
*/
fun getLatestBlockHeight(): Int
fun getLatestBlockHeight(): BlockHeight
/**
* Return basic information about the server such as:
@ -70,7 +71,7 @@ interface LightWalletService {
*
* @return a list of transactions that correspond to the given address for the given range.
*/
fun getTAddressTransactions(tAddress: String, blockHeightRange: IntRange): 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

View File

@ -12,12 +12,13 @@ import cash.z.ecc.android.sdk.internal.db.DerivedDataDb
import cash.z.ecc.android.sdk.internal.ext.android.toFlowPagedList
import cash.z.ecc.android.sdk.internal.ext.android.toRefreshable
import cash.z.ecc.android.sdk.internal.ext.tryWarn
import cash.z.ecc.android.sdk.internal.model.Checkpoint
import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.jni.RustBackend
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.type.UnifiedViewingKey
import cash.z.ecc.android.sdk.type.WalletBirthday
import cash.z.ecc.android.sdk.type.ZcashNetwork
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.withContext
@ -28,7 +29,8 @@ import kotlinx.coroutines.withContext
*
* @param pageSize transactions per page. This influences pre-fetch and memory configuration.
*/
class PagedTransactionRepository private constructor(
internal class PagedTransactionRepository private constructor(
private val zcashNetwork: ZcashNetwork,
private val db: DerivedDataDb,
private val pageSize: Int
) : TransactionRepository {
@ -62,20 +64,20 @@ class PagedTransactionRepository private constructor(
override fun invalidate() = allTransactionsFactory.refresh()
override suspend fun lastScannedHeight() = blocks.lastScannedHeight()
override suspend fun lastScannedHeight() = BlockHeight.new(zcashNetwork, blocks.lastScannedHeight())
override suspend fun firstScannedHeight() = blocks.firstScannedHeight()
override suspend fun firstScannedHeight() = BlockHeight.new(zcashNetwork, blocks.firstScannedHeight())
override suspend fun isInitialized() = blocks.count() > 0
override suspend fun findEncodedTransactionById(txId: Long) =
transactions.findEncodedTransactionById(txId)
override suspend fun findNewTransactions(blockHeightRange: IntRange): List<ConfirmedTransaction> =
transactions.findAllTransactionsByRange(blockHeightRange.first, blockHeightRange.last)
override suspend fun findNewTransactions(blockHeightRange: ClosedRange<BlockHeight>): List<ConfirmedTransaction> =
transactions.findAllTransactionsByRange(blockHeightRange.start.value, blockHeightRange.endInclusive.value)
override suspend fun findMinedHeight(rawTransactionId: ByteArray) =
transactions.findMinedHeight(rawTransactionId)
transactions.findMinedHeight(rawTransactionId)?.let { BlockHeight.new(zcashNetwork, it) }
override suspend fun findMatchingTransactionId(rawTransactionId: ByteArray): Long? =
transactions.findMatchingTransactionId(rawTransactionId)
@ -84,8 +86,8 @@ class PagedTransactionRepository private constructor(
transactions.cleanupCancelledTx(rawTransactionId)
// let expired transactions linger in the UI for a little while
override suspend fun deleteExpired(lastScannedHeight: Int) =
transactions.deleteExpired(lastScannedHeight - (ZcashSdk.EXPIRY_OFFSET / 2))
override suspend fun deleteExpired(lastScannedHeight: BlockHeight) =
transactions.deleteExpired(lastScannedHeight.value - (ZcashSdk.EXPIRY_OFFSET / 2))
override suspend fun count() = transactions.count()
@ -103,17 +105,18 @@ class PagedTransactionRepository private constructor(
}
// TODO: begin converting these into Data Access API. For now, just collect the desired operations and iterate/refactor, later
suspend fun findBlockHash(height: Int): ByteArray? = blocks.findHashByHeight(height)
suspend fun findBlockHash(height: BlockHeight): ByteArray? = blocks.findHashByHeight(height.value)
suspend fun getTransactionCount(): Int = transactions.count()
// TODO: convert this into a wallet repository rather than "transaction repository"
companion object {
suspend fun new(
internal suspend fun new(
appContext: Context,
zcashNetwork: ZcashNetwork,
pageSize: Int = 10,
rustBackend: RustBackend,
birthday: WalletBirthday,
birthday: Checkpoint,
viewingKeys: List<UnifiedViewingKey>,
overwriteVks: Boolean = false
): PagedTransactionRepository {
@ -122,7 +125,7 @@ class PagedTransactionRepository private constructor(
val db = buildDatabase(appContext.applicationContext, rustBackend.pathDataDb)
applyKeyMigrations(rustBackend, overwriteVks, viewingKeys)
return PagedTransactionRepository(db, pageSize)
return PagedTransactionRepository(zcashNetwork, db, pageSize)
}
/**
@ -155,7 +158,7 @@ class PagedTransactionRepository private constructor(
*/
private suspend fun initMissingDatabases(
rustBackend: RustBackend,
birthday: WalletBirthday,
birthday: Checkpoint,
viewingKeys: List<UnifiedViewingKey>
) {
maybeCreateDataDb(rustBackend)
@ -178,20 +181,15 @@ class PagedTransactionRepository private constructor(
*/
private suspend fun maybeInitBlocksTable(
rustBackend: RustBackend,
birthday: WalletBirthday
checkpoint: Checkpoint
) {
// TODO: consider converting these to typed exceptions in the welding layer
tryWarn(
"Warning: did not initialize the blocks table. It probably was already initialized.",
ifContains = "table is not empty"
) {
rustBackend.initBlocksTable(
birthday.height,
birthday.hash,
birthday.time,
birthday.tree
)
twig("seeded the database with sapling tree at height ${birthday.height}")
rustBackend.initBlocksTable(checkpoint)
twig("seeded the database with sapling tree at height ${checkpoint.height}")
}
twig("database file: ${rustBackend.pathDataDb}")
}

View File

@ -12,6 +12,7 @@ import cash.z.ecc.android.sdk.internal.db.PendingTransactionDao
import cash.z.ecc.android.sdk.internal.db.PendingTransactionDb
import cash.z.ecc.android.sdk.internal.service.LightWalletService
import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.Zatoshi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.IO
@ -98,10 +99,10 @@ class PersistentTransactionManager(
tx
}
override suspend fun applyMinedHeight(pendingTx: PendingTransaction, minedHeight: Int) {
override suspend fun applyMinedHeight(pendingTx: PendingTransaction, minedHeight: BlockHeight) {
twig("a pending transaction has been mined!")
safeUpdate("updating mined height for pending tx id: ${pendingTx.id} to $minedHeight") {
updateMinedHeight(pendingTx.id, minedHeight)
updateMinedHeight(pendingTx.id, minedHeight.value)
}
}

View File

@ -10,7 +10,7 @@ interface TransactionEncoder {
* exception ourselves (rather than using double-bangs for things).
*
* @param spendingKey the key associated with the notes that will be spent.
* @param zatoshi the amount of zatoshi to send.
* @param amount the amount of zatoshi to send.
* @param toAddress the recipient's address.
* @param memo the optional memo to include as part of the transaction.
* @param fromAccountIndex the optional account id to use. By default, the 1st account is used.

View File

@ -1,6 +1,7 @@
package cash.z.ecc.android.sdk.internal.transaction
import cash.z.ecc.android.sdk.db.entity.PendingTransaction
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.Zatoshi
import kotlinx.coroutines.flow.Flow
@ -65,7 +66,7 @@ interface OutboundTransactionManager {
* @param minedHeight the height at which the given transaction was mined, according to the data
* that has been processed from the blockchain.
*/
suspend fun applyMinedHeight(pendingTx: PendingTransaction, minedHeight: Int)
suspend fun applyMinedHeight(pendingTx: PendingTransaction, minedHeight: BlockHeight)
/**
* Generate a flow of information about the given id where a new pending transaction is emitted

View File

@ -2,6 +2,7 @@ package cash.z.ecc.android.sdk.internal.transaction
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
import cash.z.ecc.android.sdk.db.entity.EncodedTransaction
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.type.UnifiedAddressAccount
import kotlinx.coroutines.flow.Flow
@ -15,14 +16,14 @@ interface TransactionRepository {
*
* @return the last height scanned by this repository.
*/
suspend fun lastScannedHeight(): Int
suspend fun lastScannedHeight(): BlockHeight
/**
* The height of the first block in this repository. This is typically the checkpoint that was
* used to initialize this wallet. If we overwrite this block, it breaks our ability to spend
* funds.
*/
suspend fun firstScannedHeight(): Int
suspend fun firstScannedHeight(): BlockHeight
/**
* Returns true when this repository has been initialized and seeded with the initial checkpoint.
@ -51,7 +52,7 @@ interface TransactionRepository {
*
* @return a list of transactions that were mined in the given range, inclusive.
*/
suspend fun findNewTransactions(blockHeightRange: IntRange): List<ConfirmedTransaction>
suspend fun findNewTransactions(blockHeightRange: ClosedRange<BlockHeight>): List<ConfirmedTransaction>
/**
* Find the mined height that matches the given raw tx_id in bytes. This is useful for matching
@ -61,7 +62,7 @@ interface TransactionRepository {
*
* @return the mined height of the given transaction, if it is known to this wallet.
*/
suspend fun findMinedHeight(rawTransactionId: ByteArray): Int?
suspend fun findMinedHeight(rawTransactionId: ByteArray): BlockHeight?
suspend fun findMatchingTransactionId(rawTransactionId: ByteArray): Long?
@ -79,7 +80,7 @@ interface TransactionRepository {
*/
suspend fun cleanupCancelledTx(rawTransactionId: ByteArray): Boolean
suspend fun deleteExpired(lastScannedHeight: Int): Int
suspend fun deleteExpired(lastScannedHeight: BlockHeight): Int
suspend fun count(): Int

View File

@ -1,11 +1,12 @@
package cash.z.ecc.android.sdk.jni
import cash.z.ecc.android.sdk.exception.BirthdayException
import cash.z.ecc.android.sdk.ext.ZcashSdk.OUTPUT_PARAM_FILE_NAME
import cash.z.ecc.android.sdk.ext.ZcashSdk.SPEND_PARAM_FILE_NAME
import cash.z.ecc.android.sdk.internal.SdkDispatchers
import cash.z.ecc.android.sdk.internal.ext.deleteSuspend
import cash.z.ecc.android.sdk.internal.model.Checkpoint
import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.WalletBalance
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.tool.DerivationTool
@ -19,21 +20,13 @@ import java.io.File
* 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.
*/
class RustBackend private constructor() : RustBackendWelding {
// Paths
lateinit var pathDataDb: String
internal set
lateinit var pathCacheDb: String
internal set
lateinit var pathParamsDir: String
internal set
override lateinit var network: ZcashNetwork
internal var birthdayHeight: Int = -1
get() = if (field != -1) field else throw BirthdayException.UninitializedBirthdayException
private set
internal class RustBackend private constructor(
override val network: ZcashNetwork,
val birthdayHeight: BlockHeight,
val pathDataDb: String,
val pathCacheDb: String,
val pathParamsDir: String
) : RustBackendWelding {
suspend fun clear(clearCacheDb: Boolean = true, clearDataDb: Boolean = true) {
if (clearCacheDb) {
@ -84,18 +77,15 @@ class RustBackend private constructor() : RustBackendWelding {
}
override suspend fun initBlocksTable(
height: Int,
hash: String,
time: Long,
saplingTree: String
checkpoint: Checkpoint
): Boolean {
return withContext(SdkDispatchers.DATABASE_IO) {
initBlocksTable(
pathDataDb,
height,
hash,
time,
saplingTree,
checkpoint.height.value,
checkpoint.hash,
checkpoint.epochSeconds,
checkpoint.tree,
networkId = network.id
)
}
@ -156,19 +146,28 @@ class RustBackend private constructor() : RustBackendWelding {
}
override suspend fun validateCombinedChain() = withContext(SdkDispatchers.DATABASE_IO) {
validateCombinedChain(
val validationResult = validateCombinedChain(
pathCacheDb,
pathDataDb,
networkId = network.id
)
if (-1L == validationResult) {
null
} else {
BlockHeight.new(network, validationResult)
}
}
override suspend fun getNearestRewindHeight(height: Int): Int =
override suspend fun getNearestRewindHeight(height: BlockHeight): BlockHeight =
withContext(SdkDispatchers.DATABASE_IO) {
getNearestRewindHeight(
pathDataDb,
height,
networkId = network.id
BlockHeight.new(
network,
getNearestRewindHeight(
pathDataDb,
height.value,
networkId = network.id
)
)
}
@ -177,11 +176,11 @@ class RustBackend private constructor() : RustBackendWelding {
*
* DELETE FROM blocks WHERE height > ?
*/
override suspend fun rewindToHeight(height: Int) =
override suspend fun rewindToHeight(height: BlockHeight) =
withContext(SdkDispatchers.DATABASE_IO) {
rewindToHeight(
pathDataDb,
height,
height.value,
networkId = network.id
)
}
@ -264,7 +263,7 @@ class RustBackend private constructor() : RustBackendWelding {
index: Int,
script: ByteArray,
value: Long,
height: Int
height: BlockHeight
): Boolean = withContext(SdkDispatchers.DATABASE_IO) {
putUtxo(
pathDataDb,
@ -273,19 +272,21 @@ class RustBackend private constructor() : RustBackendWelding {
index,
script,
value,
height,
height.value,
networkId = network.id
)
}
override suspend fun clearUtxos(
tAddress: String,
aboveHeight: Int
aboveHeightInclusive: BlockHeight
): Boolean = withContext(SdkDispatchers.DATABASE_IO) {
clearUtxos(
pathDataDb,
tAddress,
aboveHeight,
// The Kotlin API is inclusive, but the Rust API is exclusive.
// This can create invalid BlockHeights if the height is saplingActivationHeight.
aboveHeightInclusive.value - 1,
networkId = network.id
)
}
@ -314,8 +315,8 @@ class RustBackend private constructor() : RustBackendWelding {
override fun isValidTransparentAddr(addr: String) =
isValidTransparentAddress(addr, networkId = network.id)
override fun getBranchIdForHeight(height: Int): Long =
branchIdForHeight(height, networkId = network.id)
override fun getBranchIdForHeight(height: BlockHeight): Long =
branchIdForHeight(height.value, networkId = network.id)
// /**
// * This is a proof-of-concept for doing Local RPC, where we are effectively using the JNI
@ -351,19 +352,17 @@ class RustBackend private constructor() : RustBackendWelding {
dataDbPath: String,
paramsPath: String,
zcashNetwork: ZcashNetwork,
birthdayHeight: Int? = null
birthdayHeight: BlockHeight
): RustBackend {
rustLibraryLoader.load()
return RustBackend().apply {
pathCacheDb = cacheDbPath
pathDataDb = dataDbPath
return RustBackend(
zcashNetwork,
birthdayHeight,
pathDataDb = dataDbPath,
pathCacheDb = cacheDbPath,
pathParamsDir = paramsPath
network = zcashNetwork
if (birthdayHeight != null) {
this.birthdayHeight = birthdayHeight
}
}
)
}
/**
@ -396,7 +395,7 @@ class RustBackend private constructor() : RustBackendWelding {
@JvmStatic
private external fun initBlocksTable(
dbDataPath: String,
height: Int,
height: Long,
hash: String,
time: Long,
saplingTree: String,
@ -445,19 +444,19 @@ class RustBackend private constructor() : RustBackendWelding {
dbCachePath: String,
dbDataPath: String,
networkId: Int
): Int
): Long
@JvmStatic
private external fun getNearestRewindHeight(
dbDataPath: String,
height: Int,
height: Long,
networkId: Int
): Int
): Long
@JvmStatic
private external fun rewindToHeight(
dbDataPath: String,
height: Int,
height: Long,
networkId: Int
): Boolean
@ -513,7 +512,7 @@ class RustBackend private constructor() : RustBackendWelding {
private external fun initLogs()
@JvmStatic
private external fun branchIdForHeight(height: Int, networkId: Int): Long
private external fun branchIdForHeight(height: Long, networkId: Int): Long
@JvmStatic
private external fun putUtxo(
@ -523,7 +522,7 @@ class RustBackend private constructor() : RustBackendWelding {
index: Int,
script: ByteArray,
value: Long,
height: Int,
height: Long,
networkId: Int
): Boolean
@ -531,7 +530,7 @@ class RustBackend private constructor() : RustBackendWelding {
private external fun clearUtxos(
dbDataPath: String,
tAddress: String,
aboveHeight: Int,
aboveHeight: Long,
networkId: Int
): Boolean

View File

@ -1,5 +1,7 @@
package cash.z.ecc.android.sdk.jni
import cash.z.ecc.android.sdk.internal.model.Checkpoint
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.WalletBalance
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.type.UnifiedViewingKey
@ -11,7 +13,7 @@ import cash.z.ecc.android.sdk.type.ZcashNetwork
* It is not documented because it is not intended to be used, directly.
* Instead, use the synchronizer or one of its subcomponents.
*/
interface RustBackendWelding {
internal interface RustBackendWelding {
val network: ZcashNetwork
@ -36,7 +38,7 @@ interface RustBackendWelding {
suspend fun initAccountsTable(vararg keys: UnifiedViewingKey): Boolean
suspend fun initBlocksTable(height: Int, hash: String, time: Long, saplingTree: String): Boolean
suspend fun initBlocksTable(checkpoint: Checkpoint): Boolean
suspend fun initDataDb(): Boolean
@ -50,7 +52,7 @@ interface RustBackendWelding {
suspend fun getBalance(account: Int = 0): Zatoshi
fun getBranchIdForHeight(height: Int): Long
fun getBranchIdForHeight(height: BlockHeight): Long
suspend fun getReceivedMemoAsUtf8(idNote: Long): String
@ -60,13 +62,16 @@ interface RustBackendWelding {
// fun parseTransactionDataList(tdl: LocalRpcTypes.TransactionDataList): LocalRpcTypes.TransparentTransactionList
suspend fun getNearestRewindHeight(height: Int): Int
suspend fun getNearestRewindHeight(height: BlockHeight): BlockHeight
suspend fun rewindToHeight(height: Int): Boolean
suspend fun rewindToHeight(height: BlockHeight): Boolean
suspend fun scanBlocks(limit: Int = -1): Boolean
suspend fun validateCombinedChain(): Int
/**
* @return Null if successful. If an error occurs, the height will be the height where the error was detected.
*/
suspend fun validateCombinedChain(): BlockHeight?
suspend fun putUtxo(
tAddress: String,
@ -74,10 +79,10 @@ interface RustBackendWelding {
index: Int,
script: ByteArray,
value: Long,
height: Int
height: BlockHeight
): Boolean
suspend fun clearUtxos(tAddress: String, aboveHeight: Int = network.saplingActivationHeight - 1): Boolean
suspend fun clearUtxos(tAddress: String, aboveHeightInclusive: BlockHeight = BlockHeight(network.saplingActivationHeight.value)): Boolean
suspend fun getDownloadedUtxoBalance(address: String): WalletBalance

View File

@ -0,0 +1,68 @@
package cash.z.ecc.android.sdk.model
import android.content.Context
import cash.z.ecc.android.sdk.tool.CheckpointTool
import cash.z.ecc.android.sdk.type.ZcashNetwork
/**
* Represents a block height, which is a UInt32. SDK clients use this class to represent the "birthday" of a wallet.
*
* New instances are constructed using the [new] factory method.
*
* @param value The block height. Must be in range of a UInt32.
*/
/*
* For easier compatibility with Java clients, this class represents the height value as a Long with
* assertions to ensure that it is a 32-bit unsigned integer.
*/
data class BlockHeight internal constructor(val value: Long) : Comparable<BlockHeight> {
init {
require(UINT_RANGE.contains(value)) { "Height $value is outside of allowed range $UINT_RANGE" }
}
override fun compareTo(other: BlockHeight): Int = value.compareTo(other.value)
operator fun plus(other: BlockHeight) = BlockHeight(value + other.value)
operator fun plus(other: Int): BlockHeight {
if (other < 0) {
throw IllegalArgumentException("Cannot add negative value $other to BlockHeight")
}
return BlockHeight(value + other.toLong())
}
operator fun plus(other: Long): BlockHeight {
if (other < 0) {
throw IllegalArgumentException("Cannot add negative value $other to BlockHeight")
}
return BlockHeight(value + other)
}
companion object {
private val UINT_RANGE = 0.toLong()..UInt.MAX_VALUE.toLong()
/**
* @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.
*/
fun new(zcashNetwork: ZcashNetwork, blockHeight: Long): BlockHeight {
require(blockHeight >= zcashNetwork.saplingActivationHeight.value) {
"Height $blockHeight is below sapling activation height ${zcashNetwork.saplingActivationHeight}"
}
return BlockHeight(blockHeight)
}
/**
* Useful when creating a new wallet to reduce sync times.
*
* @param zcashNetwork Network to use for the block height.
* @return The block height of the newest checkpoint known by the SDK.
*/
suspend fun ofLatestCheckpoint(context: Context, zcashNetwork: ZcashNetwork): BlockHeight {
return CheckpointTool.loadNearest(context, zcashNetwork, null).height
}
}
}

View File

@ -4,8 +4,9 @@ import android.content.Context
import androidx.annotation.VisibleForTesting
import cash.z.ecc.android.sdk.exception.BirthdayException
import cash.z.ecc.android.sdk.internal.from
import cash.z.ecc.android.sdk.internal.model.Checkpoint
import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.type.WalletBirthday
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.type.ZcashNetwork
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@ -16,7 +17,7 @@ import java.util.*
/**
* Tool for loading checkpoints for the wallet, based on the height at which the wallet was born.
*/
object WalletBirthdayTool {
internal object CheckpointTool {
// Behavior change implemented as a fix for issue #270. Temporarily adding a boolean
// that allows the change to be rolled back quickly if needed, although long-term
@ -31,29 +32,29 @@ object WalletBirthdayTool {
suspend fun loadNearest(
context: Context,
network: ZcashNetwork,
birthdayHeight: Int? = null
): WalletBirthday {
birthdayHeight: BlockHeight?
): Checkpoint {
// TODO: potentially pull from shared preferences first
return loadBirthdayFromAssets(context, network, birthdayHeight)
return loadCheckpointFromAssets(context, network, birthdayHeight)
}
/**
* Useful for when an exact checkpoint is needed, like for SAPLING_ACTIVATION_HEIGHT. In
* most cases, loading the nearest checkpoint is preferred for privacy reasons.
*/
suspend fun loadExact(context: Context, network: ZcashNetwork, birthdayHeight: Int) =
loadNearest(context, network, birthdayHeight).also {
if (it.height != birthdayHeight) {
suspend fun loadExact(context: Context, network: ZcashNetwork, birthday: BlockHeight) =
loadNearest(context, network, birthday).also {
if (it.height != birthday) {
throw BirthdayException.ExactBirthdayNotFoundException(
birthdayHeight,
it.height
birthday,
it
)
}
}
// Converting this to suspending will then propagate
@Throws(IOException::class)
internal suspend fun listBirthdayDirectoryContents(context: Context, directory: String) =
internal suspend fun listCheckpointDirectoryContents(context: Context, directory: String) =
withContext(Dispatchers.IO) {
context.assets.list(directory)
}
@ -63,58 +64,64 @@ object WalletBirthdayTool {
* (i.e. sapling trees for a given height) can be found.
*/
@VisibleForTesting
internal fun birthdayDirectory(network: ZcashNetwork) =
"co.electriccoin.zcash/checkpoint/${(network.networkName as java.lang.String).toLowerCase(Locale.ROOT)}"
internal fun checkpointDirectory(network: ZcashNetwork) =
"co.electriccoin.zcash/checkpoint/${
(network.networkName as java.lang.String).toLowerCase(
Locale.ROOT
)
}"
internal fun birthdayHeight(fileName: String) = fileName.split('.').first().toInt()
internal fun checkpointHeightFromFilename(zcashNetwork: ZcashNetwork, fileName: String) =
BlockHeight.new(zcashNetwork, fileName.split('.').first().toLong())
private fun Array<String>.sortDescending() =
apply { sortByDescending { birthdayHeight(it) } }
private fun Array<String>.sortDescending(zcashNetwork: ZcashNetwork) =
apply { sortByDescending { checkpointHeightFromFilename(zcashNetwork, it).value } }
/**
* Load the given birthday file from the assets of the given context. When no height is
* specified, we default to the file with the greatest name.
*
* @param context the context from which to load assets.
* @param birthdayHeight the height file to look for among the file names.
* @param birthday the height file to look for among the file names.
*
* @return a WalletBirthday that reflects the contents of the file or an exception when
* parsing fails.
*/
private suspend fun loadBirthdayFromAssets(
private suspend fun loadCheckpointFromAssets(
context: Context,
network: ZcashNetwork,
birthdayHeight: Int? = null
): WalletBirthday {
twig("loading birthday from assets: $birthdayHeight")
val directory = birthdayDirectory(network)
val treeFiles = getFilteredFileNames(context, directory, birthdayHeight)
birthday: BlockHeight?
): Checkpoint {
twig("loading checkpoint from assets: $birthday")
val directory = checkpointDirectory(network)
val treeFiles = getFilteredFileNames(context, network, directory, birthday)
twig("found ${treeFiles.size} sapling tree checkpoints: $treeFiles")
return getFirstValidWalletBirthday(context, directory, treeFiles)
return getFirstValidWalletBirthday(context, network, directory, treeFiles)
}
private suspend fun getFilteredFileNames(
context: Context,
network: ZcashNetwork,
directory: String,
birthdayHeight: Int? = null
birthday: BlockHeight? = null
): List<String> {
val unfilteredTreeFiles = listBirthdayDirectoryContents(context, directory)
val unfilteredTreeFiles = listCheckpointDirectoryContents(context, directory)
if (unfilteredTreeFiles.isNullOrEmpty()) {
throw BirthdayException.MissingBirthdayFilesException(directory)
}
val filteredTreeFiles = unfilteredTreeFiles
.sortDescending()
.sortDescending(network)
.filter { filename ->
birthdayHeight?.let { birthdayHeight(filename) <= it } ?: true
birthday?.let { checkpointHeightFromFilename(network, filename) <= it } ?: true
}
if (filteredTreeFiles.isEmpty()) {
throw BirthdayException.BirthdayFileNotFoundException(
directory,
birthdayHeight
birthday
)
}
@ -127,9 +134,10 @@ object WalletBirthdayTool {
@VisibleForTesting
internal suspend fun getFirstValidWalletBirthday(
context: Context,
network: ZcashNetwork,
directory: String,
treeFiles: List<String>
): WalletBirthday {
): Checkpoint {
var lastException: Exception? = null
treeFiles.forEach { treefile ->
try {
@ -143,7 +151,7 @@ object WalletBirthdayTool {
}
}
return WalletBirthday.from(jsonString)
return Checkpoint.from(network, jsonString)
} catch (t: Throwable) {
val exception = BirthdayException.MalformattedBirthdayFilesException(
directory,

View File

@ -1,23 +1,5 @@
package cash.z.ecc.android.sdk.type
/**
* Model object for holding a wallet birthday.
*
* @param height the height at the time the wallet was born.
* @param hash the hash of the block at the height.
* @param time the block time at the height. Represented as seconds since the Unix epoch.
* @param tree the sapling tree corresponding to the height.
*/
data class WalletBirthday(
val height: Int = -1,
val hash: String = "",
val time: Long = -1,
// Note: this field does NOT match the name of the JSON, so will break with field-based JSON parsing
val tree: String = ""
) {
companion object
}
/**
* A grouping of keys that correspond to a single wallet account but do not have spend authority.
*
@ -41,18 +23,3 @@ interface UnifiedAddress {
val rawShieldedAddress: String
val rawTransparentAddress: String
}
enum class ZcashNetwork(
val id: Int,
val networkName: String,
val saplingActivationHeight: Int,
val defaultHost: String,
val defaultPort: Int
) {
Testnet(0, "testnet", 280_000, "testnet.lightwalletd.com", 9067),
Mainnet(1, "mainnet", 419_200, "mainnet.lightwalletd.com", 9067);
companion object {
fun from(id: Int) = values().first { it.id == id }
}
}

View File

@ -0,0 +1,18 @@
package cash.z.ecc.android.sdk.type
import cash.z.ecc.android.sdk.model.BlockHeight
enum class ZcashNetwork(
val id: Int,
val networkName: String,
val saplingActivationHeight: BlockHeight,
val defaultHost: String,
val defaultPort: Int
) {
Testnet(0, "testnet", BlockHeight(280_000), "testnet.lightwalletd.com", 9067),
Mainnet(1, "mainnet", BlockHeight(419_200), "mainnet.lightwalletd.com", 9067);
companion object {
fun from(id: Int) = values().first { it.id == id }
}
}

View File

@ -29,16 +29,16 @@ message DarksideBlocksURL {
// of hex-encoded transactions, one per line, that are to be associated
// with the given height (fake-mined into the block at that height)
message DarksideTransactionsURL {
int32 height = 1;
int64 height = 1;
string url = 2;
}
message DarksideHeight {
int32 height = 1;
int64 height = 1;
}
message DarksideEmptyBlocks {
int32 height = 1;
int64 height = 1;
int32 nonce = 2;
int32 count = 3;
}

View File

@ -367,7 +367,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_initBlocksT
env: JNIEnv<'_>,
_: JClass<'_>,
db_data: JString<'_>,
height: jint,
height: jlong,
hash_string: JString<'_>,
time: jlong,
sapling_tree_string: JString<'_>,
@ -390,7 +390,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_initBlocksT
hex::decode(utils::java_string_to_rust(&env, sapling_tree_string)).unwrap();
debug!("initializing blocks table with height {}", height);
match init_blocks_table(&db_data, height.try_into()?, hash, time, &sapling_tree) {
match init_blocks_table(&db_data, (height as u32).try_into()?, hash, time, &sapling_tree) {
Ok(()) => Ok(JNI_TRUE),
Err(e) => Err(format_err!("Error while initializing blocks table: {}", e)),
}
@ -673,7 +673,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_validateCom
db_cache: JString<'_>,
db_data: JString<'_>,
network_id: jint,
) -> jint {
) -> jlong {
let res = panic::catch_unwind(|| {
let network = parse_network(network_id as u32)?;
let block_db = block_db(&env, db_cache)?;
@ -689,7 +689,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_validateCom
match e {
SqliteClientError::BackendError(Error::InvalidChain(upper_bound, _)) => {
let upper_bound_u32 = u32::from(upper_bound);
Ok(upper_bound_u32 as i32)
Ok(upper_bound_u32 as i64)
}
_ => Err(format_err!("Error while validating chain: {}", e)),
}
@ -699,7 +699,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_validateCom
}
});
unwrap_exc_or(&env, res, 0)
unwrap_exc_or(&env, res, 0) as jlong
}
#[no_mangle]
@ -707,9 +707,9 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_getNearestR
env: JNIEnv<'_>,
_: JClass<'_>,
db_data: JString<'_>,
height: jint,
height: jlong,
network_id: jint,
) -> jint {
) -> jlong {
let res = panic::catch_unwind(|| {
if height < 100 {
Ok(height)
@ -720,11 +720,11 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_getNearestR
Ok(Some(best_height)) => {
let first_unspent_note_height = u32::from(best_height);
Ok(std::cmp::min(
first_unspent_note_height as i32,
height as i32,
first_unspent_note_height as i64,
height as i64,
))
}
Ok(None) => Ok(height as i32),
Ok(None) => Ok(height as i64),
Err(e) => Err(format_err!(
"Error while getting nearest rewind height for {}: {}",
height,
@ -734,7 +734,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_getNearestR
}
});
unwrap_exc_or(&env, res, -1)
unwrap_exc_or(&env, res, -1) as jlong
}
#[no_mangle]
@ -742,7 +742,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_rewindToHei
env: JNIEnv<'_>,
_: JClass<'_>,
db_data: JString<'_>,
height: jint,
height: jlong,
network_id: jint,
) -> jboolean {
let res = panic::catch_unwind(|| {
@ -830,7 +830,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_clearUtxos(
_: JClass<'_>,
db_data: JString<'_>,
taddress: JString<'_>,
above_height: jint,
above_height: jlong,
network_id: jint,
) -> jint {
let res = panic::catch_unwind(|| {
@ -1153,7 +1153,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_shieldToAdd
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_branchIdForHeight(
env: JNIEnv<'_>,
_: JClass<'_>,
height: jint,
height: jlong,
network_id: jint,
) -> jlong {
let res = panic::catch_unwind(|| {

View File

@ -0,0 +1,72 @@
package cash.z.ecc.android.sdk.model
import cash.z.ecc.android.sdk.type.ZcashNetwork
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
class BlockHeightTest {
@Test
fun new_mainnet_fails_below_sapling_activation_height() {
assertFailsWith(IllegalArgumentException::class) {
BlockHeight.new(
ZcashNetwork.Mainnet,
ZcashNetwork.Mainnet.saplingActivationHeight.value - 1
)
}
}
@Test
fun new_mainnet_succeeds_at_sapling_activation_height() {
BlockHeight.new(ZcashNetwork.Mainnet, ZcashNetwork.Mainnet.saplingActivationHeight.value)
}
@Test
fun new_mainnet_succeeds_above_sapling_activation_height() {
BlockHeight.new(ZcashNetwork.Mainnet, ZcashNetwork.Mainnet.saplingActivationHeight.value + 10_000)
}
@Test
fun new_mainnet_succeeds_at_max_value() {
BlockHeight.new(ZcashNetwork.Mainnet, UInt.MAX_VALUE.toLong())
}
@Test
fun new_fails_above_max_value() {
assertFailsWith(IllegalArgumentException::class) {
BlockHeight.new(ZcashNetwork.Mainnet, UInt.MAX_VALUE.toLong() + 1)
}
}
@Test
fun addition_of_blockheight_succeeds() {
val one = BlockHeight.new(ZcashNetwork.Mainnet, ZcashNetwork.Mainnet.saplingActivationHeight.value)
val two = BlockHeight.new(ZcashNetwork.Mainnet, ZcashNetwork.Mainnet.saplingActivationHeight.value + 123)
assertEquals(838523L, (one + two).value)
}
@Test
fun addition_of_int_succeeds() {
assertEquals(419323L, (ZcashNetwork.Mainnet.saplingActivationHeight + 123).value)
}
@Test
fun addition_of_long_succeeds() {
assertEquals(419323L, (ZcashNetwork.Mainnet.saplingActivationHeight + 123L).value)
}
@Test
fun subtraction_of_int_fails() {
assertFailsWith<IllegalArgumentException> {
ZcashNetwork.Mainnet.saplingActivationHeight + -1
}
}
@Test
fun subtraction_of_long_fails() {
assertFailsWith<IllegalArgumentException> {
ZcashNetwork.Mainnet.saplingActivationHeight + -1L
}
}
}

View File

@ -2,6 +2,7 @@
<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>
@ -12,6 +13,8 @@
<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>
@ -39,25 +42,24 @@
<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:WalletBirthdayTool.kt$WalletBirthdayTool$// TODO: If we ever add crash analytics hooks, this would be something to report</ID>
<ID>ForbiddenComment:WalletBirthdayTool.kt$WalletBirthdayTool$// TODO: potentially pull from shared preferences first</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>LargeClass:CompactBlockProcessor.kt$CompactBlockProcessor</ID>
<ID>LongMethod:SdkSynchronizer.kt$SdkSynchronizer$private suspend fun refreshPendingTransactions()</ID>
<ID>LongParameterList:Initializer.kt$Initializer$( val context: Context, val rustBackend: RustBackend, val network: ZcashNetwork, val alias: String, val host: String, val port: Int, val viewingKeys: List&lt;UnifiedViewingKey>, val overwriteVks: Boolean, val birthday: WalletBirthday )</ID>
<ID>LongParameterList:Initializer.kt$Initializer.Config$( seed: ByteArray, birthdayHeight: Int? = null, network: ZcashNetwork, host: String = network.defaultHost, port: Int = network.defaultPort, alias: String = ZcashSdk.DEFAULT_ALIAS )</ID>
<ID>LongParameterList:Initializer.kt$Initializer.Config$( viewingKey: UnifiedViewingKey, birthdayHeight: Int? = null, network: ZcashNetwork, host: String = network.defaultHost, port: Int = network.defaultPort, alias: String = ZcashSdk.DEFAULT_ALIAS )</ID>
<ID>LongParameterList:PagedTransactionRepository.kt$PagedTransactionRepository.Companion$( appContext: Context, pageSize: Int = 10, rustBackend: RustBackend, birthday: WalletBirthday, viewingKeys: List&lt;UnifiedViewingKey>, overwriteVks: Boolean = false )</ID>
<ID>LongParameterList:Initializer.kt$Initializer$( val context: Context, internal val rustBackend: RustBackend, val network: ZcashNetwork, val alias: String, val host: String, val port: Int, val viewingKeys: List&lt;UnifiedViewingKey>, val overwriteVks: Boolean, internal val checkpoint: Checkpoint )</ID>
<ID>LongParameterList:Initializer.kt$Initializer.Config$( seed: ByteArray, birthday: BlockHeight?, network: ZcashNetwork, host: String = network.defaultHost, port: Int = network.defaultPort, alias: String = ZcashSdk.DEFAULT_ALIAS )</ID>
<ID>LongParameterList:Initializer.kt$Initializer.Config$( viewingKey: UnifiedViewingKey, birthday: BlockHeight?, network: ZcashNetwork, host: String = network.defaultHost, port: Int = network.defaultPort, alias: String = ZcashSdk.DEFAULT_ALIAS )</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: Int, 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: Int, 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: Int )</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>
@ -114,10 +116,10 @@
<ID>MagicNumber:UtxoViewHolder.kt$UtxoViewHolder$1000L</ID>
<ID>MagicNumber:WalletService.kt$1000L</ID>
<ID>MagicNumber:WalletService.kt$4.0</ID>
<ID>MagicNumber:WalletTypes.kt$ZcashNetwork.Mainnet$419_200</ID>
<ID>MagicNumber:WalletTypes.kt$ZcashNetwork.Mainnet$9067</ID>
<ID>MagicNumber:WalletTypes.kt$ZcashNetwork.Testnet$280_000</ID>
<ID>MagicNumber:WalletTypes.kt$ZcashNetwork.Testnet$9067</ID>
<ID>MagicNumber:ZcashNetwork.kt$ZcashNetwork.Mainnet$419_200</ID>
<ID>MagicNumber:ZcashNetwork.kt$ZcashNetwork.Mainnet$9067</ID>
<ID>MagicNumber:ZcashNetwork.kt$ZcashNetwork.Testnet$280_000</ID>
<ID>MagicNumber:ZcashNetwork.kt$ZcashNetwork.Testnet$9067</ID>
<ID>MagicNumber:ZcashSdk.kt$ZcashSdk$10</ID>
<ID>MagicNumber:ZcashSdk.kt$ZcashSdk$100</ID>
<ID>MagicNumber:ZcashSdk.kt$ZcashSdk$150</ID>
@ -132,20 +134,27 @@
<ID>MagicNumber:build.gradle.kts$21</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:ChangeServiceTest.kt$ChangeServiceTest$"Exception was of the wrong type. Expected ${ChainInfoNotMatching::class.simpleName} but was ${caughtException!!::class.simpleName}"</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$!info.matchingConsensusBranchId(clientBranch) -> MismatchedBranch(clientBranch = clientBranch, serverBranch = info.consensusBranchId, networkName = network)</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$!info.matchingNetwork(network) -> MismatchedNetwork(clientNetwork = network, serverNetwork = info.chainName)</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:CompactBlockDbStore.kt$CompactBlockDbStore$override suspend fun getLatestHeight(): BlockHeight</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$internal suspend</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$oldestTransactionHeight = repository.receivedTransactions.first().lastOrNull()?.minedHeight ?: lowerBoundHeight</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$lastDownloadRange</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 == ERROR_CODE_FAILED_ENHANCE) " (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("$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>
@ -153,18 +162,12 @@
<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.first}..${range.last}")</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.last} | ${metrics.batchTime}ms, ${metrics.batchItems}blks, ${metrics.batchIps.format()}bps")</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$twig("block: $height\thash=${hash?.toHexReversed()} \tprevHash=${block?.prevHash?.toByteArray()?.toHexReversed()}")</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:CompactBlockProcessor.kt$CompactBlockProcessor$val end = min((range.first + (i * DOWNLOAD_BATCH_SIZE)) - 1, range.last) // subtract 1 on the first value because the range is inclusive</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$val errorMessage = "ERROR: unable to resolve reorg at height $result after ${consecutiveChainErrors.get()} correction attempts!"</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$val lastDownloadedHeight = downloader.getLastDownloadedHeight().takeUnless { it &lt; network.saplingActivationHeight } ?: -1</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$val originalCheckpoint = lowerBoundHeight + MAX_REORG_SIZE + 2 // add one because we already have the checkpoint. Add one again because we delete ABOVE the block</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$val percentValue = (lastScannedHeight - range.first) / (range.last - range.first + 1).toFloat() * 100.0f</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$val summary = if (noWorkDone) "Nothing to process: no new blocks to download or scan" else "Done processing blocks"</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$onNext(it.newBuilderForType().setData(it.data).setHeight(tipHeight.toLong()).build())</ID>
<ID>MaxLineLength:DarksideApi.kt$DarksideApi$twig("resetting darksidewalletd with saplingActivation=$saplingActivationHeight branchId=$branchId chainName=$chainName")</ID>
@ -186,6 +189,7 @@
<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>
@ -203,6 +207,9 @@
<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>
@ -232,9 +239,12 @@
<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:LightWalletGrpcService.kt$LightWalletGrpcService$twig("getting channel isShutdown: ${channel.isShutdown} isTerminated: ${channel.isTerminated} getState: $state stateCount: $stateCount", -1)</ID>
<ID>MaxLineLength:LightWalletService.kt$LightWalletService$fun</ID>
<ID>MaxLineLength:ListUtxosFragment.kt$ListUtxosFragment$binding.inputAddress.setText(DerivationTool.deriveTransparentAddress(seed, ZcashNetwork.fromResources(requireApplicationContext())))</ID>
<ID>MaxLineLength:ListUtxosFragment.kt$ListUtxosFragment$binding.inputRangeStart.setText(ZcashNetwork.fromResources(requireApplicationContext()).saplingActivationHeight.toString())</ID>
<ID>MaxLineLength:ListUtxosFragment.kt$ListUtxosFragment$val startToUse = binding.inputRangeStart.text.toString().toIntOrNull() ?: ZcashNetwork.fromResources(requireApplicationContext()).saplingActivationHeight</ID>
<ID>MaxLineLength:ListUtxosFragment.kt$ListUtxosFragment$val endToUse = binding.inputRangeEnd.text.toString().toLongOrNull() ?: DemoConstants.getUxtoEndHeight(requireApplicationContext()).value</ID>
<ID>MaxLineLength:ListUtxosFragment.kt$ListUtxosFragment$val startToUse = max(binding.inputRangeStart.text.toString().toLongOrNull() ?: network.saplingActivationHeight.value, network.saplingActivationHeight.value)</ID>
<ID>MaxLineLength:ListUtxosFragment.kt$ListUtxosFragment$val txids = lightwalletService?.getTAddressTransactions(addressToUse, BlockHeight.new(network, startToUse)..BlockHeight.new(network, endToUse))</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>
@ -275,6 +285,7 @@
<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$"$info\n ${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$"is using plaintext. This will cause problems for the test. Ensure that the `lightwalletd_allow_very_insecure_connections` resource value is false"</ID>
<ID>MaxLineLength:SanityTest.kt$SanityTest$assertTrue("$networkName failed to return a proper block. Height was ${block.height} but we expected $height", block.height.toInt() == height)</ID>
@ -322,7 +333,7 @@
<ID>MaxLineLength:TestWallet.kt$TestWallet.Backups.DEFAULT$DEFAULT</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: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 &lt; saplingActivationHeight || expiryHeight &lt; saplingActivationHeight) return false</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>
@ -332,7 +343,6 @@
<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:WalletBirthdayTool.kt$WalletBirthdayTool$* @param treeFiles A list of files, sorted in descending order based on `int` value of the first part of the filename.</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>
@ -346,7 +356,6 @@
<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:DemoConstants.kt$DemoConstants$val utxoEndHeight: Int = 968085</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>
@ -379,6 +388,7 @@
<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>
@ -395,7 +405,6 @@
<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:WalletBirthdayTool.kt$WalletBirthdayTool$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>