[#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:
parent
f29ffa1895
commit
9b666833b1
|
@ -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()
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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)),
|
||||
;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()}")
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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()))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 ->
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)),
|
||||
;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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" +
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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?
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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?)
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}")
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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(|| {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@
|
|||
<SmellBaseline>
|
||||
<ManuallySuppressedIssues/>
|
||||
<CurrentIssues>
|
||||
<ID>ComplexCondition:CompactBlockProcessor.kt$CompactBlockProcessor$(null == lastScannedHeight && targetHeight < lastLocalBlock) || (null != lastScannedHeight && targetHeight < 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 <= or <</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<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<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<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<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 && 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 < 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 < saplingActivationHeight || expiryHeight < saplingActivationHeight) return false</ID>
|
||||
<ID>MaxLineLength:Transactions.kt$if (latestHeight == null || latestHeight.value < saplingActivationHeight.value || expiryHeight < 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>
|
||||
|
|
Loading…
Reference in New Issue