v1.9.0-beta01 changes
This commit includes HUSH specific changes starting at v.1.9.0-beta01 release here: https://github.com/zcash/zcash-android-wallet-sdk/releases/tag/v1.9.0-beta01
This commit is contained in:
@@ -3,8 +3,8 @@ 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.type.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.tool.CheckpointTool
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.json.JSONObject
|
||||
import org.junit.Assert.assertEquals
|
||||
@@ -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 ->
|
||||
@@ -77,12 +77,13 @@ class AssetTest {
|
||||
val expectedNetworkName = when (network) {
|
||||
ZcashNetwork.Mainnet -> "main"
|
||||
ZcashNetwork.Testnet -> "test"
|
||||
else -> IllegalArgumentException("Unsupported network $network")
|
||||
}
|
||||
assertEquals("File: ${it.filename}", expectedNetworkName, jsonObject.getString("network"))
|
||||
|
||||
assertEquals(
|
||||
"File: ${it.filename}",
|
||||
WalletBirthdayTool.birthdayHeight(it.filename),
|
||||
CheckpointTool.checkpointHeightFromFilename(network, it.filename),
|
||||
jsonObject.getInt("height")
|
||||
)
|
||||
|
||||
@@ -94,9 +95,9 @@ class AssetTest {
|
||||
|
||||
companion object {
|
||||
fun listAssets(network: ZcashNetwork) = runBlocking {
|
||||
WalletBirthdayTool.listBirthdayDirectoryContents(
|
||||
CheckpointTool.listCheckpointDirectoryContents(
|
||||
ApplicationProvider.getApplicationContext<Context>(),
|
||||
WalletBirthdayTool.birthdayDirectory(network)
|
||||
CheckpointTool.checkpointDirectory(network)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package cash.z.ecc.android.sdk.ext
|
||||
|
||||
import cash.z.ecc.android.sdk.Initializer
|
||||
import cash.z.ecc.android.sdk.type.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.util.SimpleMnemonics
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okhttp3.OkHttpClient
|
||||
@@ -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,11 +3,11 @@ 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.type.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.util.TestWallet
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
@@ -30,13 +30,6 @@ class SanityTest(
|
||||
val networkName = wallet.networkName
|
||||
val name = "$networkName wallet"
|
||||
|
||||
@Test
|
||||
fun testNotPlaintext() {
|
||||
val message =
|
||||
"is using plaintext. This will cause problems for the test. Ensure that the `lightwalletd_allow_very_insecure_connections` resource value is false"
|
||||
assertFalse("$name $message", wallet.service.connectionInfo.usePlaintext)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testFilePaths() {
|
||||
assertEquals(
|
||||
@@ -61,7 +54,7 @@ class SanityTest(
|
||||
assertEquals(
|
||||
"$name has invalid birthday height",
|
||||
birthday,
|
||||
wallet.initializer.birthday.height
|
||||
wallet.initializer.checkpoint.height
|
||||
)
|
||||
}
|
||||
|
||||
@@ -79,25 +72,15 @@ class SanityTest(
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testServerConnection() {
|
||||
assertEquals(
|
||||
"$name has an invalid server connection",
|
||||
"$networkName.lite.hushpool.is:9067?usePlaintext=true",
|
||||
wallet.connectionInfo
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLatestHeight() = runBlocking {
|
||||
if (wallet.networkName == "mainnet") {
|
||||
val expectedHeight = BlockExplorer.fetchLatestHeight()
|
||||
// fetch height directly because the synchronizer hasn't started, yet
|
||||
val downloaderHeight = wallet.service.getLatestBlockHeight()
|
||||
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
|
||||
"${wallet.endpoint} ${wallet.networkName} Lightwalletd is too far behind. Downloader height $downloaderHeight is more than 10 blocks behind block explorer height $expectedHeight",
|
||||
expectedHeight - 10 < downloaderHeight.value
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -105,9 +88,9 @@ class SanityTest(
|
||||
@Test
|
||||
fun testSingleBlockDownload() = runBlocking {
|
||||
// fetch block directly because the synchronizer hasn't started, yet
|
||||
val height = 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)
|
||||
val height = BlockHeight.new(wallet.network, 1_000_000)
|
||||
val block = wallet.service.getBlockRange(height..height).first()
|
||||
assertTrue("$networkName failed to return a proper block. Height was ${block.height} but we expected $height", block.height == height.value)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -4,7 +4,6 @@ import androidx.test.filters.LargeTest
|
||||
import androidx.test.filters.MediumTest
|
||||
import cash.z.ecc.android.sdk.annotation.MaintainedTest
|
||||
import cash.z.ecc.android.sdk.annotation.TestPurpose
|
||||
import cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService
|
||||
import cash.z.ecc.android.sdk.util.TestWallet
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Assert
|
||||
@@ -19,16 +18,6 @@ import org.junit.Test
|
||||
@MediumTest
|
||||
class SmokeTest {
|
||||
|
||||
@Test
|
||||
fun testNotPlaintext() {
|
||||
val service =
|
||||
wallet.synchronizer.processor.downloader.lightWalletService as LightWalletGrpcService
|
||||
Assert.assertFalse(
|
||||
"Wallet is using plaintext. This will cause problems for the test. Ensure that the `lightwalletd_allow_very_insecure_connections` resource value is false",
|
||||
service.connectionInfo.usePlaintext
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testFilePaths() {
|
||||
Assert.assertEquals("Invalid DataDB file", "/data/user/0/cash.z.ecc.android.sdk.test/databases/TestWallet_testnet_Data.db", wallet.initializer.rustBackend.pathDataDb)
|
||||
@@ -38,7 +27,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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package cash.z.wallet.sdk.integration
|
||||
package cash.z.ecc.android.sdk.integration
|
||||
|
||||
import androidx.test.filters.LargeTest
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
@@ -12,11 +12,13 @@ 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.LightWalletEndpoint
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
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
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
@@ -38,9 +40,9 @@ class TestnetIntegrationTest : ScopedTest() {
|
||||
@Test
|
||||
@Ignore("This test is broken")
|
||||
fun testLatestBlockTest() {
|
||||
val service = LightWalletGrpcService(
|
||||
val service = LightWalletGrpcService.new(
|
||||
context,
|
||||
host
|
||||
lightWalletEndpoint
|
||||
)
|
||||
val height = service.getLatestBlockHeight()
|
||||
assertTrue(height > saplingActivation)
|
||||
@@ -49,7 +51,7 @@ class TestnetIntegrationTest : ScopedTest() {
|
||||
@Test
|
||||
fun testLoadBirthday() {
|
||||
val (height, hash, time, tree) = runBlocking {
|
||||
WalletBirthdayTool.loadNearest(
|
||||
CheckpointTool.loadNearest(
|
||||
context,
|
||||
synchronizer.network,
|
||||
saplingActivation + 1
|
||||
@@ -117,8 +119,8 @@ class TestnetIntegrationTest : ScopedTest() {
|
||||
companion object {
|
||||
init { Twig.plant(TroubleshootingTwig()) }
|
||||
|
||||
const val host = "lightwalletd.testnet.z.cash"
|
||||
private const val birthdayHeight = 963150
|
||||
val lightWalletEndpoint = LightWalletEndpoint("lightwalletd.testnet.z.cash", 9087, true)
|
||||
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()
|
||||
@@ -128,8 +130,8 @@ class TestnetIntegrationTest : ScopedTest() {
|
||||
private val context = InstrumentationRegistry.getInstrumentation().context
|
||||
private val initializer = runBlocking {
|
||||
Initializer.new(context) { config ->
|
||||
config.setNetwork(ZcashNetwork.Testnet, host)
|
||||
runBlocking { config.importWallet(seed, birthdayHeight, ZcashNetwork.Testnet) }
|
||||
config.setNetwork(ZcashNetwork.Testnet, lightWalletEndpoint)
|
||||
runBlocking { config.importWallet(seed, BlockHeight.new(ZcashNetwork.Testnet, birthdayHeight), ZcashNetwork.Testnet, lightWalletEndpoint) }
|
||||
}
|
||||
}
|
||||
private lateinit var synchronizer: Synchronizer
|
||||
|
||||
@@ -11,8 +11,12 @@ 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.model.LightWalletEndpoint
|
||||
import cash.z.ecc.android.sdk.model.Mainnet
|
||||
import cash.z.ecc.android.sdk.model.Testnet
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.test.ScopedTest
|
||||
import cash.z.ecc.android.sdk.type.ZcashNetwork
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
@@ -34,13 +38,15 @@ import org.mockito.Spy
|
||||
class ChangeServiceTest : ScopedTest() {
|
||||
|
||||
val network = ZcashNetwork.Mainnet
|
||||
val lightWalletEndpoint = LightWalletEndpoint.Mainnet
|
||||
private val eccEndpoint = LightWalletEndpoint("lightwalletd.electriccoin.co", 9087, true)
|
||||
|
||||
@Mock
|
||||
lateinit var mockBlockStore: CompactBlockStore
|
||||
var mockCloseable: AutoCloseable? = null
|
||||
|
||||
@Spy
|
||||
val service = LightWalletGrpcService(context, network)
|
||||
val service = LightWalletGrpcService.new(context, lightWalletEndpoint)
|
||||
|
||||
lateinit var downloader: CompactBlockDownloader
|
||||
lateinit var otherService: LightWalletService
|
||||
@@ -49,7 +55,7 @@ class ChangeServiceTest : ScopedTest() {
|
||||
fun setup() {
|
||||
initMocks()
|
||||
downloader = CompactBlockDownloader(service, mockBlockStore)
|
||||
otherService = LightWalletGrpcService(context, "lightwalletd.electriccoin.co")
|
||||
otherService = LightWalletGrpcService.new(context, eccEndpoint)
|
||||
}
|
||||
|
||||
@After
|
||||
@@ -70,7 +76,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 +87,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
|
||||
@@ -105,7 +111,7 @@ class ChangeServiceTest : ScopedTest() {
|
||||
@Test
|
||||
fun testSwitchToInvalidServer() = runBlocking {
|
||||
var caughtException: Throwable? = null
|
||||
downloader.changeService(LightWalletGrpcService(context, "invalid.lightwalletd")) {
|
||||
downloader.changeService(LightWalletGrpcService.new(context, LightWalletEndpoint("invalid.lightwalletd", 9087, true))) {
|
||||
caughtException = it
|
||||
}
|
||||
assertNotNull("Using an invalid host should generate an exception.", caughtException)
|
||||
@@ -118,7 +124,7 @@ class ChangeServiceTest : ScopedTest() {
|
||||
@Test
|
||||
fun testSwitchToTestnetFails() = runBlocking {
|
||||
var caughtException: Throwable? = null
|
||||
downloader.changeService(LightWalletGrpcService(context, ZcashNetwork.Testnet)) {
|
||||
downloader.changeService(LightWalletGrpcService.new(context, LightWalletEndpoint.Testnet)) {
|
||||
caughtException = it
|
||||
}
|
||||
assertNotNull("Using an invalid host should generate an exception.", caughtException)
|
||||
|
||||
@@ -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,7 +2,8 @@ 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.type.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
@@ -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,14 +45,14 @@ 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),
|
||||
arrayOf("Sapling", 1, 1991772603L, "76b809bb", mainnetBackend),
|
||||
|
||||
// Testnet Cases
|
||||
arrayOf("Sapling", 280_000, 1991772603L, "76b809bb", testnetBackend)
|
||||
arrayOf("Sapling", 1, 1991772603L, "76b809bb", testnetBackend),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ import cash.z.ecc.android.sdk.annotation.MaintainedTest
|
||||
import cash.z.ecc.android.sdk.annotation.TestPurpose
|
||||
import cash.z.ecc.android.sdk.internal.TroubleshootingTwig
|
||||
import cash.z.ecc.android.sdk.internal.Twig
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.tool.DerivationTool
|
||||
import cash.z.ecc.android.sdk.type.ZcashNetwork
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.BeforeClass
|
||||
|
||||
@@ -2,7 +2,7 @@ package cash.z.ecc.android.sdk.sample
|
||||
|
||||
import cash.z.ecc.android.sdk.internal.Twig
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import cash.z.ecc.android.sdk.type.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.util.TestWallet
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Assert
|
||||
|
||||
@@ -3,8 +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.Testnet
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.util.TestWallet
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.runBlocking
|
||||
@@ -73,7 +74,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 +86,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",
|
||||
ZcashNetwork.Testnet,
|
||||
startHeight = BlockHeight.new(
|
||||
ZcashNetwork.Testnet,
|
||||
1330190
|
||||
)
|
||||
)
|
||||
// val wallet1 = SimpleWallet(WALLET0_PHRASE, "Wallet1")
|
||||
|
||||
wallet0.sync() // .shieldFunds()
|
||||
@@ -107,7 +116,15 @@ 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",
|
||||
ZcashNetwork.Testnet,
|
||||
startHeight = BlockHeight.new(
|
||||
ZcashNetwork.Testnet,
|
||||
1330190
|
||||
)
|
||||
)
|
||||
// val job = walletA.walletScope.launch {
|
||||
// twig("Syncing WalletA")
|
||||
// walletA.sync()
|
||||
@@ -125,7 +142,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.model.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.tool.CheckpointTool.IS_FALLBACK_ON_FAILURE
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
package cash.z.ecc.android.sdk.util
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.tool.DerivationTool
|
||||
import cash.z.ecc.android.sdk.type.ZcashNetwork
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
@@ -6,10 +6,13 @@ 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.type.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.model.defaultForNetwork
|
||||
import cash.z.ecc.android.sdk.tool.CheckpointTool
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
@@ -30,7 +33,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 +49,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 {
|
||||
@@ -81,8 +84,8 @@ class BalancePrinterUtil {
|
||||
}.collect { seed ->
|
||||
// TODO: clear the dataDb but leave the cacheDb
|
||||
val initializer = Initializer.new(context) { config ->
|
||||
runBlocking { config.importWallet(seed, birthdayHeight, network) }
|
||||
config.setNetwork(network)
|
||||
val endpoint = LightWalletEndpoint.defaultForNetwork(network)
|
||||
runBlocking { config.importWallet(seed, birthdayHeight, network, endpoint) }
|
||||
config.alias = alias
|
||||
}
|
||||
/*
|
||||
|
||||
@@ -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.model.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,11 @@ class DataDbScannerUtil {
|
||||
val initializer = runBlocking {
|
||||
Initializer.new(context) {
|
||||
it.setBirthdayHeight(
|
||||
birthdayHeight
|
||||
BlockHeight.new(
|
||||
ZcashNetwork.Mainnet,
|
||||
birthdayHeight
|
||||
),
|
||||
false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,10 +10,13 @@ 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.LightWalletEndpoint
|
||||
import cash.z.ecc.android.sdk.model.Testnet
|
||||
import cash.z.ecc.android.sdk.model.WalletBalance
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.tool.DerivationTool
|
||||
import cash.z.ecc.android.sdk.type.WalletBalance
|
||||
import cash.z.ecc.android.sdk.type.ZcashNetwork
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -35,9 +38,8 @@ class TestWallet(
|
||||
val seedPhrase: String,
|
||||
val alias: String = "TestWallet",
|
||||
val network: ZcashNetwork = ZcashNetwork.Testnet,
|
||||
val host: String = network.defaultHost,
|
||||
startHeight: Int? = null,
|
||||
val port: Int = network.defaultPort
|
||||
val endpoint: LightWalletEndpoint = LightWalletEndpoint.Testnet,
|
||||
startHeight: BlockHeight? = null
|
||||
) {
|
||||
constructor(
|
||||
backup: Backups,
|
||||
@@ -65,7 +67,7 @@ class TestWallet(
|
||||
runBlocking { DerivationTool.deriveTransparentSecretKey(seed, network = network) }
|
||||
val initializer = runBlocking {
|
||||
Initializer.new(context) { config ->
|
||||
runBlocking { config.importWallet(seed, startHeight, network, host, alias = alias) }
|
||||
runBlocking { config.importWallet(seed, startHeight, network, endpoint, alias = alias) }
|
||||
}
|
||||
}
|
||||
val synchronizer: SdkSynchronizer = Synchronizer.newBlocking(initializer) as SdkSynchronizer
|
||||
@@ -78,14 +80,11 @@ class TestWallet(
|
||||
runBlocking { DerivationTool.deriveTransparentAddress(seed, network = network) }
|
||||
val birthdayHeight get() = synchronizer.latestBirthdayHeight
|
||||
val networkName get() = synchronizer.network.networkName
|
||||
val connectionInfo get() = service.connectionInfo.toString()
|
||||
|
||||
/* NOT SUPPORTED IN HUSH LIGHTWALLETD
|
||||
suspend fun transparentBalance(): WalletBalance {
|
||||
synchronizer.refreshUtxos(transparentAddress, synchronizer.latestBirthdayHeight)
|
||||
return synchronizer.getTransparentBalance(transparentAddress)
|
||||
}
|
||||
*/
|
||||
|
||||
suspend fun sync(timeout: Long = -1): TestWallet {
|
||||
val killSwitch = walletScope.launch {
|
||||
@@ -111,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")
|
||||
}
|
||||
@@ -119,15 +118,14 @@ class TestWallet(
|
||||
return this
|
||||
}
|
||||
|
||||
suspend fun rewindToHeight(height: Int): TestWallet {
|
||||
suspend fun rewindToHeight(height: BlockHeight): TestWallet {
|
||||
synchronizer.rewindToNearestHeight(height, false)
|
||||
return this
|
||||
}
|
||||
|
||||
/* NOT SUPPORTED IN HUSH LIGHTWALLETD
|
||||
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")
|
||||
}
|
||||
|
||||
@@ -144,7 +142,6 @@ class TestWallet(
|
||||
|
||||
return this
|
||||
}
|
||||
*/
|
||||
|
||||
suspend fun join(timeout: Long? = null): TestWallet {
|
||||
// block until stopped
|
||||
@@ -167,13 +164,48 @@ 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,7 +5,10 @@ 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.type.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
|
||||
import cash.z.ecc.android.sdk.model.Mainnet
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
|
||||
@@ -13,7 +16,7 @@ class TransactionCounterUtil {
|
||||
|
||||
private val network = ZcashNetwork.Mainnet
|
||||
private val context = InstrumentationRegistry.getInstrumentation().context
|
||||
private val service = LightWalletGrpcService(context, network)
|
||||
private val service = LightWalletGrpcService.new(context, LightWalletEndpoint.Mainnet)
|
||||
|
||||
init {
|
||||
Twig.plant(TroubleshootingTwig())
|
||||
@@ -23,7 +26,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 +46,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.model.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()
|
||||
@@ -3,5 +3,5 @@
|
||||
"height": "1150000",
|
||||
"hash": "0000000650e627bd7da6868f14070aff8fdbd31ef7125fe77851976ed3adfc54",
|
||||
"time": 1668316308,
|
||||
"saplingtree": "012e058162e4e6bc9553c413134b66e5e89cd63c330fc557060878c623fdedc63d01533dbef6ad6b226c7138bef1e1961ca170510a91fdb1ff5972e1cd79c347401c150168979af907639a39b427d83d4602fd22867faffb0942cbb11955dac680aab85901d8d396d94feb0cc78cbec422c6fd09834c4031a1ffdcbef04178827add5193160102e9b3fecaf39d40e5e63ef3253d1ac5aee4141bdf26763de74033d6016f5955019f59fdd4a570ad22980428caea7b5fa61f55b52e2e6fbb29600eee53d31ed35f017c5ad297c1e83430ce9d3768fd38e74bd06757b67e2917b34d0f3f763803f32600000000015c1016fb7c68d85099ce8423d6446c2ea3d77a63b5ab6044f6eb0c024ddb0d5a01627b5eae7588998ef2645fe8be1ec3227d560828956b7df00632b26784c4f80a0000012d8bdb15bce00ab0c8bd332355a100d9db356ac05fb97412b479214dcefea331000001a5e10b312a666ed313eb0db76bfc977430ec6b463f944816c43cd82d42181d1f000001708c9850eb440b259f233187662c5228804cb4500263949301b6fac8f6428f2301d6f84c424acdb1d10f8cef641662e0f63f954f07fe6199d504a61979c9ba3e13"
|
||||
}
|
||||
"saplingTree": "012e058162e4e6bc9553c413134b66e5e89cd63c330fc557060878c623fdedc63d01533dbef6ad6b226c7138bef1e1961ca170510a91fdb1ff5972e1cd79c347401c150168979af907639a39b427d83d4602fd22867faffb0942cbb11955dac680aab85901d8d396d94feb0cc78cbec422c6fd09834c4031a1ffdcbef04178827add5193160102e9b3fecaf39d40e5e63ef3253d1ac5aee4141bdf26763de74033d6016f5955019f59fdd4a570ad22980428caea7b5fa61f55b52e2e6fbb29600eee53d31ed35f017c5ad297c1e83430ce9d3768fd38e74bd06757b67e2917b34d0f3f763803f32600000000015c1016fb7c68d85099ce8423d6446c2ea3d77a63b5ab6044f6eb0c024ddb0d5a01627b5eae7588998ef2645fe8be1ec3227d560828956b7df00632b26784c4f80a0000012d8bdb15bce00ab0c8bd332355a100d9db356ac05fb97412b479214dcefea331000001a5e10b312a666ed313eb0db76bfc977430ec6b463f944816c43cd82d42181d1f000001708c9850eb440b259f233187662c5228804cb4500263949301b6fac8f6428f2301d6f84c424acdb1d10f8cef641662e0f63f954f07fe6199d504a61979c9ba3e13"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"network": "main",
|
||||
"height": "1160000",
|
||||
"hash": "000000006904fea1620eb53ad7f912197881e3d47a8a6683d943efc0ff43c94e",
|
||||
"time": 1669068992,
|
||||
"saplingTree": "01569b6b693b7fc56715b01e81a3d07dcfd723f54dc8f31cdb465509f39304755800150001dcf14961a27da1444097a9618a6a3d4a6a198b4afc8c9c2960e30da42c5790210001f95f7e52338af7b69ae511d4ae5c1fbaa394380e92fbb51cddfdc4b8b0cfef0601cd00c0bca2308a408b94327da9505d55a241aa05ae730884ee38c8d553d218290000000001d637d869f0247b4dc198156dee890fa12f44ad83c1ecd9f73bb89ab578ff31260001cebd13b9f31ab7c442c5a89562817d5b590b51ac75d735041d2389ceabd3cf7101e3a962887edb0c646e6390f87eb64ab4b12753338b8b72f3afcbca0b499b1f3a01447d8106fe66cea724f27e0f0310821d7e5c536b02f4540ce14f6359ec30650a014431c3e5e81d9dec6f8c51f83ad971de208c6a7d990f727f2b203af910f760410001a5e10b312a666ed313eb0db76bfc977430ec6b463f944816c43cd82d42181d1f000001708c9850eb440b259f233187662c5228804cb4500263949301b6fac8f6428f2301d6f84c424acdb1d10f8cef641662e0f63f954f07fe6199d504a61979c9ba3e13"
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"network": "test",
|
||||
"height": "1000000",
|
||||
"hash": "00000002e920ff85f57a164e0637d12147e95428b4da8ca2f0dde4192f5df829",
|
||||
"time": 1657012340,
|
||||
"saplingTree": "014e4c1dba2b623864148a9dc4828fc67ee7f231907425be3695949749092e845b01ebe665e0198710c608da05a8a7964700642cbfd9b7bcac005ed967df32357e6a140001688ee9449b2d45afe7fb411f19c58f56269a43967fc1d1fdb9fd28edc2344016018746e37c9fd6c64662e26f2e146427a69ed3800280a9124c4d8c19f25a2aa155011b980b27687b29f85d315c87db08ea5559cc812159a838240b45bd88fd545059000171bc61ad6eacc00f6252a2898470e6a6391311db651b443e74f1f506e35ae61b011ca124f40831dd203b8d4eb8386f58b593d503168df44aa2eed6f4e3fdacd51000000001216607336029bd6f322d3decb8dd7ae1f8df1760b240030f1879b7aa3b4c646e0001f529692b1ee5845a6e0681b2efcc66342586397c79b09cdefa957aa1b0e614310156e2babf0dca08c8b1991c00a5d74d740e7d0c4b95099065016719e93455833a018d57c3859e298989e2eae8e1d8f9135944eef930e3ad20330e0de0541aacc94801f2cec17739de7e1476938f895b1a6381b36ec44ccdbbac2eeb60be43be6f815e01a8d81d60d7de99c1e45988bc29029102ab653c13b490ee0133dc739bf63a971601437aa93f8bebde50ea70fd8c7b60fe826aa6892fd6a9c6d72a60a7f8d12bea58000108a67f0c370f350b9179a081f2fc8d33b62e01e729419860b5ae143cbbcd2769"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"network": "main",
|
||||
"height": "1150000",
|
||||
"hash": "0000000650e627bd7da6868f14070aff8fdbd31ef7125fe77851976ed3adfc54",
|
||||
"time": 1668316308,
|
||||
"saplingTree": "012e058162e4e6bc9553c413134b66e5e89cd63c330fc557060878c623fdedc63d01533dbef6ad6b226c7138bef1e1961ca170510a91fdb1ff5972e1cd79c347401c150168979af907639a39b427d83d4602fd22867faffb0942cbb11955dac680aab85901d8d396d94feb0cc78cbec422c6fd09834c4031a1ffdcbef04178827add5193160102e9b3fecaf39d40e5e63ef3253d1ac5aee4141bdf26763de74033d6016f5955019f59fdd4a570ad22980428caea7b5fa61f55b52e2e6fbb29600eee53d31ed35f017c5ad297c1e83430ce9d3768fd38e74bd06757b67e2917b34d0f3f763803f32600000000015c1016fb7c68d85099ce8423d6446c2ea3d77a63b5ab6044f6eb0c024ddb0d5a01627b5eae7588998ef2645fe8be1ec3227d560828956b7df00632b26784c4f80a0000012d8bdb15bce00ab0c8bd332355a100d9db356ac05fb97412b479214dcefea331000001a5e10b312a666ed313eb0db76bfc977430ec6b463f944816c43cd82d42181d1f000001708c9850eb440b259f233187662c5228804cb4500263949301b6fac8f6428f2301d6f84c424acdb1d10f8cef641662e0f63f954f07fe6199d504a61979c9ba3e13"
|
||||
}
|
||||
@@ -6,13 +6,15 @@ 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.model.LightWalletEndpoint
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
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
|
||||
import java.io.File
|
||||
@@ -20,16 +22,16 @@ import java.io.File
|
||||
/**
|
||||
* Simplified Initializer focused on starting from a ViewingKey.
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
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 lightWalletEndpoint: LightWalletEndpoint,
|
||||
val viewingKeys: List<UnifiedViewingKey>,
|
||||
val overwriteVks: Boolean,
|
||||
val birthday: WalletBirthday
|
||||
internal val checkpoint: Checkpoint
|
||||
) {
|
||||
|
||||
suspend fun erase() = erase(context, network, alias)
|
||||
@@ -38,16 +40,13 @@ 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
|
||||
private set
|
||||
|
||||
lateinit var host: String
|
||||
private set
|
||||
|
||||
var port: Int = ZcashNetwork.Mainnet.defaultPort
|
||||
lateinit var lightWalletEndpoint: LightWalletEndpoint
|
||||
private set
|
||||
|
||||
/**
|
||||
@@ -86,7 +85,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): Config =
|
||||
apply {
|
||||
this.birthdayHeight = height
|
||||
this.defaultToOldestHeight = defaultToOldestHeight
|
||||
@@ -105,7 +104,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
|
||||
}
|
||||
@@ -159,12 +158,10 @@ class Initializer private constructor(
|
||||
*/
|
||||
fun setNetwork(
|
||||
network: ZcashNetwork,
|
||||
host: String = network.defaultHost,
|
||||
port: Int = network.defaultPort
|
||||
lightWalletEndpoint: LightWalletEndpoint
|
||||
): Config = apply {
|
||||
this.network = network
|
||||
this.host = host
|
||||
this.port = port
|
||||
this.lightWalletEndpoint = lightWalletEndpoint
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -172,18 +169,16 @@ class Initializer private constructor(
|
||||
*/
|
||||
suspend fun importWallet(
|
||||
seed: ByteArray,
|
||||
birthdayHeight: Int? = null,
|
||||
birthday: BlockHeight?,
|
||||
network: ZcashNetwork,
|
||||
host: String = network.defaultHost,
|
||||
port: Int = network.defaultPort,
|
||||
lightWalletEndpoint: LightWalletEndpoint,
|
||||
alias: String = ZcashSdk.DEFAULT_ALIAS
|
||||
): Config =
|
||||
importWallet(
|
||||
DerivationTool.deriveUnifiedViewingKeys(seed, network = network)[0],
|
||||
birthdayHeight,
|
||||
birthday,
|
||||
network,
|
||||
host,
|
||||
port,
|
||||
lightWalletEndpoint,
|
||||
alias
|
||||
)
|
||||
|
||||
@@ -192,15 +187,14 @@ class Initializer private constructor(
|
||||
*/
|
||||
fun importWallet(
|
||||
viewingKey: UnifiedViewingKey,
|
||||
birthdayHeight: Int? = null,
|
||||
birthday: BlockHeight?,
|
||||
network: ZcashNetwork,
|
||||
host: String = network.defaultHost,
|
||||
port: Int = network.defaultPort,
|
||||
lightWalletEndpoint: LightWalletEndpoint,
|
||||
alias: String = ZcashSdk.DEFAULT_ALIAS
|
||||
): Config = apply {
|
||||
setViewingKeys(viewingKey)
|
||||
setNetwork(network, host, port)
|
||||
importedWalletBirthday(birthdayHeight)
|
||||
setNetwork(network, lightWalletEndpoint)
|
||||
importedWalletBirthday(birthday)
|
||||
this.alias = alias
|
||||
}
|
||||
|
||||
@@ -210,14 +204,12 @@ class Initializer private constructor(
|
||||
suspend fun newWallet(
|
||||
seed: ByteArray,
|
||||
network: ZcashNetwork,
|
||||
host: String = network.defaultHost,
|
||||
port: Int = network.defaultPort,
|
||||
lightWalletEndpoint: LightWalletEndpoint,
|
||||
alias: String = ZcashSdk.DEFAULT_ALIAS
|
||||
): Config = newWallet(
|
||||
DerivationTool.deriveUnifiedViewingKeys(seed, network)[0],
|
||||
network,
|
||||
host,
|
||||
port,
|
||||
lightWalletEndpoint,
|
||||
alias
|
||||
)
|
||||
|
||||
@@ -227,12 +219,11 @@ class Initializer private constructor(
|
||||
fun newWallet(
|
||||
viewingKey: UnifiedViewingKey,
|
||||
network: ZcashNetwork,
|
||||
host: String = network.defaultHost,
|
||||
port: Int = network.defaultPort,
|
||||
lightWalletEndpoint: LightWalletEndpoint,
|
||||
alias: String = ZcashSdk.DEFAULT_ALIAS
|
||||
): Config = apply {
|
||||
setViewingKeys(viewingKey)
|
||||
setNetwork(network, host, port)
|
||||
setNetwork(network, lightWalletEndpoint)
|
||||
newWalletBirthday()
|
||||
this.alias = alias
|
||||
}
|
||||
@@ -284,8 +275,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)
|
||||
}
|
||||
@@ -328,25 +319,33 @@ class Initializer private constructor(
|
||||
config: Config
|
||||
): Initializer {
|
||||
config.validate()
|
||||
// heightToUse hardcoded for now, otherwise detects older JSON checkpoint files.
|
||||
val heightToUse = 1150000
|
||||
//config.birthdayHeight
|
||||
//?: (if (config.defaultToOldestHeight == true) config.network.saplingActivationHeight else null)
|
||||
val loadedBirthday =
|
||||
WalletBirthdayTool.loadNearest(context, config.network, heightToUse)
|
||||
|
||||
val rustBackend = initRustBackend(context, config.network, config.alias, loadedBirthday)
|
||||
val loadedCheckpoint = run {
|
||||
val height = config.birthdayHeight
|
||||
?: if (config.defaultToOldestHeight == true) {
|
||||
config.network.saplingActivationHeight
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
CheckpointTool.loadNearest(
|
||||
context,
|
||||
config.network,
|
||||
height
|
||||
)
|
||||
}
|
||||
|
||||
val rustBackend = initRustBackend(context, config.network, config.alias, loadedCheckpoint.height)
|
||||
|
||||
return Initializer(
|
||||
context.applicationContext,
|
||||
rustBackend,
|
||||
config.network,
|
||||
config.alias,
|
||||
config.host,
|
||||
config.port,
|
||||
config.lightWalletEndpoint,
|
||||
config.viewingKeys,
|
||||
config.overwriteVks,
|
||||
loadedBirthday
|
||||
loadedCheckpoint
|
||||
)
|
||||
}
|
||||
|
||||
@@ -377,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,14 +47,15 @@ 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.model.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.tool.DerivationTool
|
||||
import cash.z.ecc.android.sdk.type.AddressType
|
||||
import cash.z.ecc.android.sdk.type.AddressType.Shielded
|
||||
import cash.z.ecc.android.sdk.type.AddressType.Transparent
|
||||
import cash.z.ecc.android.sdk.type.ConsensusMatchType
|
||||
import cash.z.ecc.android.sdk.type.WalletBalance
|
||||
import cash.z.ecc.android.sdk.type.ZcashNetwork
|
||||
import cash.z.wallet.sdk.rpc.Service
|
||||
import io.grpc.ManagedChannel
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
@@ -95,7 +97,6 @@ import kotlin.coroutines.EmptyCoroutineContext
|
||||
*/
|
||||
@ExperimentalCoroutinesApi
|
||||
@FlowPreview
|
||||
|
||||
class SdkSynchronizer internal constructor(
|
||||
private val storage: TransactionRepository,
|
||||
private val txManager: OutboundTransactionManager,
|
||||
@@ -103,9 +104,9 @@ class SdkSynchronizer internal constructor(
|
||||
) : Synchronizer {
|
||||
|
||||
// pools
|
||||
//private val _orchardBalances = MutableStateFlow<WalletBalance?>(null)
|
||||
private val _orchardBalances = MutableStateFlow<WalletBalance?>(null)
|
||||
private val _saplingBalances = MutableStateFlow<WalletBalance?>(null)
|
||||
//private val _transparentBalances = MutableStateFlow<WalletBalance?>(null)
|
||||
private val _transparentBalances = MutableStateFlow<WalletBalance?>(null)
|
||||
|
||||
private val _status = ConflatedBroadcastChannel<Synchronizer.Status>(DISCONNECTED)
|
||||
|
||||
@@ -144,9 +145,9 @@ class SdkSynchronizer internal constructor(
|
||||
// Balances
|
||||
//
|
||||
|
||||
//override val orchardBalances = _orchardBalances.asStateFlow()
|
||||
override val orchardBalances = _orchardBalances.asStateFlow()
|
||||
override val saplingBalances = _saplingBalances.asStateFlow()
|
||||
//override val transparentBalances = _transparentBalances.asStateFlow()
|
||||
override val transparentBalances = _transparentBalances.asStateFlow()
|
||||
|
||||
//
|
||||
// Transactions
|
||||
@@ -189,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
|
||||
@@ -231,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
|
||||
@@ -243,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
|
||||
@@ -305,24 +308,10 @@ class SdkSynchronizer internal constructor(
|
||||
*/
|
||||
override suspend fun getServerInfo(): Service.LightdInfo = processor.downloader.getServerInfo()
|
||||
|
||||
/**
|
||||
* Changes the server that is being used to download compact blocks. This will throw an
|
||||
* exception if it detects that the server change is invalid e.g. switching to testnet from
|
||||
* mainnet.
|
||||
*/
|
||||
override suspend fun changeServer(host: String, port: Int, errorHandler: (Throwable) -> Unit) {
|
||||
val info =
|
||||
(processor.downloader.lightWalletService as LightWalletGrpcService).connectionInfo
|
||||
processor.downloader.changeService(
|
||||
LightWalletGrpcService(info.appContext, host, port),
|
||||
errorHandler
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -336,11 +325,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()
|
||||
}
|
||||
|
||||
@@ -356,12 +345,10 @@ class SdkSynchronizer internal constructor(
|
||||
// Private API
|
||||
//
|
||||
|
||||
/* THIS IS NOT SUPPORTED IN HUSH LIGHTWALLETD
|
||||
suspend fun refreshUtxos() {
|
||||
twig("refreshing utxos", -1)
|
||||
refreshUtxos(getTransparentAddress())
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Calculate the latest balance, based on the blocks that have been scanned and transmit this
|
||||
@@ -369,7 +356,7 @@ class SdkSynchronizer internal constructor(
|
||||
*/
|
||||
suspend fun refreshAllBalances() {
|
||||
refreshSaplingBalance()
|
||||
// refreshTransparentBalance()
|
||||
refreshTransparentBalance()
|
||||
// TODO: refresh orchard balance
|
||||
twig("Warning: Orchard balance does not yet refresh. Only some of the plumbing is in place.")
|
||||
}
|
||||
@@ -379,14 +366,11 @@ class SdkSynchronizer internal constructor(
|
||||
_saplingBalances.value = processor.getBalanceInfo()
|
||||
}
|
||||
|
||||
/* THIS IS NOT SUPPORTED IN HUSH LIGHTWALLETD
|
||||
suspend fun refreshTransparentBalance() {
|
||||
twig("refreshing transparent balance")
|
||||
_transparentBalances.value = processor.getUtxoCacheBalance(getTransparentAddress())
|
||||
}
|
||||
*/
|
||||
|
||||
/* THIS IS NOT SUPPORTED IN HUSH LIGHTWALLETD
|
||||
suspend fun isValidAddress(address: String): Boolean {
|
||||
try {
|
||||
return !validateAddress(address).isNotValid
|
||||
@@ -394,7 +378,6 @@ class SdkSynchronizer internal constructor(
|
||||
}
|
||||
return false
|
||||
}
|
||||
*/
|
||||
|
||||
private fun CoroutineScope.onReady() = launch(CoroutineExceptionHandler(::onCriticalError)) {
|
||||
twig("Preparing to start...")
|
||||
@@ -486,7 +469,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(
|
||||
@@ -501,7 +484,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
|
||||
@@ -523,7 +506,7 @@ class SdkSynchronizer internal constructor(
|
||||
// balance refresh is complete.
|
||||
if (shouldRefresh) {
|
||||
twigTask("Triggering utxo refresh since $reason!", -1) {
|
||||
//refreshUtxos()
|
||||
refreshUtxos()
|
||||
}
|
||||
twigTask("Triggering balance refresh since $reason!", -1) {
|
||||
refreshAllBalances()
|
||||
@@ -701,22 +684,17 @@ class SdkSynchronizer internal constructor(
|
||||
txManager.monitorById(it.id)
|
||||
}.distinctUntilChanged()
|
||||
|
||||
/* THIS IS NOT SUPPORTED IN HUSH LIGHTWALLETD
|
||||
override suspend fun refreshUtxos(address: String, startHeight: Int): Int? {
|
||||
override suspend fun refreshUtxos(address: String, startHeight: BlockHeight): Int? {
|
||||
return processor.refreshUtxos(address, startHeight)
|
||||
}
|
||||
*/
|
||||
|
||||
/* THIS IS NOT SUPPORTED IN HUSH LIGHTWALLETD
|
||||
override suspend fun getTransparentBalance(tAddr: String): WalletBalance {
|
||||
return processor.getUtxoCacheBalance(tAddr)
|
||||
}
|
||||
*/
|
||||
|
||||
override suspend fun isValidShieldedAddr(address: String) =
|
||||
txManager.isValidShieldedAddress(address)
|
||||
|
||||
/* THIS IS NOT SUPPORTED IN HUSH LIGHTWALLETD
|
||||
override suspend fun isValidTransparentAddr(address: String) =
|
||||
txManager.isValidTransparentAddress(address)
|
||||
|
||||
@@ -737,7 +715,6 @@ class SdkSynchronizer internal constructor(
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
override suspend fun validateConsensusBranch(): ConsensusMatchType {
|
||||
val serverBranchId = tryNull { processor.downloader.getServerInfo().consensusBranchId }
|
||||
@@ -797,18 +774,19 @@ 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)
|
||||
LightWalletGrpcService.new(initializer.context, initializer.lightWalletEndpoint)
|
||||
|
||||
fun defaultEncoder(
|
||||
initializer: Initializer,
|
||||
|
||||
@@ -4,11 +4,12 @@ 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.model.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.type.AddressType
|
||||
import cash.z.ecc.android.sdk.type.ConsensusMatchType
|
||||
import cash.z.ecc.android.sdk.type.WalletBalance
|
||||
import cash.z.ecc.android.sdk.type.ZcashNetwork
|
||||
import cash.z.wallet.sdk.rpc.Service
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@@ -98,12 +99,12 @@ 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.
|
||||
*/
|
||||
//val orchardBalances: StateFlow<WalletBalance?>
|
||||
val orchardBalances: StateFlow<WalletBalance?>
|
||||
|
||||
/**
|
||||
* A stream of balance values for the sapling pool. Includes the available and total balance.
|
||||
@@ -113,7 +114,7 @@ interface Synchronizer {
|
||||
/**
|
||||
* A stream of balance values for the transparent pool. Includes the available and total balance.
|
||||
*/
|
||||
//val transparentBalances: StateFlow<WalletBalance?>
|
||||
val transparentBalances: StateFlow<WalletBalance?>
|
||||
|
||||
/* Transactions */
|
||||
|
||||
@@ -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
|
||||
@@ -238,9 +239,7 @@ interface Synchronizer {
|
||||
*
|
||||
* @throws RuntimeException when the address is invalid.
|
||||
*/
|
||||
/* THIS IS NOT SUPPORTED IN HUSH LIGHTWALLETD
|
||||
suspend fun isValidTransparentAddr(address: String): Boolean
|
||||
*/
|
||||
|
||||
/**
|
||||
* Validate whether the server and this SDK share the same consensus branch. This is
|
||||
@@ -266,9 +265,7 @@ interface Synchronizer {
|
||||
*
|
||||
* @return an instance of [AddressType] providing validation info regarding the given address.
|
||||
*/
|
||||
/* THIS IS NOT SUPPORTED IN HUSH LIGHTWALLETD
|
||||
suspend fun validateAddress(address: String): AddressType
|
||||
*/
|
||||
|
||||
/**
|
||||
* Attempts to cancel a transaction that is about to be sent. Typically, cancellation is only
|
||||
@@ -287,38 +284,22 @@ interface Synchronizer {
|
||||
*/
|
||||
suspend fun getServerInfo(): Service.LightdInfo
|
||||
|
||||
/**
|
||||
* Gracefully change the server that the Synchronizer is currently using. In some cases, this
|
||||
* will require waiting until current network activity is complete. Ideally, this would protect
|
||||
* against accidentally switching between testnet and mainnet, by comparing the service info of
|
||||
* the existing server with that of the new one.
|
||||
*/
|
||||
suspend fun changeServer(
|
||||
host: String,
|
||||
port: Int = network.defaultPort,
|
||||
errorHandler: (Throwable) -> Unit = { throw it }
|
||||
)
|
||||
|
||||
/**
|
||||
* Download all UTXOs for the given address and store any new ones in the database.
|
||||
*
|
||||
* @return the number of utxos that were downloaded and addded to the UTXO table.
|
||||
*/
|
||||
/* THIS IS NOT SUPPORTED IN HUSH LIGHTWALLETD
|
||||
suspend fun refreshUtxos(
|
||||
tAddr: String,
|
||||
sinceHeight: Int = network.saplingActivationHeight
|
||||
since: BlockHeight = network.saplingActivationHeight
|
||||
): Int?
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns the balance that the wallet knows about. This should be called after [refreshUtxos].
|
||||
*/
|
||||
/* THIS IS NOT SUPPORT IN HUSH LIGHTWALLETD
|
||||
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
|
||||
@@ -326,7 +307,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()
|
||||
|
||||
@@ -380,7 +361,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,13 +33,15 @@ 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.type.WalletBalance
|
||||
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
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -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,10 @@ 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()
|
||||
// This accessed by the Dispatchers.IO thread, which means multiple threads are reading/writing
|
||||
// concurrently.
|
||||
@Volatile
|
||||
internal var currentInfo = ProcessorInfo(null, null, null, null, null)
|
||||
|
||||
/**
|
||||
* The zcash network that is being processed. Either Testnet or Mainnet.
|
||||
@@ -193,25 +204,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 +262,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 +307,39 @@ class CompactBlockProcessor(
|
||||
ProcessorInfo(
|
||||
networkBlockHeight = downloader.getLatestBlockHeight(),
|
||||
lastScannedHeight = getLastScannedHeight(),
|
||||
lastDownloadedHeight = max(getLastDownloadedHeight(), lowerBoundHeight - 1)
|
||||
lastDownloadedHeight = getLastDownloadedHeight()?.let {
|
||||
BlockHeight.new(
|
||||
network,
|
||||
max(
|
||||
it.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.networkBlockHeight != null) {
|
||||
BlockHeight.new(
|
||||
network,
|
||||
buildList {
|
||||
add(network.saplingActivationHeight.value)
|
||||
initialInfo.lastDownloadedHeight?.let { add(it.value + 1) }
|
||||
initialInfo.lastScannedHeight?.let { add(it.value + 1) }
|
||||
}.max()
|
||||
)..initialInfo.networkBlockHeight
|
||||
} else {
|
||||
null
|
||||
}
|
||||
)
|
||||
}
|
||||
true
|
||||
@@ -306,35 +355,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 +394,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 +425,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,12 +445,18 @@ class CompactBlockProcessor(
|
||||
else -> {
|
||||
// verify that the server is correct
|
||||
downloader.getServerInfo().let { info ->
|
||||
//val clientBranch = "%x".format(rustBackend.getBranchIdForHeight(info.blockHeight.toInt()))
|
||||
val clientBranch = "76b809bb"
|
||||
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,34 +486,36 @@ class CompactBlockProcessor(
|
||||
}
|
||||
}
|
||||
|
||||
/* THIS IS NOT SUPPORTED IN HUSH LIGHTWALLETD
|
||||
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
|
||||
}
|
||||
*/
|
||||
|
||||
/* THIS IS NOT SUPPORTED IN HUSH LIGHTWALLETD
|
||||
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")
|
||||
@@ -466,7 +528,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)
|
||||
@@ -477,7 +539,6 @@ class CompactBlockProcessor(
|
||||
// return the number of UTXOs that were downloaded
|
||||
result.size - skipped
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Request all blocks in the given range and persist them locally for processing, later.
|
||||
@@ -485,75 +546,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.new(
|
||||
network,
|
||||
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()
|
||||
updateProgress(lastDownloadedHeight = lastDownloadedHeight)
|
||||
downloadedBlockHeight = end + 1
|
||||
}
|
||||
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 {
|
||||
@@ -570,16 +637,18 @@ 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")
|
||||
@@ -605,12 +674,12 @@ 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
|
||||
): Unit = withContext(IO) {
|
||||
networkBlockHeight: BlockHeight? = currentInfo.networkBlockHeight,
|
||||
lastScannedHeight: BlockHeight? = currentInfo.lastScannedHeight,
|
||||
lastDownloadedHeight: BlockHeight? = currentInfo.lastDownloadedHeight,
|
||||
lastScanRange: ClosedRange<BlockHeight>? = currentInfo.lastScanRange,
|
||||
lastDownloadRange: ClosedRange<BlockHeight>? = currentInfo.lastDownloadRange
|
||||
) {
|
||||
currentInfo = currentInfo.copy(
|
||||
networkBlockHeight = networkBlockHeight,
|
||||
lastScannedHeight = lastScannedHeight,
|
||||
@@ -618,11 +687,14 @@ class CompactBlockProcessor(
|
||||
lastScanRange = lastScanRange,
|
||||
lastDownloadRange = lastDownloadRange
|
||||
)
|
||||
_networkHeight.value = networkBlockHeight
|
||||
_processorInfo.send(currentInfo)
|
||||
|
||||
withContext(IO) {
|
||||
_networkHeight.value = networkBlockHeight
|
||||
_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 ->
|
||||
@@ -632,14 +704,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -649,7 +724,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)
|
||||
}
|
||||
|
||||
@@ -657,45 +735,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
|
||||
|
||||
@@ -704,19 +810,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)
|
||||
@@ -734,9 +846,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")
|
||||
}
|
||||
@@ -759,19 +871,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 }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -824,7 +945,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.
|
||||
@@ -869,7 +991,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
|
||||
@@ -912,11 +1034,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>?
|
||||
) {
|
||||
|
||||
/**
|
||||
@@ -924,19 +1046,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.
|
||||
@@ -944,32 +1071,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?
|
||||
@@ -1007,10 +1141,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,7 +5,7 @@ import androidx.room.Entity
|
||||
|
||||
@Entity(primaryKeys = ["height"], tableName = "compactblocks")
|
||||
data class CompactBlockEntity(
|
||||
val height: Int,
|
||||
val height: Long,
|
||||
@ColumnInfo(typeAffinity = ColumnInfo.BLOB)
|
||||
val data: ByteArray
|
||||
) {
|
||||
@@ -20,7 +20,7 @@ data class CompactBlockEntity(
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = height
|
||||
var result = height.hashCode()
|
||||
result = 31 * result + data.contentHashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
@@ -96,6 +97,10 @@ data class PendingTransactionEntity(
|
||||
@ColumnInfo(typeAffinity = ColumnInfo.BLOB)
|
||||
override val rawTransactionId: ByteArray? = byteArrayOf()
|
||||
) : PendingTransaction {
|
||||
|
||||
val valueZatoshi: Zatoshi
|
||||
get() = Zatoshi(value)
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is PendingTransactionEntity) return false
|
||||
@@ -131,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
|
||||
@@ -159,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),
|
||||
|
||||
@@ -169,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
|
||||
@@ -200,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)
|
||||
@@ -213,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
|
||||
@@ -229,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
|
||||
}
|
||||
}
|
||||
@@ -260,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
|
||||
@@ -273,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
|
||||
@@ -333,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 {
|
||||
@@ -369,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,6 +1,8 @@
|
||||
package cash.z.ecc.android.sdk.exception
|
||||
|
||||
import cash.z.ecc.android.sdk.type.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.internal.model.Checkpoint
|
||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import cash.z.wallet.sdk.rpc.Service
|
||||
import io.grpc.Status
|
||||
import io.grpc.Status.Code.UNAVAILABLE
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -9,7 +9,12 @@ import java.util.Locale
|
||||
*/
|
||||
enum class ConsensusBranchId(val displayName: String, val id: Long, val hexId: String) {
|
||||
// TODO: see if we can find a way to not rely on this separate source of truth (either stop converting from hex to display name in the apps or use Rust to get this info)
|
||||
SAPLING("Sapling", 0x76b8_09bb, "76b809bb");
|
||||
SPROUT("Sprout", 0, "0"),
|
||||
OVERWINTER("Overwinter", 0x5ba8_1b19, "5ba81b19"),
|
||||
SAPLING("Sapling", 0x76b8_09bb, "76b809bb"),
|
||||
BLOSSOM("Blossom", 0x2bb4_0e60, "2bb40e60"),
|
||||
HEARTWOOD("Heartwood", 0xf5b9_230b, "f5b9230b"),
|
||||
CANOPY("Canopy", 0xe9ff_75a6, "e9ff75a6");
|
||||
|
||||
override fun toString(): String = displayName
|
||||
|
||||
|
||||
@@ -34,7 +34,10 @@ object ZcashSdk {
|
||||
/**
|
||||
* Default size of batches of blocks to request from the compact block service.
|
||||
*/
|
||||
val DOWNLOAD_BATCH_SIZE = 100
|
||||
// Because blocks are buffered in memory upon download and storage into SQLite, there is an upper bound
|
||||
// above which OutOfMemoryError is thrown. Experimentally, this value is below 50 blocks.
|
||||
// Back of the envelope calculation says the maximum block size is ~100kb.
|
||||
const val DOWNLOAD_BATCH_SIZE = 10
|
||||
|
||||
/**
|
||||
* Default size of batches of blocks to scan via librustzcash. The smaller this number the more granular information
|
||||
|
||||
@@ -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.model.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
|
||||
@@ -75,6 +75,7 @@ interface Twig {
|
||||
* @see [Twig.clip]
|
||||
*/
|
||||
object Bush {
|
||||
@Volatile
|
||||
var trunk: Twig = SilentTwig()
|
||||
val leaves: MutableSet<Leaf> = CopyOnWriteArraySet<Leaf>()
|
||||
}
|
||||
|
||||
@@ -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,30 +7,34 @@ 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.model.ZcashNetwork
|
||||
import cash.z.wallet.sdk.rpc.CompactFormats
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.math.max
|
||||
|
||||
/**
|
||||
* An implementation of CompactBlockStore that persists information to a database in the given
|
||||
* 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? = runCatching {
|
||||
BlockHeight.new(network, cacheDao.latestBlockHeight())
|
||||
}.getOrNull()
|
||||
|
||||
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 write(result: Sequence<CompactFormats.CompactBlock>) =
|
||||
cacheDao.insert(result.map { CompactBlockEntity(it.height, 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 +47,14 @@ 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,10 +44,9 @@ 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 +55,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,28 +12,29 @@ 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.
|
||||
*
|
||||
* @param result the list of compact blocks to persist.
|
||||
* @return Number of blocks that were written.
|
||||
*/
|
||||
suspend fun write(result: List<CompactFormats.CompactBlock>)
|
||||
suspend fun write(result: Sequence<CompactFormats.CompactBlock>): Int
|
||||
|
||||
/**
|
||||
* Remove every block above the given height.
|
||||
*
|
||||
* @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.
|
||||
|
||||
@@ -6,6 +6,7 @@ import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.Transaction
|
||||
import cash.z.ecc.android.sdk.db.entity.CompactBlockEntity
|
||||
|
||||
//
|
||||
@@ -42,12 +43,24 @@ interface CompactBlockDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insert(block: List<CompactBlockEntity>)
|
||||
|
||||
@Transaction
|
||||
suspend fun insert(blocks: Sequence<CompactBlockEntity>): Int {
|
||||
var count = 0
|
||||
|
||||
blocks.forEach {
|
||||
insert(it)
|
||||
count++
|
||||
}
|
||||
|
||||
return count
|
||||
}
|
||||
|
||||
@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
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
package cash.z.ecc.android.sdk.internal.service
|
||||
|
||||
import android.content.Context
|
||||
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.type.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
|
||||
import cash.z.wallet.sdk.rpc.CompactFormats
|
||||
import cash.z.wallet.sdk.rpc.CompactTxStreamerGrpc
|
||||
import cash.z.wallet.sdk.rpc.Service
|
||||
@@ -15,69 +14,50 @@ 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(
|
||||
context: Context,
|
||||
private val lightWalletEndpoint: LightWalletEndpoint,
|
||||
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
|
||||
|
||||
constructor(
|
||||
appContext: Context,
|
||||
network: ZcashNetwork,
|
||||
usePlaintext: Boolean =
|
||||
appContext.resources.getBoolean(R.bool.lightwalletd_allow_very_insecure_connections)
|
||||
) : this(appContext, network.defaultHost, network.defaultPort, true)
|
||||
|
||||
/**
|
||||
* Construct an instance that corresponds to the given host and port.
|
||||
*
|
||||
* @param appContext the application context used to check whether TLS is required by this build
|
||||
* flavor.
|
||||
* @param host the host of the server to use.
|
||||
* @param port the port of the server to use.
|
||||
* @param usePlaintext whether to use TLS or plaintext for requests. Plaintext is dangerous so
|
||||
* it requires jumping through a few more hoops.
|
||||
*/
|
||||
constructor(
|
||||
appContext: Context,
|
||||
host: String,
|
||||
port: Int = ZcashNetwork.Mainnet.defaultPort,
|
||||
usePlaintext: Boolean =
|
||||
appContext.resources.getBoolean(R.bool.lightwalletd_allow_very_insecure_connections)
|
||||
) : this(createDefaultChannel(appContext, host, port, true)) {
|
||||
connectionInfo = ConnectionInfo(appContext.applicationContext, host, port, true)
|
||||
}
|
||||
private val applicationContext = context.applicationContext
|
||||
|
||||
/* LightWalletService implementation */
|
||||
|
||||
override fun getBlockRange(heightRange: IntRange): List<CompactFormats.CompactBlock> {
|
||||
if (heightRange.isEmpty()) return listOf()
|
||||
override fun getBlockRange(heightRange: ClosedRange<BlockHeight>): Sequence<CompactFormats.CompactBlock> {
|
||||
if (heightRange.isEmpty()) {
|
||||
return emptySequence()
|
||||
}
|
||||
|
||||
return requireChannel().createStub(streamingRequestTimeoutSec)
|
||||
.getBlockRange(heightRange.toBlockRange()).toList()
|
||||
return requireChannel().createStub(streamingRequestTimeout)
|
||||
.getBlockRange(heightRange.toBlockRange()).iterator().asSequence()
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
@@ -109,23 +89,20 @@ class LightWalletGrpcService private constructor(
|
||||
)
|
||||
}
|
||||
|
||||
/* THIS IS NOT SUPPORTED IN HUSH LIGHTWALLETD
|
||||
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
|
||||
}
|
||||
*/
|
||||
|
||||
/* THIS IS NOT SUPPORTED IN HUSH LIGHTWALLETD
|
||||
override fun getTAddressTransactions(
|
||||
tAddress: String,
|
||||
blockHeightRange: IntRange
|
||||
blockHeightRange: ClosedRange<BlockHeight>
|
||||
): List<Service.RawTransaction> {
|
||||
if (blockHeightRange.isEmpty() || tAddress.isBlank()) return listOf()
|
||||
|
||||
@@ -135,75 +112,38 @@ class LightWalletGrpcService private constructor(
|
||||
)
|
||||
return result.toList()
|
||||
}
|
||||
*/
|
||||
|
||||
override fun reconnect() {
|
||||
twig(
|
||||
"closing existing channel and then reconnecting to ${connectionInfo.host}:" +
|
||||
"${connectionInfo.port}?usePlaintext=${connectionInfo.usePlaintext}"
|
||||
)
|
||||
twig("closing existing channel and then reconnecting")
|
||||
channel.shutdown()
|
||||
channel = createDefaultChannel(
|
||||
connectionInfo.appContext,
|
||||
connectionInfo.host,
|
||||
connectionInfo.port,
|
||||
true
|
||||
)
|
||||
channel = createDefaultChannel(applicationContext, lightWalletEndpoint)
|
||||
}
|
||||
|
||||
// test code
|
||||
var stateCount = 0
|
||||
var state: ConnectivityState? = null
|
||||
internal var stateCount = 0
|
||||
internal var state: ConnectivityState? = null
|
||||
private fun requireChannel(): ManagedChannel {
|
||||
state = channel.getState(false).let { new ->
|
||||
if (state == new) stateCount++ else stateCount = 0
|
||||
new
|
||||
}
|
||||
channel.resetConnectBackoff()
|
||||
twig("getting channel isShutdown: ${channel.isShutdown} isTerminated: ${channel.isTerminated} getState: $state stateCount: $stateCount", -1)
|
||||
twig(
|
||||
"getting channel isShutdown: ${channel.isShutdown} " +
|
||||
"isTerminated: ${channel.isTerminated} " +
|
||||
"getState: $state stateCount: $stateCount",
|
||||
-1
|
||||
)
|
||||
return channel
|
||||
}
|
||||
|
||||
//
|
||||
// Utilities
|
||||
//
|
||||
|
||||
private fun Channel.createStub(timeoutSec: Long = 60L) = 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()
|
||||
|
||||
/**
|
||||
* This function effectively parses streaming responses. Each call to next(), on the iterators
|
||||
* returned from grpc, triggers a network call.
|
||||
*/
|
||||
private fun <T> Iterator<T>.toList(): List<T> =
|
||||
mutableListOf<T>().apply {
|
||||
while (hasNext()) {
|
||||
this@apply += next()
|
||||
}
|
||||
}
|
||||
|
||||
inner class ConnectionInfo(
|
||||
val appContext: Context,
|
||||
val host: String,
|
||||
val port: Int,
|
||||
val usePlaintext: Boolean
|
||||
) {
|
||||
override fun toString(): String {
|
||||
return "$host:$port?usePlaintext=true"
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun new(context: Context, lightWalletEndpoint: LightWalletEndpoint): LightWalletGrpcService {
|
||||
val channel = createDefaultChannel(context, lightWalletEndpoint)
|
||||
|
||||
return LightWalletGrpcService(context, lightWalletEndpoint, channel)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function for creating the default channel to be used for all connections. It
|
||||
* is important that this channel can handle transitioning from WiFi to Cellular connections
|
||||
@@ -211,27 +151,53 @@ class LightWalletGrpcService private constructor(
|
||||
*/
|
||||
fun createDefaultChannel(
|
||||
appContext: Context,
|
||||
host: String,
|
||||
port: Int,
|
||||
usePlaintext: Boolean
|
||||
lightWalletEndpoint: LightWalletEndpoint
|
||||
): ManagedChannel {
|
||||
twig("Creating channel that will connect to $host:$port?usePlaintext=$usePlaintext")
|
||||
twig(
|
||||
"Creating channel that will connect to " +
|
||||
"${lightWalletEndpoint.host}:${lightWalletEndpoint.port}" +
|
||||
"/?usePlaintext=${!lightWalletEndpoint.isSecure}"
|
||||
)
|
||||
return AndroidChannelBuilder
|
||||
.forAddress(host, port)
|
||||
.forAddress(lightWalletEndpoint.host, lightWalletEndpoint.port)
|
||||
.context(appContext)
|
||||
.enableFullStreamDecompression()
|
||||
.apply {
|
||||
if (usePlaintext) {
|
||||
if (!appContext.resources.getBoolean(
|
||||
R.bool.lightwalletd_allow_very_insecure_connections
|
||||
)
|
||||
) throw LightWalletException.InsecureConnection
|
||||
usePlaintext()
|
||||
} else {
|
||||
usePlaintext()
|
||||
/*
|
||||
if (lightWalletEndpoint.isSecure) {
|
||||
useTransportSecurity()
|
||||
} else {
|
||||
twig("WARNING: Using insecure channel")
|
||||
usePlaintext()
|
||||
}
|
||||
*/
|
||||
}
|
||||
.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Channel.createStub(timeoutSec: Duration = 60.seconds) = CompactTxStreamerGrpc
|
||||
.newBlockingStub(this)
|
||||
.withDeadlineAfter(timeoutSec.inWholeSeconds, TimeUnit.SECONDS)
|
||||
|
||||
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()
|
||||
|
||||
/**
|
||||
* This function effectively parses streaming responses. Each call to next(), on the iterators
|
||||
* returned from grpc, triggers a network call.
|
||||
*/
|
||||
private fun <T> Iterator<T>.toList(): List<T> =
|
||||
mutableListOf<T>().apply {
|
||||
while (hasNext()) {
|
||||
this@apply += next()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,9 +25,7 @@ interface LightWalletService {
|
||||
*
|
||||
* @return the UTXOs for the given address from the startHeight.
|
||||
*/
|
||||
/* THIS IS NOT SUPPORT IN HUSH LIGHTWALLETD
|
||||
fun fetchUtxos(tAddress: String, startHeight: Int): List<Service.GetAddressUtxosReply>
|
||||
*/
|
||||
fun fetchUtxos(tAddress: String, startHeight: BlockHeight): List<Service.GetAddressUtxosReply>
|
||||
|
||||
/**
|
||||
* Return the given range of blocks.
|
||||
@@ -37,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>): Sequence<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:
|
||||
@@ -72,9 +71,7 @@ interface LightWalletService {
|
||||
*
|
||||
* @return a list of transactions that correspond to the given address for the given range.
|
||||
*/
|
||||
/* THIS IS NOT SUPPORTED IN HUSH LIGHTWALLETD
|
||||
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.model.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.type.UnifiedViewingKey
|
||||
import cash.z.ecc.android.sdk.type.WalletBirthday
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +116,7 @@ class PersistentTransactionManager(
|
||||
twig("beginning to encode transaction with : $encoder")
|
||||
val encodedTx = encoder.createTransaction(
|
||||
spendingKey,
|
||||
tx.value,
|
||||
tx.valueZatoshi,
|
||||
tx.toAddress,
|
||||
tx.memo,
|
||||
tx.accountIndex
|
||||
@@ -230,10 +231,8 @@ class PersistentTransactionManager(
|
||||
override suspend fun isValidShieldedAddress(address: String) =
|
||||
encoder.isValidShieldedAddress(address)
|
||||
|
||||
/* THIS IS NOT SUPPORTED IN HUSH LIGHTWALLETD
|
||||
override suspend fun isValidTransparentAddress(address: String) =
|
||||
encoder.isValidTransparentAddress(address)
|
||||
*/
|
||||
|
||||
override suspend fun cancel(pendingId: Long): Boolean {
|
||||
return pendingTransactionDao {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package cash.z.ecc.android.sdk.internal.transaction
|
||||
|
||||
import cash.z.ecc.android.sdk.db.entity.EncodedTransaction
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
|
||||
interface TransactionEncoder {
|
||||
/**
|
||||
@@ -9,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.
|
||||
@@ -18,7 +19,7 @@ interface TransactionEncoder {
|
||||
*/
|
||||
suspend fun createTransaction(
|
||||
spendingKey: String,
|
||||
zatoshi: Long,
|
||||
amount: Zatoshi,
|
||||
toAddress: String,
|
||||
memo: ByteArray? = byteArrayOf(),
|
||||
fromAccountIndex: Int = 0
|
||||
@@ -48,9 +49,7 @@ interface TransactionEncoder {
|
||||
*
|
||||
* @return true when the given address is a valid t-addr
|
||||
*/
|
||||
/* THIS IS NOT SUPPORTED IN HUSH LIGHTWALLETD
|
||||
suspend fun isValidTransparentAddress(address: String): Boolean
|
||||
*/
|
||||
|
||||
/**
|
||||
* Return the consensus branch that the encoder is using when making transactions.
|
||||
|
||||
@@ -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
|
||||
@@ -94,9 +95,7 @@ interface OutboundTransactionManager {
|
||||
*
|
||||
* @return true when the given address is a valid z-addr.
|
||||
*/
|
||||
/* THIS IS NOT SUPPORTED IN HUSH LIGHTWALLETD
|
||||
suspend fun isValidTransparentAddress(address: String): Boolean
|
||||
*/
|
||||
|
||||
/**
|
||||
* Attempt to cancel a transaction.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ 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.Zatoshi
|
||||
|
||||
/**
|
||||
* Class responsible for encoding a transaction in a consistent way. This bridges the gap by
|
||||
@@ -18,7 +19,7 @@ import cash.z.ecc.android.sdk.jni.RustBackendWelding
|
||||
* @property repository the repository that stores information about the transactions being created
|
||||
* such as the raw bytes and raw txId.
|
||||
*/
|
||||
class WalletTransactionEncoder(
|
||||
internal class WalletTransactionEncoder(
|
||||
private val rustBackend: RustBackendWelding,
|
||||
private val repository: TransactionRepository
|
||||
) : TransactionEncoder {
|
||||
@@ -29,7 +30,7 @@ class WalletTransactionEncoder(
|
||||
* 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.
|
||||
@@ -38,12 +39,12 @@ class WalletTransactionEncoder(
|
||||
*/
|
||||
override suspend fun createTransaction(
|
||||
spendingKey: String,
|
||||
zatoshi: Long,
|
||||
amount: Zatoshi,
|
||||
toAddress: String,
|
||||
memo: ByteArray?,
|
||||
fromAccountIndex: Int
|
||||
): EncodedTransaction {
|
||||
val transactionId = createSpend(spendingKey, zatoshi, toAddress, memo)
|
||||
val transactionId = createSpend(spendingKey, amount, toAddress, memo)
|
||||
return repository.findEncodedTransactionById(transactionId)
|
||||
?: throw TransactionEncoderException.TransactionNotFoundException(transactionId)
|
||||
}
|
||||
@@ -77,10 +78,8 @@ class WalletTransactionEncoder(
|
||||
*
|
||||
* @return true when the given address is a valid t-addr
|
||||
*/
|
||||
/* THIS IS NOT SUPPORTED BY HUSH LIGHTWALLETD
|
||||
override suspend fun isValidTransparentAddress(address: String): Boolean =
|
||||
rustBackend.isValidTransparentAddr(address)
|
||||
*/
|
||||
|
||||
override suspend fun getConsensusBranchId(): Long {
|
||||
val height = repository.lastScannedHeight()
|
||||
@@ -95,7 +94,7 @@ class WalletTransactionEncoder(
|
||||
* the result in the database. On average, this call takes over 10 seconds.
|
||||
*
|
||||
* @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.
|
||||
@@ -105,13 +104,13 @@ class WalletTransactionEncoder(
|
||||
*/
|
||||
private suspend fun createSpend(
|
||||
spendingKey: String,
|
||||
zatoshi: Long,
|
||||
amount: Zatoshi,
|
||||
toAddress: String,
|
||||
memo: ByteArray? = byteArrayOf(),
|
||||
fromAccountIndex: Int = 0
|
||||
): Long {
|
||||
return twigTask(
|
||||
"creating transaction to spend $zatoshi zatoshi to" +
|
||||
"creating transaction to spend $amount zatoshi to" +
|
||||
" ${toAddress.masked()} with memo $memo"
|
||||
) {
|
||||
try {
|
||||
@@ -123,7 +122,7 @@ class WalletTransactionEncoder(
|
||||
fromAccountIndex,
|
||||
spendingKey,
|
||||
toAddress,
|
||||
zatoshi,
|
||||
amount.value,
|
||||
memo
|
||||
)
|
||||
} catch (t: Throwable) {
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
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.model.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.tool.DerivationTool
|
||||
import cash.z.ecc.android.sdk.type.UnifiedViewingKey
|
||||
import cash.z.ecc.android.sdk.type.WalletBalance
|
||||
import cash.z.ecc.android.sdk.type.ZcashNetwork
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -110,11 +100,9 @@ class RustBackend private constructor() : RustBackendWelding {
|
||||
)
|
||||
}
|
||||
|
||||
/* THIS IS NOT SUPPORTED IN HUSH LIGHTWALLETD
|
||||
override suspend fun getTransparentAddress(account: Int, index: Int): String {
|
||||
throw NotImplementedError("TODO: implement this at the zcash_client_sqlite level. But for now, use DerivationTool, instead to derive addresses from seeds")
|
||||
}
|
||||
*/
|
||||
|
||||
override suspend fun getBalance(account: Int): Zatoshi {
|
||||
val longValue = withContext(SdkDispatchers.DATABASE_IO) {
|
||||
@@ -158,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
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -179,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
|
||||
)
|
||||
}
|
||||
@@ -266,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,
|
||||
@@ -275,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
|
||||
)
|
||||
}
|
||||
@@ -313,13 +312,11 @@ class RustBackend private constructor() : RustBackendWelding {
|
||||
override fun isValidShieldedAddr(addr: String) =
|
||||
isValidShieldedAddress(addr, networkId = network.id)
|
||||
|
||||
/* THIS IS NOT SUPPORTED IN HUSH LIGHTWALLETD
|
||||
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
|
||||
@@ -355,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
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -400,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,
|
||||
@@ -417,10 +412,8 @@ class RustBackend private constructor() : RustBackendWelding {
|
||||
@JvmStatic
|
||||
private external fun isValidShieldedAddress(addr: String, networkId: Int): Boolean
|
||||
|
||||
/* THIS IS NOT SUPPORT IN HUSH LIGHTWALLETD
|
||||
@JvmStatic
|
||||
private external fun isValidTransparentAddress(addr: String, networkId: Int): Boolean
|
||||
*/
|
||||
|
||||
@JvmStatic
|
||||
private external fun getBalance(dbDataPath: String, account: Int, networkId: Int): Long
|
||||
@@ -451,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
|
||||
|
||||
@@ -519,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(
|
||||
@@ -529,7 +522,7 @@ class RustBackend private constructor() : RustBackendWelding {
|
||||
index: Int,
|
||||
script: ByteArray,
|
||||
value: Long,
|
||||
height: Int,
|
||||
height: Long,
|
||||
networkId: Int
|
||||
): Boolean
|
||||
|
||||
@@ -537,7 +530,7 @@ class RustBackend private constructor() : RustBackendWelding {
|
||||
private external fun clearUtxos(
|
||||
dbDataPath: String,
|
||||
tAddress: String,
|
||||
aboveHeight: Int,
|
||||
aboveHeight: Long,
|
||||
networkId: Int
|
||||
): Boolean
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
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.model.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.type.UnifiedViewingKey
|
||||
import cash.z.ecc.android.sdk.type.WalletBalance
|
||||
import cash.z.ecc.android.sdk.type.ZcashNetwork
|
||||
|
||||
/**
|
||||
* Contract defining the exposed capabilities of the Rust backend.
|
||||
@@ -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,21 +38,21 @@ 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
|
||||
|
||||
fun isValidShieldedAddr(addr: String): Boolean
|
||||
|
||||
//fun isValidTransparentAddr(addr: String): Boolean
|
||||
fun isValidTransparentAddr(addr: String): Boolean
|
||||
|
||||
suspend fun getShieldedAddress(account: Int = 0): String
|
||||
|
||||
//suspend fun getTransparentAddress(account: Int = 0, index: Int = 0): String
|
||||
suspend fun getTransparentAddress(account: Int = 0, index: Int = 0): String
|
||||
|
||||
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,67 @@
|
||||
package cash.z.ecc.android.sdk.model
|
||||
|
||||
import android.content.Context
|
||||
import cash.z.ecc.android.sdk.tool.CheckpointTool
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package cash.z.ecc.android.sdk.model
|
||||
|
||||
data class LightWalletEndpoint(val host: String, val port: Int, val isSecure: Boolean) {
|
||||
companion object
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
@file:Suppress("ktlint:filename")
|
||||
|
||||
package cash.z.ecc.android.sdk.model
|
||||
|
||||
/*
|
||||
* This is a set of extension functions currently, because we expect them to change in the future.
|
||||
*/
|
||||
|
||||
fun LightWalletEndpoint.Companion.defaultForNetwork(zcashNetwork: ZcashNetwork): LightWalletEndpoint {
|
||||
return when (zcashNetwork.id) {
|
||||
ZcashNetwork.Mainnet.id -> LightWalletEndpoint.Mainnet
|
||||
ZcashNetwork.Testnet.id -> LightWalletEndpoint.Testnet
|
||||
else -> error("Unknown network id: ${zcashNetwork.id}")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a special localhost value on the Android emulator, which allows it to contact
|
||||
* the localhost of the computer running the emulator.
|
||||
*/
|
||||
private const val COMPUTER_LOCALHOST = "10.0.2.2"
|
||||
|
||||
private const val DEFAULT_PORT = 9067
|
||||
|
||||
val LightWalletEndpoint.Companion.Mainnet
|
||||
get() = LightWalletEndpoint(
|
||||
"lite2.hushpool.is",
|
||||
DEFAULT_PORT,
|
||||
isSecure = false
|
||||
)
|
||||
|
||||
val LightWalletEndpoint.Companion.Testnet
|
||||
get() = LightWalletEndpoint(
|
||||
"lite2.hushpool.is",
|
||||
DEFAULT_PORT,
|
||||
isSecure = false
|
||||
)
|
||||
|
||||
val LightWalletEndpoint.Companion.Darkside
|
||||
get() = LightWalletEndpoint(
|
||||
COMPUTER_LOCALHOST,
|
||||
DEFAULT_PORT,
|
||||
isSecure = false
|
||||
)
|
||||
@@ -0,0 +1,28 @@
|
||||
package cash.z.ecc.android.sdk.model
|
||||
|
||||
/**
|
||||
* Data structure to hold the total and available balance of the wallet. This is what is
|
||||
* received on the balance channel.
|
||||
*
|
||||
* @param total the total balance, ignoring funds that cannot be used.
|
||||
* @param available the amount of funds that are available for use. Typical reasons that funds
|
||||
* may be unavailable include fairly new transactions that do not have enough confirmations or
|
||||
* notes that are tied up because we are awaiting change from a transaction. When a note has
|
||||
* been spent, its change cannot be used until there are enough confirmations.
|
||||
*/
|
||||
data class WalletBalance(
|
||||
val total: Zatoshi,
|
||||
val available: Zatoshi
|
||||
) {
|
||||
init {
|
||||
require(total.value >= available.value) { "Wallet total balance must be >= available balance" }
|
||||
}
|
||||
|
||||
val pending = total - available
|
||||
|
||||
operator fun plus(other: WalletBalance): WalletBalance =
|
||||
WalletBalance(
|
||||
total + other.total,
|
||||
available + other.available
|
||||
)
|
||||
}
|
||||
@@ -7,7 +7,7 @@ package cash.z.ecc.android.sdk.model
|
||||
* with ZEC, which is a decimal value represented only as a String. ZEC are not used internally,
|
||||
* to avoid floating point imprecision.
|
||||
*/
|
||||
data class Zatoshi(val value: Long) {
|
||||
data class Zatoshi(val value: Long) : Comparable<Zatoshi> {
|
||||
init {
|
||||
require(value >= MIN_INCLUSIVE) { "Zatoshi must be in the range [$MIN_INCLUSIVE, $MAX_INCLUSIVE]" }
|
||||
require(value <= MAX_INCLUSIVE) { "Zatoshi must be in the range [$MIN_INCLUSIVE, $MAX_INCLUSIVE]" }
|
||||
@@ -16,6 +16,8 @@ data class Zatoshi(val value: Long) {
|
||||
operator fun plus(other: Zatoshi) = Zatoshi(value + other.value)
|
||||
operator fun minus(other: Zatoshi) = Zatoshi(value - other.value)
|
||||
|
||||
override fun compareTo(other: Zatoshi) = value.compareTo(other.value)
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* The number of Zatoshi that equal 1 ZEC.
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package cash.z.ecc.android.sdk.model
|
||||
|
||||
/**
|
||||
* The Zcash network. Should be one of [ZcashNetwork.Testnet] or [ZcashNetwork.Mainnet].
|
||||
*
|
||||
* The constructor for the network is public to allow for certain test cases to use a custom "darkside" network.
|
||||
*/
|
||||
data class ZcashNetwork(
|
||||
val id: Int,
|
||||
val networkName: String,
|
||||
val saplingActivationHeight: BlockHeight
|
||||
) {
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
companion object {
|
||||
const val ID_TESTNET = 0
|
||||
const val ID_MAINNET = 1
|
||||
|
||||
// You may notice there are extra checkpoints bundled in the SDK that match the
|
||||
// sapling/orchard activation heights.
|
||||
|
||||
val Testnet = ZcashNetwork(
|
||||
ID_TESTNET,
|
||||
"testnet",
|
||||
saplingActivationHeight = BlockHeight(1),
|
||||
)
|
||||
|
||||
val Mainnet = ZcashNetwork(
|
||||
ID_MAINNET,
|
||||
"mainnet",
|
||||
saplingActivationHeight = BlockHeight(1),
|
||||
)
|
||||
|
||||
fun from(id: Int) = when (id) {
|
||||
0 -> Testnet
|
||||
1 -> Mainnet
|
||||
else -> throw IllegalArgumentException("Unknown network id: $id")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,10 @@ 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.type.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.BufferedReader
|
||||
@@ -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?
|
||||
): 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,
|
||||
@@ -2,8 +2,8 @@ package cash.z.ecc.android.sdk.tool
|
||||
|
||||
import cash.z.ecc.android.sdk.jni.RustBackend
|
||||
import cash.z.ecc.android.sdk.jni.RustBackendWelding
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.type.UnifiedViewingKey
|
||||
import cash.z.ecc.android.sdk.type.ZcashNetwork
|
||||
|
||||
class DerivationTool {
|
||||
|
||||
|
||||
@@ -1,52 +1,5 @@
|
||||
package cash.z.ecc.android.sdk.type
|
||||
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
|
||||
/**
|
||||
* Data structure to hold the total and available balance of the wallet. This is what is
|
||||
* received on the balance channel.
|
||||
*
|
||||
* @param total the total balance, ignoring funds that cannot be used.
|
||||
* @param available the amount of funds that are available for use. Typical reasons that funds
|
||||
* may be unavailable include fairly new transactions that do not have enough confirmations or
|
||||
* notes that are tied up because we are awaiting change from a transaction. When a note has
|
||||
* been spent, its change cannot be used until there are enough confirmations.
|
||||
*/
|
||||
data class WalletBalance(
|
||||
val total: Zatoshi,
|
||||
val available: Zatoshi
|
||||
) {
|
||||
init {
|
||||
require(total.value >= available.value) { "Wallet total balance must be >= available balance" }
|
||||
}
|
||||
|
||||
val pending = total - available
|
||||
|
||||
operator fun plus(other: WalletBalance): WalletBalance =
|
||||
WalletBalance(
|
||||
total + other.total,
|
||||
available + other.available
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
@@ -70,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", 995_000, "lite.hushpool.is", 9067),
|
||||
Mainnet(1, "mainnet", 995_000, "lite.hushpool.is", 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;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
// Copyright (c) 2019-2020 The Zcash developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or https://www.opensource.org/licenses/mit-license.php .
|
||||
|
||||
syntax = "proto3";
|
||||
package cash.z.wallet.sdk.rpc;
|
||||
option go_package = "walletrpc";
|
||||
|
||||
option go_package = ".;walletrpc";
|
||||
option swift_prefix = "";
|
||||
import "compact_formats.proto";
|
||||
|
||||
// A BlockID message contains identifiers to select a block: a height or a
|
||||
// hash. If the hash is present it takes precedence.
|
||||
// hash. Specification by hash is not implemented, but may be in the future.
|
||||
message BlockID {
|
||||
uint64 height = 1;
|
||||
bytes hash = 2;
|
||||
uint64 height = 1;
|
||||
bytes hash = 2;
|
||||
}
|
||||
|
||||
// BlockRange technically allows ranging from hash to hash etc but this is not
|
||||
// currently intended for support, though there is no reason you couldn't do
|
||||
// it. Further permutations are left as an exercise.
|
||||
// BlockRange specifies a series of blocks from start to end inclusive.
|
||||
// Both BlockIDs must be heights; specification by hash is not yet supported.
|
||||
message BlockRange {
|
||||
BlockID start = 1;
|
||||
BlockID end = 2;
|
||||
@@ -21,73 +24,154 @@ message BlockRange {
|
||||
|
||||
// A TxFilter contains the information needed to identify a particular
|
||||
// transaction: either a block and an index, or a direct transaction hash.
|
||||
// Currently, only specification by hash is supported.
|
||||
message TxFilter {
|
||||
BlockID block = 1;
|
||||
uint64 index = 2;
|
||||
bytes hash = 3;
|
||||
BlockID block = 1; // block identifier, height or hash
|
||||
uint64 index = 2; // index within the block
|
||||
bytes hash = 3; // transaction ID (hash, txid)
|
||||
}
|
||||
|
||||
// RawTransaction contains the complete transaction data. It also optionally includes
|
||||
// the block height in which the transaction was included
|
||||
// RawTransaction contains the complete transaction data. It also optionally includes
|
||||
// the block height in which the transaction was included.
|
||||
message RawTransaction {
|
||||
bytes data = 1;
|
||||
uint64 height = 2;
|
||||
bytes data = 1; // exact data returned by Zcash 'getrawtransaction'
|
||||
uint64 height = 2; // height that the transaction was mined (or -1)
|
||||
}
|
||||
|
||||
// A SendResponse encodes an error code and a string. It is currently used
|
||||
// only by SendTransaction(). If error code is zero, the operation was
|
||||
// successful; if non-zero, it and the message specify the failure.
|
||||
message SendResponse {
|
||||
int32 errorCode = 1;
|
||||
string errorMessage = 2;
|
||||
}
|
||||
|
||||
// Empty placeholder. Someday we may want to specify e.g. a particular chain fork.
|
||||
// Chainspec is a placeholder to allow specification of a particular chain fork.
|
||||
message ChainSpec {}
|
||||
|
||||
// Empty is for gRPCs that take no arguments, currently only GetLightdInfo.
|
||||
message Empty {}
|
||||
|
||||
// LightdInfo returns various information about this lightwalletd instance
|
||||
// and the state of the blockchain.
|
||||
message LightdInfo {
|
||||
string version = 1;
|
||||
string vendor = 2;
|
||||
bool taddrSupport = 3;
|
||||
string chainName = 4;
|
||||
uint64 saplingActivationHeight = 5;
|
||||
string consensusBranchId = 6; // This should really be u32 or []byte, but string for readability
|
||||
uint64 blockHeight = 7;
|
||||
uint64 difficulty = 8;
|
||||
uint64 longestchain = 9;
|
||||
uint64 notarized = 10;
|
||||
}
|
||||
message Coinsupply {
|
||||
string result = 1;
|
||||
string coin = 2;
|
||||
uint64 height = 3;
|
||||
uint64 supply = 4;
|
||||
uint64 zfunds = 5;
|
||||
uint64 total = 6;
|
||||
}
|
||||
|
||||
message TransparentAddress {
|
||||
string address = 1;
|
||||
bool taddrSupport = 3; // true
|
||||
string chainName = 4; // either "main" or "test"
|
||||
uint64 saplingActivationHeight = 5; // depends on mainnet or testnet
|
||||
string consensusBranchId = 6; // protocol identifier, see consensus/upgrades.cpp
|
||||
uint64 blockHeight = 7; // latest block on the best chain
|
||||
string gitCommit = 8;
|
||||
string branch = 9;
|
||||
string buildDate = 10;
|
||||
string buildUser = 11;
|
||||
uint64 estimatedHeight = 12; // less than tip height if zcashd is syncing
|
||||
string zcashdBuild = 13; // example: "v4.1.1-877212414"
|
||||
string zcashdSubversion = 14; // example: "/MagicBean:4.1.1/"
|
||||
}
|
||||
|
||||
// TransparentAddressBlockFilter restricts the results to the given address
|
||||
// or block range.
|
||||
message TransparentAddressBlockFilter {
|
||||
string address = 1; // t-address
|
||||
BlockRange range = 2; // start, end heights
|
||||
}
|
||||
|
||||
// Duration is currently used only for testing, so that the Ping rpc
|
||||
// can simulate a delay, to create many simultaneous connections. Units
|
||||
// are microseconds.
|
||||
message Duration {
|
||||
int64 intervalUs = 1;
|
||||
}
|
||||
|
||||
// PingResponse is used to indicate concurrency, how many Ping rpcs
|
||||
// are executing upon entry and upon exit (after the delay).
|
||||
// This rpc is used for testing only.
|
||||
message PingResponse {
|
||||
int64 entry = 1;
|
||||
int64 exit = 2;
|
||||
}
|
||||
|
||||
message Address {
|
||||
string address = 1;
|
||||
BlockRange range = 2;
|
||||
}
|
||||
message AddressList {
|
||||
repeated string addresses = 1;
|
||||
}
|
||||
message Balance {
|
||||
int64 valueZat = 1;
|
||||
}
|
||||
|
||||
message Exclude {
|
||||
repeated bytes txid = 1;
|
||||
}
|
||||
|
||||
// The TreeState is derived from the Zcash z_gettreestate rpc.
|
||||
message TreeState {
|
||||
string network = 1; // "main" or "test"
|
||||
uint64 height = 2;
|
||||
string hash = 3; // block id
|
||||
uint32 time = 4; // Unix epoch time when the block was mined
|
||||
string tree = 5; // sapling commitment tree state
|
||||
}
|
||||
|
||||
message GetAddressUtxosArg {
|
||||
string address = 1;
|
||||
uint64 startHeight = 2;
|
||||
uint32 maxEntries = 3; // zero means unlimited
|
||||
}
|
||||
message GetAddressUtxosReply {
|
||||
bytes txid = 1;
|
||||
int32 index = 2;
|
||||
bytes script = 3;
|
||||
int64 valueZat = 4;
|
||||
uint64 height = 5;
|
||||
}
|
||||
message GetAddressUtxosReplyList {
|
||||
repeated GetAddressUtxosReply addressUtxos = 1;
|
||||
}
|
||||
|
||||
service CompactTxStreamer {
|
||||
// Compact Blocks
|
||||
// Return the height of the tip of the best chain
|
||||
rpc GetLatestBlock(ChainSpec) returns (BlockID) {}
|
||||
// Return the compact block corresponding to the given block identifier
|
||||
rpc GetBlock(BlockID) returns (CompactBlock) {}
|
||||
// Return a list of consecutive compact blocks
|
||||
rpc GetBlockRange(BlockRange) returns (stream CompactBlock) {}
|
||||
|
||||
// Transactions
|
||||
// Return the requested full (not compact) transaction (as from zcashd)
|
||||
rpc GetTransaction(TxFilter) returns (RawTransaction) {}
|
||||
// Submit the given transaction to the Zcash network
|
||||
rpc SendTransaction(RawTransaction) returns (SendResponse) {}
|
||||
|
||||
// t-Address support
|
||||
rpc GetAddressTxids(TransparentAddressBlockFilter) returns (stream RawTransaction) {}
|
||||
// Return the txids corresponding to the given t-address within the given block range
|
||||
rpc GetTaddressTxids(TransparentAddressBlockFilter) returns (stream RawTransaction) {}
|
||||
rpc GetTaddressBalance(AddressList) returns (Balance) {}
|
||||
rpc GetTaddressBalanceStream(stream Address) returns (Balance) {}
|
||||
|
||||
// Misc
|
||||
// Return the compact transactions currently in the mempool; the results
|
||||
// can be a few seconds out of date. If the Exclude list is empty, return
|
||||
// all transactions; otherwise return all *except* those in the Exclude list
|
||||
// (if any); this allows the client to avoid receiving transactions that it
|
||||
// already has (from an earlier call to this rpc). The transaction IDs in the
|
||||
// Exclude list can be shortened to any number of bytes to make the request
|
||||
// more bandwidth-efficient; if two or more transactions in the mempool
|
||||
// match a shortened txid, they are all sent (none is excluded). Transactions
|
||||
// in the exclude list that don't exist in the mempool are ignored.
|
||||
rpc GetMempoolTx(Exclude) returns (stream CompactTx) {}
|
||||
|
||||
// GetTreeState returns the note commitment tree state corresponding to the given block.
|
||||
// See section 3.7 of the Zcash protocol specification. It returns several other useful
|
||||
// values also (even though they can be obtained using GetBlock).
|
||||
// The block can be specified by either height or hash.
|
||||
rpc GetTreeState(BlockID) returns (TreeState) {}
|
||||
|
||||
rpc GetAddressUtxos(GetAddressUtxosArg) returns (GetAddressUtxosReplyList) {}
|
||||
rpc GetAddressUtxosStream(GetAddressUtxosArg) returns (stream GetAddressUtxosReply) {}
|
||||
|
||||
// Return information about this lightwalletd instance and the blockchain
|
||||
rpc GetLightdInfo(Empty) returns (LightdInfo) {}
|
||||
rpc GetCoinsupply(Empty) returns (Coinsupply) {}
|
||||
}
|
||||
// Testing-only
|
||||
rpc Ping(Duration) returns (PingResponse) {}
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<bool name="lightwalletd_allow_very_insecure_connections">true</bool>
|
||||
</resources>
|
||||
@@ -1,3 +0,0 @@
|
||||
<resources>
|
||||
<string name="sdk_test_message">Library linking is working!</string>
|
||||
</resources>
|
||||
@@ -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(|| {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package cash.z.ecc.android.sdk.ext
|
||||
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.math.BigDecimal
|
||||
@@ -9,7 +10,7 @@ internal class ConversionsTest {
|
||||
|
||||
@Test
|
||||
fun `default right padding is 6`() {
|
||||
assertEquals(1.13.toZec(6), 113000000L.convertZatoshiToZec())
|
||||
assertEquals(1.13.toZec(6), Zatoshi(113000000L).convertZatoshiToZec())
|
||||
assertEquals(1.13.toZec(6), 1.13.toZec())
|
||||
}
|
||||
|
||||
@@ -21,12 +22,12 @@ internal class ConversionsTest {
|
||||
|
||||
@Test
|
||||
fun `toZecString defaults to 6 digits`() {
|
||||
assertEquals("1.123457", 112345678L.convertZatoshiToZecString())
|
||||
assertEquals("1.123457", Zatoshi(112345678L).convertZatoshiToZecString())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toZecString uses banker's rounding`() {
|
||||
assertEquals("1.123456", 112345650L.convertZatoshiToZecString())
|
||||
assertEquals("1.123456", Zatoshi(112345650L).convertZatoshiToZecString())
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -72,7 +73,7 @@ internal class ConversionsTest {
|
||||
|
||||
@Test
|
||||
fun `toZecString zatoshi converts`() {
|
||||
assertEquals("1.123456", 112345650L.convertZatoshiToZecString(6, 0))
|
||||
assertEquals("1.123456", Zatoshi(112345650L).convertZatoshiToZecString(6, 0))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -3,6 +3,7 @@ package cash.z.ecc.android.sdk.model
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class ZatoshiTest {
|
||||
@Test
|
||||
@@ -29,6 +30,21 @@ class ZatoshiTest {
|
||||
assertEquals(Zatoshi(3), Zatoshi(4) - Zatoshi(1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun compare_equal() {
|
||||
assertEquals(0, Zatoshi(1).compareTo(Zatoshi(1)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun compare_greater() {
|
||||
assertTrue(Zatoshi(2) > Zatoshi(1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun compare_less() {
|
||||
assertTrue(Zatoshi(1) < Zatoshi(2))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun minus_fail() {
|
||||
assertFailsWith<IllegalArgumentException> {
|
||||
|
||||
Reference in New Issue
Block a user