From 3b826f8f6a4a34a62fbe723ca35aee1091bc221a Mon Sep 17 00:00:00 2001 From: Carter Jernigan Date: Thu, 6 Oct 2022 13:48:21 -0400 Subject: [PATCH] Simplify Synchronizer instantiation fix synchronizer --- .../android/sdk/darkside/test/TestWallet.kt | 17 +- .../sdk/sample/demoapp/SampleCodeTest.kt | 13 +- .../demos/getaddress/GetAddressFragment.kt | 22 +- .../demos/getbalance/GetBalanceFragment.kt | 32 +- .../ListTransactionsFragment.kt | 26 +- .../demos/listutxos/ListUtxosFragment.kt | 26 +- .../sdk/demoapp/demos/send/SendFragment.kt | 23 +- .../z/ecc/android/sdk/ext/TestExtensions.kt | 8 - .../ecc/android/sdk/integration/SanityTest.kt | 36 +- .../ecc/android/sdk/integration/SmokeTest.kt | 32 +- .../sdk/integration/TestnetIntegrationTest.kt | 25 +- .../android/sdk/util/BalancePrinterUtil.kt | 31 +- .../ecc/android/sdk/util/DataDbScannerUtil.kt | 35 +- .../cash/z/ecc/android/sdk/util/TestWallet.kt | 22 +- .../cash/z/ecc/android/sdk/Initializer.kt | 434 ------------------ .../cash/z/ecc/android/sdk/SdkSynchronizer.kt | 86 ++-- .../cash/z/ecc/android/sdk/Synchronizer.kt | 124 ++++- .../android/sdk/internal/db/DerivedDataDb.kt | 1 - .../transaction/TransactionRepository.kt | 1 - 19 files changed, 310 insertions(+), 684 deletions(-) delete mode 100644 sdk-lib/src/main/java/cash/z/ecc/android/sdk/Initializer.kt diff --git a/darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/test/TestWallet.kt b/darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/test/TestWallet.kt index e510590d..5bbaeaf6 100644 --- a/darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/test/TestWallet.kt +++ b/darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/test/TestWallet.kt @@ -3,7 +3,6 @@ package cash.z.ecc.android.sdk.darkside.test import androidx.test.platform.app.InstrumentationRegistry import cash.z.ecc.android.bip39.Mnemonics import cash.z.ecc.android.bip39.toSeed -import cash.z.ecc.android.sdk.Initializer import cash.z.ecc.android.sdk.SdkSynchronizer import cash.z.ecc.android.sdk.Synchronizer import cash.z.ecc.android.sdk.db.entity.isPending @@ -67,12 +66,16 @@ class TestWallet( runBlocking { DerivationTool.deriveSpendingKeys(seed, network = network)[0] } private val transparentAccountPrivateKey = runBlocking { DerivationTool.deriveTransparentAccountPrivateKey(seed, network = network) } - val initializer = runBlocking { - Initializer.new(context) { config -> - runBlocking { config.importWallet(seed, startHeight, network, endpoint, alias = alias) } - } - } - val synchronizer: SdkSynchronizer = runBlocking { Synchronizer.new(initializer) } as SdkSynchronizer + val synchronizer: SdkSynchronizer = Synchronizer.newBlocking( + context, + network, + alias, + endpoint, + seed, + startHeight + ) + as + SdkSynchronizer val service = (synchronizer.processor.downloader.lightWalletService as LightWalletGrpcService) val available get() = synchronizer.saplingBalances.value?.available diff --git a/demo-app/src/androidTest/java/cash/z/wallet/sdk/sample/demoapp/SampleCodeTest.kt b/demo-app/src/androidTest/java/cash/z/wallet/sdk/sample/demoapp/SampleCodeTest.kt index eaca74e9..e32b0f48 100644 --- a/demo-app/src/androidTest/java/cash/z/wallet/sdk/sample/demoapp/SampleCodeTest.kt +++ b/demo-app/src/androidTest/java/cash/z/wallet/sdk/sample/demoapp/SampleCodeTest.kt @@ -1,9 +1,9 @@ package cash.z.wallet.sdk.sample.demoapp import androidx.test.platform.app.InstrumentationRegistry -import cash.z.ecc.android.sdk.Initializer import cash.z.ecc.android.sdk.Synchronizer import cash.z.ecc.android.sdk.db.entity.isFailure +import cash.z.ecc.android.sdk.demoapp.util.fromResources import cash.z.ecc.android.sdk.ext.convertZecToZatoshi import cash.z.ecc.android.sdk.ext.toHex import cash.z.ecc.android.sdk.internal.TroubleshootingTwig @@ -14,6 +14,7 @@ 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 cash.z.ecc.android.sdk.model.defaultForNetwork import cash.z.ecc.android.sdk.tool.DerivationTool import kotlinx.coroutines.flow.collect import kotlinx.coroutines.runBlocking @@ -160,8 +161,14 @@ class SampleCodeTest { private val context = InstrumentationRegistry.getInstrumentation().targetContext private val synchronizer: Synchronizer = run { - val initializer = runBlocking { Initializer.new(context) {} } - Synchronizer.newBlocking(initializer) + val network = ZcashNetwork.fromResources(context) + Synchronizer.newBlocking( + context, + network, + lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network), + seed = seed, + birthday = null + ) } @BeforeClass diff --git a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getaddress/GetAddressFragment.kt b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getaddress/GetAddressFragment.kt index f4df9d0b..9813de7d 100644 --- a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getaddress/GetAddressFragment.kt +++ b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getaddress/GetAddressFragment.kt @@ -5,7 +5,6 @@ import android.view.LayoutInflater import androidx.lifecycle.lifecycleScope import cash.z.ecc.android.bip39.Mnemonics import cash.z.ecc.android.bip39.toSeed -import cash.z.ecc.android.sdk.Initializer import cash.z.ecc.android.sdk.Synchronizer import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment import cash.z.ecc.android.sdk.demoapp.databinding.FragmentGetAddressBinding @@ -49,19 +48,14 @@ class GetAddressFragment : BaseDemoFragment() { ).first() } - // using the ViewingKey to initialize - runBlocking { - Initializer.new(requireApplicationContext(), null) { - val network = ZcashNetwork.fromResources(requireApplicationContext()) - it.newWallet( - viewingKey, - network = network, - lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network) - ) - } - }.let { initializer -> - synchronizer = Synchronizer.newBlocking(initializer) - } + val network = ZcashNetwork.fromResources(requireApplicationContext()) + synchronizer = Synchronizer.newBlocking( + requireApplicationContext(), + network, + lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network), + seed = seed, + birthday = network.saplingActivationHeight + ) } private fun displayAddress() { diff --git a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getbalance/GetBalanceFragment.kt b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getbalance/GetBalanceFragment.kt index 94dfa413..07efd4c4 100644 --- a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getbalance/GetBalanceFragment.kt +++ b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getbalance/GetBalanceFragment.kt @@ -5,7 +5,6 @@ import android.view.LayoutInflater import androidx.lifecycle.lifecycleScope import cash.z.ecc.android.bip39.Mnemonics import cash.z.ecc.android.bip39.toSeed -import cash.z.ecc.android.sdk.Initializer import cash.z.ecc.android.sdk.Synchronizer import cash.z.ecc.android.sdk.block.CompactBlockProcessor import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment @@ -18,9 +17,7 @@ import cash.z.ecc.android.sdk.model.LightWalletEndpoint import cash.z.ecc.android.sdk.model.WalletBalance import cash.z.ecc.android.sdk.model.ZcashNetwork import cash.z.ecc.android.sdk.model.defaultForNetwork -import cash.z.ecc.android.sdk.tool.DerivationTool import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.runBlocking /** * Displays the available balance && total balance associated with the seed defined by the default config. @@ -46,27 +43,14 @@ class GetBalanceFragment : BaseDemoFragment() { // have the seed stored val seed = Mnemonics.MnemonicCode(seedPhrase).toSeed() - // converting seed into viewingKey - val viewingKey = runBlocking { - DerivationTool.deriveUnifiedFullViewingKeys( - seed, - ZcashNetwork.fromResources(requireApplicationContext()) - ).first() - } - - // using the ViewingKey to initialize - runBlocking { - Initializer.new(requireApplicationContext(), null) { - val network = ZcashNetwork.fromResources(requireApplicationContext()) - it.newWallet( - viewingKey, - network = network, - lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network) - ) - } - }.let { initializer -> - synchronizer = Synchronizer.newBlocking(initializer, seed) - } + val network = ZcashNetwork.fromResources(requireApplicationContext()) + synchronizer = Synchronizer.newBlocking( + requireApplicationContext(), + network, + lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network), + seed = seed, + birthday = null + ) } override fun onResume() { diff --git a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listtransactions/ListTransactionsFragment.kt b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listtransactions/ListTransactionsFragment.kt index 1bfb97cf..8721f964 100644 --- a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listtransactions/ListTransactionsFragment.kt +++ b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listtransactions/ListTransactionsFragment.kt @@ -8,7 +8,6 @@ import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import cash.z.ecc.android.bip39.Mnemonics import cash.z.ecc.android.bip39.toSeed -import cash.z.ecc.android.sdk.Initializer import cash.z.ecc.android.sdk.Synchronizer import cash.z.ecc.android.sdk.block.CompactBlockProcessor import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction @@ -33,7 +32,6 @@ import kotlinx.coroutines.runBlocking */ @Suppress("TooManyFunctions") class ListTransactionsFragment : BaseDemoFragment() { - private lateinit var initializer: Initializer private lateinit var synchronizer: Synchronizer private lateinit var adapter: TransactionAdapter private lateinit var address: String @@ -51,28 +49,20 @@ class ListTransactionsFragment : BaseDemoFragment() { private lateinit var seed: ByteArray - private lateinit var initializer: Initializer private lateinit var synchronizer: Synchronizer private lateinit var adapter: UtxoAdapter private val address: String = "t1RwbKka1CnktvAJ1cSqdn7c6PXWG4tZqgd" @@ -66,20 +63,15 @@ class ListUtxosFragment : BaseDemoFragment() { // Use a BIP-39 library to convert a seed phrase into a byte array. Most wallets already // have the seed stored seed = Mnemonics.MnemonicCode(sharedViewModel.seedPhrase.value).toSeed() - initializer = runBlocking { - val network = ZcashNetwork.fromResources(requireApplicationContext()) - Initializer.new(requireApplicationContext()) { - runBlocking { - it.newWallet( - seed, - network = network, - lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network) - ) - } - it.alias = "Demo_Utxos" - } - } - synchronizer = runBlocking { Synchronizer.new(initializer, seed) } + val network = ZcashNetwork.fromResources(requireApplicationContext()) + synchronizer = Synchronizer.newBlocking( + requireApplicationContext(), + network, + alias = "Demo_Utxos", + lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network), + seed = seed, + birthday = null + ) } override fun onCreate(savedInstanceState: Bundle?) { diff --git a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/send/SendFragment.kt b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/send/SendFragment.kt index ab8cf2ed..b386c3ac 100644 --- a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/send/SendFragment.kt +++ b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/send/SendFragment.kt @@ -8,7 +8,6 @@ import android.widget.TextView import androidx.lifecycle.lifecycleScope import cash.z.ecc.android.bip39.Mnemonics import cash.z.ecc.android.bip39.toSeed -import cash.z.ecc.android.sdk.Initializer import cash.z.ecc.android.sdk.Synchronizer import cash.z.ecc.android.sdk.block.CompactBlockProcessor import cash.z.ecc.android.sdk.db.entity.PendingTransaction @@ -68,20 +67,14 @@ class SendFragment : BaseDemoFragment() { // have the seed stored val seed = Mnemonics.MnemonicCode(seedPhrase).toSeed() - runBlocking { - Initializer.new(requireApplicationContext()) { - val network = ZcashNetwork.fromResources(requireApplicationContext()) - runBlocking { - it.newWallet( - seed, - network = network, - lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network) - ) - } - } - }.let { initializer -> - synchronizer = Synchronizer.newBlocking(initializer, seed) - } + val network = ZcashNetwork.fromResources(requireApplicationContext()) + synchronizer = Synchronizer.newBlocking( + requireApplicationContext(), + network, + lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network), + seed = seed, + birthday = null + ) spendingKey = runBlocking { DerivationTool.deriveSpendingKeys(seed, ZcashNetwork.fromResources(requireApplicationContext())).first() } diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/ext/TestExtensions.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/ext/TestExtensions.kt index 4bb1340f..4709743b 100644 --- a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/ext/TestExtensions.kt +++ b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/ext/TestExtensions.kt @@ -1,19 +1,11 @@ package cash.z.ecc.android.sdk.ext -import cash.z.ecc.android.sdk.Initializer -import cash.z.ecc.android.sdk.model.ZcashNetwork -import cash.z.ecc.android.sdk.util.SimpleMnemonics -import kotlinx.coroutines.runBlocking import okhttp3.OkHttpClient import okhttp3.Request import org.json.JSONObject import ru.gildor.coroutines.okhttp.await import kotlin.test.assertNotNull -fun Initializer.Config.seedPhrase(seedPhrase: String, network: ZcashNetwork) { - runBlocking { setSeed(SimpleMnemonics().toSeed(seedPhrase.toCharArray()), network) } -} - object BlockExplorer { suspend fun fetchLatestHeight(): Long { val client = OkHttpClient() diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/integration/SanityTest.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/integration/SanityTest.kt index 6fcbbf88..bf4f2864 100644 --- a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/integration/SanityTest.kt +++ b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/integration/SanityTest.kt @@ -1,5 +1,7 @@ package cash.z.ecc.android.sdk.integration +import androidx.test.core.app.ApplicationProvider +import cash.z.ecc.android.sdk.DefaultSynchronizerFactory import cash.z.ecc.android.sdk.annotation.MaintainedTest import cash.z.ecc.android.sdk.annotation.TestPurpose import cash.z.ecc.android.sdk.db.DatabaseCoordinator @@ -13,7 +15,6 @@ import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized -import kotlin.test.DefaultAsserter.assertEquals import kotlin.test.DefaultAsserter.assertTrue // TODO [#650]: https://github.com/zcash/zcash-android-wallet-sdk/issues/650 @@ -36,44 +37,35 @@ class SanityTest( @Test fun testFilePaths() { + val rustBackend = runBlocking { + DefaultSynchronizerFactory.defaultRustBackend( + ApplicationProvider.getApplicationContext(), + ZcashNetwork.Testnet, + "TestWallet", + TestWallet.Backups.SAMPLE_WALLET.testnetBirthday + ) + } + assertTrue( "$name has invalid DataDB file", - wallet.initializer.rustBackend.dataDbFile.absolutePath.endsWith( + rustBackend.dataDbFile.absolutePath.endsWith( "no_backup/co.electricoin.zcash/TestWallet_${networkName}_${DatabaseCoordinator.DB_DATA_NAME}" ) ) assertTrue( "$name has invalid CacheDB file", - wallet.initializer.rustBackend.cacheDbFile.absolutePath.endsWith( + rustBackend.cacheDbFile.absolutePath.endsWith( "no_backup/co.electricoin.zcash/TestWallet_${networkName}_${DatabaseCoordinator.DB_CACHE_NAME}" ) ) assertTrue( "$name has invalid CacheDB params dir", - wallet.initializer.rustBackend.pathParamsDir.endsWith( + rustBackend.pathParamsDir.endsWith( "cache/params" ) ) } - @Test - fun testBirthday() { - assertEquals( - "$name has invalid birthday height", - birthday, - wallet.initializer.checkpoint.height - ) - } - - @Test - fun testViewingKeys() { - assertEquals( - "$name has invalid encoding", - encoding, - wallet.initializer.viewingKeys[0].encoding - ) - } - @Test @Ignore( "This test needs to be refactored to a separate test module. It causes SSLHandshakeException: Chain " + diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/integration/SmokeTest.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/integration/SmokeTest.kt index c9475a41..33465023 100644 --- a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/integration/SmokeTest.kt +++ b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/integration/SmokeTest.kt @@ -1,13 +1,15 @@ package cash.z.ecc.android.sdk.integration +import androidx.test.core.app.ApplicationProvider import androidx.test.filters.LargeTest import androidx.test.filters.MediumTest +import cash.z.ecc.android.sdk.DefaultSynchronizerFactory import cash.z.ecc.android.sdk.annotation.MaintainedTest import cash.z.ecc.android.sdk.annotation.TestPurpose import cash.z.ecc.android.sdk.db.DatabaseCoordinator +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.assertTrue import org.junit.Ignore import org.junit.Test @@ -22,38 +24,32 @@ class SmokeTest { @Test fun testFilePaths() { + val rustBackend = runBlocking { + DefaultSynchronizerFactory.defaultRustBackend( + ApplicationProvider.getApplicationContext(), + ZcashNetwork.Testnet, + "TestWallet", + TestWallet.Backups.SAMPLE_WALLET.testnetBirthday + ) + } assertTrue( "Invalid DataDB file", - wallet.initializer.rustBackend.dataDbFile.absolutePath.endsWith( + rustBackend.dataDbFile.absolutePath.endsWith( "no_backup/co.electricoin.zcash/TestWallet_testnet_${DatabaseCoordinator.DB_DATA_NAME}" ) ) assertTrue( "Invalid CacheDB file", - wallet.initializer.rustBackend.cacheDbFile.absolutePath.endsWith( + rustBackend.cacheDbFile.absolutePath.endsWith( "no_backup/co.electricoin.zcash/TestWallet_testnet_${DatabaseCoordinator.DB_CACHE_NAME}" ) ) assertTrue( "Invalid CacheDB params dir", - wallet.initializer.rustBackend.pathParamsDir.endsWith("cache/params") + rustBackend.pathParamsDir.endsWith("cache/params") ) } - @Test - fun testBirthday() { - assertEquals( - "Invalid birthday height", - 1_330_000, - wallet.initializer.checkpoint.height.value - ) - } - - @Test - fun testViewingKeys() { - assertEquals("Invalid encoding", "uviewtest1m3cyp6tdy3rewtpqazdxlsqkmu7xjedtqmp4da8mvxm87h4as38v5kz4ulw7x7nmgv5d8uwk743a5zt7aurtz2z2g74fu740ecp5fhdgakm6hgzr5jzcl75cmddlufmjpykrpkzj84yz8j5qe9c5935qt2tvd9dpx3m0zw5dwn3t2dtsdyqvy5jstf88w799qre549yyxw7dvk3murm3568ah6wqg5tdjka2ujtgct4q62hw7mfcxcyaeu8l6882hxkt9x4025mx3w35whcrmpxy8fqsh62esatczj8awxtrgnj8h2vj65r8595qt9jl4gz84w4mja74tymt8xxaguckeam", wallet.initializer.viewingKeys[0].encoding) - } - // This test takes an extremely long time // Does its runtime grow over time based on growth of the blockchain? @Test diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/integration/TestnetIntegrationTest.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/integration/TestnetIntegrationTest.kt index 61f645f4..7f70c523 100644 --- a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/integration/TestnetIntegrationTest.kt +++ b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/integration/TestnetIntegrationTest.kt @@ -2,7 +2,6 @@ package cash.z.ecc.android.sdk.integration import androidx.test.filters.LargeTest import androidx.test.platform.app.InstrumentationRegistry -import cash.z.ecc.android.sdk.Initializer import cash.z.ecc.android.sdk.Synchronizer import cash.z.ecc.android.sdk.Synchronizer.Status.SYNCED import cash.z.ecc.android.sdk.db.entity.isSubmitSuccess @@ -63,7 +62,7 @@ class TestnetIntegrationTest : ScopedTest() { @Test @Ignore("This test is broken") fun getAddress() = runBlocking { - assertEquals(address, synchronizer.getAddress()) + assertEquals(address, synchronizer.getCurrentAddress()) } // This is an extremely slow test; it is disabled so that we can get CI set up @@ -117,29 +116,33 @@ class TestnetIntegrationTest : ScopedTest() { } companion object { - init { Twig.plant(TroubleshootingTwig()) } + init { + Twig.plant(TroubleshootingTwig()) + } 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" + 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() val address = "zs1m30y59wxut4zk9w24d6ujrdnfnl42hpy0ugvhgyhr8s0guszutqhdj05c7j472dndjstulph74m" val toAddress = "zs1vp7kvlqr4n9gpehztr76lcn6skkss9p8keqs3nv8avkdtjrcctrvmk9a7u494kluv756jeee5k0" private val context = InstrumentationRegistry.getInstrumentation().context - private val initializer = runBlocking { - Initializer.new(context) { config -> - config.setNetwork(ZcashNetwork.Testnet, lightWalletEndpoint) - runBlocking { config.importWallet(seed, BlockHeight.new(ZcashNetwork.Testnet, birthdayHeight), ZcashNetwork.Testnet, lightWalletEndpoint) } - } - } private lateinit var synchronizer: Synchronizer @JvmStatic @BeforeClass fun startUp() { - synchronizer = Synchronizer.newBlocking(initializer) + synchronizer = Synchronizer.newBlocking( + context, + ZcashNetwork.Testnet, + lightWalletEndpoint = + lightWalletEndpoint, + seed = seed, + birthday = BlockHeight.new(ZcashNetwork.Testnet, birthdayHeight) + ) synchronizer.start(classScope) } } diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/BalancePrinterUtil.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/BalancePrinterUtil.kt index 782bdc38..fa5e2828 100644 --- a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/BalancePrinterUtil.kt +++ b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/BalancePrinterUtil.kt @@ -1,7 +1,6 @@ package cash.z.ecc.android.sdk.util import androidx.test.platform.app.InstrumentationRegistry -import cash.z.ecc.android.sdk.Initializer import cash.z.ecc.android.sdk.Synchronizer import cash.z.ecc.android.sdk.internal.TroubleshootingTwig import cash.z.ecc.android.sdk.internal.Twig @@ -14,11 +13,9 @@ 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.asFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.runBlocking -import okio.buffer -import okio.source import org.junit.Before import org.junit.Ignore import org.junit.Test @@ -83,11 +80,7 @@ class BalancePrinterUtil { mnemonics.toSeed(seedPhrase.toCharArray()) }.collect { seed -> // TODO: clear the dataDb but leave the cacheDb - val initializer = Initializer.new(context) { config -> - val endpoint = LightWalletEndpoint.defaultForNetwork(network) - runBlocking { config.importWallet(seed, birthdayHeight, network, endpoint) } - config.alias = alias - } + /* what I need to do right now - for each seed @@ -103,7 +96,14 @@ class BalancePrinterUtil { - can we be more stateless and thereby improve the flexibility of this code?!!! */ synchronizer?.stop() - synchronizer = Synchronizer.new(initializer).apply { + synchronizer = Synchronizer.new( + context, + network, + lightWalletEndpoint = LightWalletEndpoint + .defaultForNetwork(network), + seed = seed, + birthday = birthdayHeight + ).apply { start() } @@ -142,16 +142,7 @@ class BalancePrinterUtil { // } @Throws(IOException::class) - fun readLines() = flow { - val seedFile = javaClass.getResourceAsStream("/utils/seeds.txt")!! - seedFile.source().buffer().use { source -> - var line: String? = source.readUtf8Line() - while (line != null) { - emit(line) - line = source.readUtf8Line() - } - } - } + fun readLines() = javaClass.getResourceAsStream("/utils/seeds.txt")!!.bufferedReader().lineSequence().asFlow() // private fun initWallet(seed: String): Wallet { // val spendingKeyProvider = Delegates.notNull() diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/DataDbScannerUtil.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/DataDbScannerUtil.kt index 94603bc5..25ce54d0 100644 --- a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/DataDbScannerUtil.kt +++ b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/DataDbScannerUtil.kt @@ -1,13 +1,14 @@ package cash.z.ecc.android.sdk.util import androidx.test.platform.app.InstrumentationRegistry -import cash.z.ecc.android.sdk.Initializer 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.LightWalletEndpoint import cash.z.ecc.android.sdk.model.ZcashNetwork +import cash.z.ecc.android.sdk.model.defaultForNetwork import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @@ -65,27 +66,17 @@ class DataDbScannerUtil { @Test @Ignore("This test is broken") fun scanExistingDb() { - synchronizer = run { - val initializer = runBlocking { - Initializer.new(context) { - it.setBirthdayHeight( - BlockHeight.new( - ZcashNetwork.Mainnet, - birthdayHeight - ), - false - ) - } - } - - val synchronizer = runBlocking { - Synchronizer.new( - initializer - ) - } - - synchronizer - } + synchronizer = Synchronizer.newBlocking( + context, + ZcashNetwork.Mainnet, + lightWalletEndpoint = LightWalletEndpoint + .defaultForNetwork(ZcashNetwork.Mainnet), + seed = byteArrayOf(), + birthday = BlockHeight.new( + ZcashNetwork.Mainnet, + birthdayHeight + ) + ) println("sync!") synchronizer.start() diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/TestWallet.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/TestWallet.kt index 8346d0ec..d36f5f13 100644 --- a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/TestWallet.kt +++ b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/TestWallet.kt @@ -3,7 +3,6 @@ package cash.z.ecc.android.sdk.util import androidx.test.platform.app.InstrumentationRegistry import cash.z.ecc.android.bip39.Mnemonics import cash.z.ecc.android.bip39.toSeed -import cash.z.ecc.android.sdk.Initializer import cash.z.ecc.android.sdk.SdkSynchronizer import cash.z.ecc.android.sdk.Synchronizer import cash.z.ecc.android.sdk.db.entity.isPending @@ -67,12 +66,14 @@ class TestWallet( runBlocking { DerivationTool.deriveSpendingKeys(seed, network = network)[0] } private val transparentAccountPrivateKey = runBlocking { DerivationTool.deriveTransparentAccountPrivateKey(seed, network = network) } - val initializer = runBlocking { - Initializer.new(context) { config -> - runBlocking { config.importWallet(seed, startHeight, network, endpoint, alias = alias) } - } - } - val synchronizer: SdkSynchronizer = Synchronizer.newBlocking(initializer) as SdkSynchronizer + val synchronizer: SdkSynchronizer = Synchronizer.newBlocking( + context, + network, + alias, + lightWalletEndpoint = endpoint, + seed = seed, + startHeight + ) as SdkSynchronizer val service = (synchronizer.processor.downloader.lightWalletService as LightWalletGrpcService) val available get() = synchronizer.saplingBalances.value?.available @@ -109,7 +110,12 @@ class TestWallet( return this } - suspend fun send(address: String = transparentAddress, memo: String = "", amount: Zatoshi = Zatoshi(500L), fromAccountIndex: Int = 0): 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(null) } diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/Initializer.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/Initializer.kt deleted file mode 100644 index a539da74..00000000 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/Initializer.kt +++ /dev/null @@ -1,434 +0,0 @@ -package cash.z.ecc.android.sdk - -import android.content.Context -import cash.z.ecc.android.sdk.db.DatabaseCoordinator -import cash.z.ecc.android.sdk.exception.InitializerException -import cash.z.ecc.android.sdk.ext.ZcashSdk -import cash.z.ecc.android.sdk.internal.ext.getCacheDirSuspend -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.type.UnifiedFullViewingKey -import kotlinx.coroutines.runBlocking -import java.io.File - -/** - * Simplified Initializer focused on starting from a ViewingKey. - */ -@Suppress("LongParameterList", "unused") -class Initializer private constructor( - val context: Context, - internal val rustBackend: RustBackend, - val network: ZcashNetwork, - val alias: String, - val lightWalletEndpoint: LightWalletEndpoint, - val viewingKeys: List, - val overwriteVks: Boolean, - internal val checkpoint: Checkpoint -) { - - suspend fun erase() = erase(context, network, alias) - - @Suppress("TooManyFunctions") - class Config private constructor( - val viewingKeys: MutableList = mutableListOf(), - var alias: String = ZcashSdk.DEFAULT_ALIAS - ) { - var birthdayHeight: BlockHeight? = null - private set - - lateinit var network: ZcashNetwork - private set - - lateinit var lightWalletEndpoint: LightWalletEndpoint - private set - - /** - * Determines the default behavior for null birthdays. When null, nothing has been specified - * so a null birthdayHeight value is an error. When false, null birthdays will be replaced - * with the most recent checkpoint height available (typically, the latest `*.json` file in - * `assets/co.electriccoin.zcash/checkpoint/`). When true, null birthdays will be replaced with the oldest - * reasonable height where a transaction could exist (typically, sapling activation but - * better approximations could be devised in the future, such as the date when the first - * BIP-39 zcash wallets came online). - */ - var defaultToOldestHeight: Boolean? = null - private set - - var overwriteVks: Boolean = false - private set - - constructor(block: (Config) -> Unit) : this() { - block(this) - } - - // - // Birthday functions - // - - /** - * Set the birthday height for this configuration. When the height is not known, the wallet - * can either default to the latest known birthday (in order to sync new wallets faster) or - * the oldest possible birthday (in order to import a wallet with an unknown birthday - * without skipping old transactions). - * - * @param height nullable birthday height to use for this configuration. - * @param defaultToOldestHeight determines how a null birthday height will be - * interpreted. Typically, `false` for new wallets and `true` for restored wallets because - * new wallets want to load quickly but restored wallets want to find all possible - * transactions. Again, this value is only considered when [height] is null. - * - */ - fun setBirthdayHeight(height: BlockHeight?, defaultToOldestHeight: Boolean): Config = - apply { - this.birthdayHeight = height - this.defaultToOldestHeight = defaultToOldestHeight - } - - /** - * Load the most recent checkpoint available. This is useful for new wallets. - */ - fun newWalletBirthday(): Config = apply { - birthdayHeight = null - defaultToOldestHeight = false - } - - /** - * Load the birthday checkpoint closest to the given wallet birthday. This is useful when - * importing a pre-existing wallet. It is the same as calling - * `birthdayHeight = importedHeight`. - */ - fun importedWalletBirthday(importedHeight: BlockHeight?): Config = apply { - birthdayHeight = importedHeight - defaultToOldestHeight = true - } - - // - // Viewing key functions - // - - /** - * Add viewing keys to the set of accounts to monitor. Note: Using more than one viewing key - * is not currently well supported. Consider it an alpha-preview feature that might work but - * probably has serious bugs. - */ - fun setViewingKeys( - vararg unifiedFullViewingKeys: UnifiedFullViewingKey, - overwrite: Boolean = false - ): Config = apply { - overwriteVks = overwrite - viewingKeys.apply { - clear() - addAll(unifiedFullViewingKeys) - } - } - - fun setOverwriteKeys(isOverwrite: Boolean) { - overwriteVks = isOverwrite - } - - /** - * Add viewing key to the set of accounts to monitor. Note: Using more than one viewing key - * is not currently well supported. Consider it an alpha-preview feature that might work but - * probably has serious bugs. - */ - fun addViewingKey(unifiedFullViewingKey: UnifiedFullViewingKey): Config = apply { - viewingKeys.add(unifiedFullViewingKey) - } - - // - // Convenience functions - // - - /** - * Set the server and the network property at the same time to prevent them from getting out - * of sync. Ultimately, this determines which host a synchronizer will use in order to - * connect to lightwalletd. - * - * @param network the Zcash network to use. Either testnet or mainnet. - * @param lightWalletEndpoint the light wallet endpoint to use. - */ - fun setNetwork( - network: ZcashNetwork, - lightWalletEndpoint: LightWalletEndpoint - ): Config = apply { - this.network = network - this.lightWalletEndpoint = lightWalletEndpoint - } - - /** - * Import a wallet using the first viewing key derived from the given seed. - */ - suspend fun importWallet( - seed: ByteArray, - birthday: BlockHeight?, - network: ZcashNetwork, - lightWalletEndpoint: LightWalletEndpoint, - alias: String = ZcashSdk.DEFAULT_ALIAS - ): Config = - importWallet( - DerivationTool.deriveUnifiedFullViewingKeys(seed, network = network)[0], - birthday, - network, - lightWalletEndpoint, - alias - ) - - /** - * Default function for importing a wallet. - */ - @Suppress("LongParameterList") - fun importWallet( - viewingKey: UnifiedFullViewingKey, - birthday: BlockHeight?, - network: ZcashNetwork, - lightWalletEndpoint: LightWalletEndpoint, - alias: String = ZcashSdk.DEFAULT_ALIAS - ): Config = apply { - setViewingKeys(viewingKey) - setNetwork(network, lightWalletEndpoint) - importedWalletBirthday(birthday) - this.alias = alias - } - - /** - * Create a new wallet using the first viewing key derived from the given seed. - */ - suspend fun newWallet( - seed: ByteArray, - network: ZcashNetwork, - lightWalletEndpoint: LightWalletEndpoint, - alias: String = ZcashSdk.DEFAULT_ALIAS - ): Config = newWallet( - DerivationTool.deriveUnifiedFullViewingKeys(seed, network)[0], - network, - lightWalletEndpoint, - alias - ) - - /** - * Default function for creating a new wallet. - */ - fun newWallet( - viewingKey: UnifiedFullViewingKey, - network: ZcashNetwork, - lightWalletEndpoint: LightWalletEndpoint, - alias: String = ZcashSdk.DEFAULT_ALIAS - ): Config = apply { - setViewingKeys(viewingKey) - setNetwork(network, lightWalletEndpoint) - newWalletBirthday() - this.alias = alias - } - - /** - * Convenience method for setting thew viewingKeys from a given seed. This is the same as - * calling `setViewingKeys` with the keys that match this seed. - */ - @Suppress("SpreadOperator") - suspend fun setSeed( - seed: ByteArray, - network: ZcashNetwork, - numberOfAccounts: Int = 1 - ): Config = - apply { - @Suppress("SpreadOperator") - setViewingKeys( - *DerivationTool.deriveUnifiedFullViewingKeys( - seed, - network, - numberOfAccounts - ) - ) - } - - /** - * Sets the network from a network id, throwing an exception if the id is not recognized. - * - * @param networkId the ID of the network corresponding to the [ZcashNetwork] enum. - * Typically, it is 0 for testnet and 1 for mainnet. - */ - fun setNetworkId(networkId: Int): Config = apply { - network = ZcashNetwork.from(networkId) - } - - // - // Validation helpers - // - - fun validate(): Config = apply { - validateAlias(alias) - validateViewingKeys() - validateBirthday() - } - - private fun validateBirthday() { - // if birthday is missing then we need to know how to interpret it - // so defaultToOldestHeight ought to be set, in that case - if (birthdayHeight == null && defaultToOldestHeight == null) { - throw InitializerException.MissingDefaultBirthdayException - } - // allow either null or a value greater than the activation height - if ( - (birthdayHeight?.value ?: network.saplingActivationHeight.value) - < network.saplingActivationHeight.value - ) { - throw InitializerException.InvalidBirthdayHeightException(birthdayHeight, network) - } - } - - private fun validateViewingKeys() { - require(viewingKeys.isNotEmpty()) { - "Unified Viewing keys are required. Ensure that the unified viewing keys or seed" + - " have been set on this Initializer." - } - viewingKeys.forEach { - DerivationTool.validateUnifiedFullViewingKey(it) - } - } - - companion object - } - - companion object : SdkSynchronizer.Erasable { - - suspend fun new(appContext: Context, config: Config) = new(appContext, null, config) - - fun newBlocking(appContext: Context, config: Config) = runBlocking { - new( - appContext, - null, - config - ) - } - - suspend fun new( - appContext: Context, - onCriticalErrorHandler: ((Throwable?) -> Boolean)? = null, - block: (Config) -> Unit - ) = new(appContext, onCriticalErrorHandler, Config(block)) - - @Suppress("UNUSED_PARAMETER") - suspend fun new( - context: Context, - onCriticalErrorHandler: ((Throwable?) -> Boolean)?, - config: Config - ): Initializer { - config.validate() - - 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.lightWalletEndpoint, - config.viewingKeys, - config.overwriteVks, - loadedCheckpoint - ) - } - - private fun onCriticalError(onCriticalErrorHandler: ((Throwable?) -> Boolean)?, error: Throwable) { - twig("********") - twig("******** INITIALIZER ERROR: $error") - if (error.cause != null) twig("******** caused by ${error.cause}") - if (error.cause?.cause != null) twig("******** caused by ${error.cause?.cause}") - twig("********") - twig(error) - - if (onCriticalErrorHandler == null) { - twig( - "WARNING: a critical error occurred on the Initializer but no callback is " + - "registered to be notified of critical errors! THIS IS PROBABLY A MISTAKE. To " + - "respond to these errors (perhaps to update the UI or alert the user) set " + - "initializer.onCriticalErrorHandler to a non-null value or use the secondary " + - "constructor: Initializer(context, handler) { ... }. Note that the synchronizer " + - "and initializer BOTH have error handlers and since the initializer exists " + - "before the synchronizer, it needs its error handler set separately." - ) - } - - onCriticalErrorHandler?.invoke(error) - } - - private suspend fun initRustBackend( - context: Context, - network: ZcashNetwork, - alias: String, - blockHeight: BlockHeight - ): RustBackend { - val coordinator = DatabaseCoordinator.getInstance(context) - - return RustBackend.init( - coordinator.cacheDbFile(network, alias).absolutePath, - coordinator.dataDbFile(network, alias).absolutePath, - File(context.getCacheDirSuspend(), "params").absolutePath, - network, - blockHeight - ) - } - - /** - * Delete the databases associated with this wallet. This removes all compact blocks and - * data derived from those blocks. For most wallets, this should not result in a loss of - * funds because the seed and spending keys are stored separately. This call just removes - * the associated data but not the seed or spending key, themselves, because those are - * managed separately by the wallet. - * - * @param appContext the application context. - * @param network the network associated with the data to be erased. - * @param alias the alias used to create the local data. - * - * @return true when one of the associated files was found. False most likely indicates - * that the wrong alias was provided. - */ - override suspend fun erase( - appContext: Context, - network: ZcashNetwork, - alias: String - ): Boolean = DatabaseCoordinator.getInstance(appContext).deleteDatabases(network, alias) - } -} - -/** - * Validate that the alias doesn't contain malicious characters by enforcing simple rules which - * permit the alias to be used as part of a file name for the preferences and databases. This - * enables multiple wallets to exist on one device, which is also helpful for sweeping funds. - * - * @param alias the alias to validate. - * - * @throws IllegalArgumentException whenever the alias is not less than 100 characters or - * contains something other than alphanumeric characters. Underscores are allowed but aliases - * must start with a letter. - */ -internal fun validateAlias(alias: String) { - require( - alias.length in ZcashSdk.ALIAS_MIN_LENGTH..ZcashSdk.ALIAS_MAX_LENGTH && alias[0].isLetter() && - alias.all { it.isLetterOrDigit() || it == '_' } - ) { - "ERROR: Invalid alias ($alias). For security, the alias must be shorter than 100 " + - "characters and only contain letters, digits or underscores and start with a letter." - } -} diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/SdkSynchronizer.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/SdkSynchronizer.kt index 93b5ce05..39287c92 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/SdkSynchronizer.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/SdkSynchronizer.kt @@ -35,9 +35,11 @@ import cash.z.ecc.android.sdk.ext.ZcashSdk import cash.z.ecc.android.sdk.internal.block.CompactBlockDbStore 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.getCacheDirSuspend 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.model.Checkpoint 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 @@ -48,17 +50,19 @@ 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.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.UnifiedSpendingKey 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.AddressType.Unified import cash.z.ecc.android.sdk.type.ConsensusMatchType +import cash.z.ecc.android.sdk.type.UnifiedFullViewingKey import cash.z.wallet.sdk.rpc.Service import io.grpc.ManagedChannel import kotlinx.coroutines.CoroutineExceptionHandler @@ -81,6 +85,7 @@ import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch +import java.io.File import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext @@ -803,37 +808,64 @@ object DefaultSynchronizerFactory { ) } + internal suspend fun defaultRustBackend( + context: Context, + network: ZcashNetwork, + alias: String, + blockHeight: BlockHeight + ): RustBackend { + val coordinator = DatabaseCoordinator.getInstance(context) + + return RustBackend.init( + coordinator.cacheDbFile(network, alias).absolutePath, + coordinator.dataDbFile(network, alias).absolutePath, + File(context.getCacheDirSuspend(), "params").absolutePath, + network, + blockHeight + ) + } + // TODO [#242]: Don't hard code page size. It is a workaround for Uncaught Exception: // android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy // can touch its views. and is probably related to FlowPagedList // TODO [#242]: https://github.com/zcash/zcash-android-wallet-sdk/issues/242 private const val DEFAULT_PAGE_SIZE = 1000 - suspend fun defaultTransactionRepository(initializer: Initializer, seed: ByteArray?): TransactionRepository = + + @Suppress("LongParameterList") + internal suspend fun defaultTransactionRepository( + context: Context, + rustBackend: RustBackend, + zcashNetwork: ZcashNetwork, + checkpoint: Checkpoint, + viewingKeys: List, + seed: ByteArray? + ): TransactionRepository = PagedTransactionRepository.new( - initializer.context, - initializer.network, + context, + zcashNetwork, DEFAULT_PAGE_SIZE, - initializer.rustBackend, + rustBackend, seed, - initializer.checkpoint, - initializer.viewingKeys, - initializer.overwriteVks + checkpoint, + viewingKeys, + false ) - fun defaultBlockStore(initializer: Initializer): CompactBlockStore = + internal fun defaultBlockStore(context: Context, rustBackend: RustBackend, zcashNetwork: ZcashNetwork): + CompactBlockStore = CompactBlockDbStore.new( - initializer.context, - initializer.network, - initializer.rustBackend.cacheDbFile + context, + zcashNetwork, + rustBackend.cacheDbFile ) - fun defaultService(initializer: Initializer): LightWalletService = - LightWalletGrpcService.new(initializer.context, initializer.lightWalletEndpoint) + fun defaultService(context: Context, lightWalletEndpoint: LightWalletEndpoint): LightWalletService = + LightWalletGrpcService.new(context, lightWalletEndpoint) - fun defaultEncoder( - initializer: Initializer, + internal fun defaultEncoder( + rustBackend: RustBackend, repository: TransactionRepository - ): TransactionEncoder = WalletTransactionEncoder(initializer.rustBackend, repository) + ): TransactionEncoder = WalletTransactionEncoder(rustBackend, repository) fun defaultDownloader( service: LightWalletService, @@ -841,31 +873,33 @@ object DefaultSynchronizerFactory { ): CompactBlockDownloader = CompactBlockDownloader(service, blockStore) suspend fun defaultTxManager( - initializer: Initializer, + context: Context, + zcashNetwork: ZcashNetwork, + alias: String, encoder: TransactionEncoder, service: LightWalletService ): OutboundTransactionManager { - val databaseFile = DatabaseCoordinator.getInstance(initializer.context).pendingTransactionsDbFile( - initializer.network, - initializer.alias + val databaseFile = DatabaseCoordinator.getInstance(context).pendingTransactionsDbFile( + zcashNetwork, + alias ) return PersistentTransactionManager( - initializer.context, + context, encoder, service, databaseFile ) } - fun defaultProcessor( - initializer: Initializer, + internal fun defaultProcessor( + rustBackend: RustBackend, downloader: CompactBlockDownloader, repository: TransactionRepository ): CompactBlockProcessor = CompactBlockProcessor( downloader, repository, - initializer.rustBackend, - initializer.rustBackend.birthdayHeight + rustBackend, + rustBackend.birthdayHeight ) } diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/Synchronizer.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/Synchronizer.kt index 4935e68e..dec91b58 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/Synchronizer.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/Synchronizer.kt @@ -1,13 +1,19 @@ package cash.z.ecc.android.sdk +import android.content.Context import cash.z.ecc.android.sdk.block.CompactBlockProcessor +import cash.z.ecc.android.sdk.db.DatabaseCoordinator 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.LightWalletEndpoint +import cash.z.ecc.android.sdk.model.UnifiedSpendingKey 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.CheckpointTool +import cash.z.ecc.android.sdk.tool.DerivationTool import cash.z.ecc.android.sdk.type.AddressType import cash.z.ecc.android.sdk.type.ConsensusMatchType import cash.z.wallet.sdk.rpc.Service @@ -463,29 +469,68 @@ interface Synchronizer { /** * Primary method that SDK clients will use to construct a synchronizer. * - * If customized initialization is required (e.g. for dependency injection or testing), see - * [DefaultSynchronizerFactory]. - * * @param initializer the helper that is leveraged for creating all the components that the * Synchronizer requires. It contains all information necessary to build a synchronizer and it is * mainly responsible for initializing the databases associated with this synchronizer and loading * the rust backend. - * @param seed the wallet's seed phrase. This only needs to be provided if this method returns an - * error indicating that the seed phrase is required for a database migration. + * @param seed the wallet's seed phrase. This is required the first time a new wallet is set up. For + * subsequent calls, seed is only needed if [InitializerException.SeedRequired] is thrown. + * @throws InitializerException.SeedRequired */ + /* + * If customized initialization is required (e.g. for dependency injection or testing), see + * [DefaultSynchronizerFactory]. + */ + @Suppress("LongParameterList") suspend fun new( - initializer: Initializer, - seed: ByteArray, + context: Context, + zcashNetwork: ZcashNetwork, + alias: String = "zcash", + lightWalletEndpoint: LightWalletEndpoint, + seed: ByteArray?, + birthday: BlockHeight? ): Synchronizer { - val repository = DefaultSynchronizerFactory.defaultTransactionRepository(initializer, seed) - val blockStore = DefaultSynchronizerFactory.defaultBlockStore(initializer) - val service = DefaultSynchronizerFactory.defaultService(initializer) - val encoder = DefaultSynchronizerFactory.defaultEncoder(initializer, repository) + val applicationContext = context.applicationContext + + validateAlias(alias) + + val loadedCheckpoint = CheckpointTool.loadNearest( + applicationContext, + zcashNetwork, + birthday ?: zcashNetwork.saplingActivationHeight + ) + + val rustBackend = DefaultSynchronizerFactory.defaultRustBackend( + applicationContext, + zcashNetwork, + alias, + loadedCheckpoint.height + ) + + val viewingKeys = seed?.let { + DerivationTool.deriveUnifiedFullViewingKeys( + seed, + zcashNetwork, + 1 + ).toList() + } ?: emptyList() + + val repository = DefaultSynchronizerFactory.defaultTransactionRepository( + applicationContext, + rustBackend, + zcashNetwork, + loadedCheckpoint, + viewingKeys, + seed + ) + val blockStore = DefaultSynchronizerFactory.defaultBlockStore(applicationContext, rustBackend, zcashNetwork) + val service = DefaultSynchronizerFactory.defaultService(applicationContext, lightWalletEndpoint) + val encoder = DefaultSynchronizerFactory.defaultEncoder(rustBackend, repository) val downloader = DefaultSynchronizerFactory.defaultDownloader(service, blockStore) val txManager = - DefaultSynchronizerFactory.defaultTxManager(initializer, encoder, service) + DefaultSynchronizerFactory.defaultTxManager(applicationContext, zcashNetwork, alias, encoder, service) val processor = - DefaultSynchronizerFactory.defaultProcessor(initializer, downloader, repository) + DefaultSynchronizerFactory.defaultProcessor(rustBackend, downloader, repository) return SdkSynchronizer( repository, @@ -501,8 +546,57 @@ interface Synchronizer { * This is a blocking call, so it should not be called from the main thread. */ @JvmStatic - fun newBlocking(initializer: Initializer, seed: ByteArray): Synchronizer = runBlocking { - new (initializer, seed) + @Suppress("LongParameterList") + fun newBlocking( + context: Context, + zcashNetwork: ZcashNetwork, + alias: String = "zcash", + lightWalletEndpoint: LightWalletEndpoint, + seed: ByteArray?, + birthday: BlockHeight? + ): Synchronizer = runBlocking { + new(context, zcashNetwork, alias, lightWalletEndpoint, seed, birthday) } + + /** + * Delete the databases associated with this wallet. This removes all compact blocks and + * data derived from those blocks. Although most data can be regenerated by setting up a new + * Synchronizer instance with the seed, there are two special cases where data is not retained: + * 1. Outputs created with a `null` OVK + * 2. The UA to which a transaction was sent (recovery from seed will only reveal the receiver, not the full UA) + * + * @param appContext the application context. + * @param network the network associated with the data to be erased. + * @param alias the alias used to create the local data. + * + * @return true when one of the associated files was found. False most likely indicates + * that the wrong alias was provided. + */ + suspend fun erase( + appContext: Context, + network: ZcashNetwork, + alias: String + ): Boolean = DatabaseCoordinator.getInstance(appContext).deleteDatabases(network, alias) + } +} + +/** + * Validate that the alias doesn't contain malicious characters by enforcing simple rules which + * permit the alias to be used as part of a file name for the preferences and databases. This + * enables multiple wallets to exist on one device, which is also helpful for sweeping funds. + * + * @param alias the alias to validate. + * + * @throws IllegalArgumentException whenever the alias is not less than 100 characters or + * contains something other than alphanumeric characters. Underscores are allowed but aliases + * must start with a letter. + */ +private fun validateAlias(alias: String) { + require( + alias.length in ZcashSdk.ALIAS_MIN_LENGTH..ZcashSdk.ALIAS_MAX_LENGTH && alias[0].isLetter() && + alias.all { it.isLetterOrDigit() || it == '_' } + ) { + "ERROR: Invalid alias ($alias). For security, the alias must be shorter than 100 " + + "characters and only contain letters, digits or underscores and start with a letter." } } diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/DerivedDataDb.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/DerivedDataDb.kt index 8d705a33..bca20157 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/DerivedDataDb.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/DerivedDataDb.kt @@ -18,7 +18,6 @@ import cash.z.ecc.android.sdk.db.entity.Sent import cash.z.ecc.android.sdk.db.entity.TransactionEntity import cash.z.ecc.android.sdk.db.entity.Utxo import cash.z.ecc.android.sdk.internal.twig -import cash.z.ecc.android.sdk.type.UnifiedAddressAccount // // Database diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/TransactionRepository.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/TransactionRepository.kt index a25fe814..aa1f2cb6 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/TransactionRepository.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/TransactionRepository.kt @@ -3,7 +3,6 @@ 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 /**