Simplify Synchronizer instantiation

fix

synchronizer
This commit is contained in:
Carter Jernigan 2022-10-06 13:48:21 -04:00 committed by Carter Jernigan
parent 597cc43886
commit 3b826f8f6a
19 changed files with 310 additions and 684 deletions

View File

@ -3,7 +3,6 @@ package cash.z.ecc.android.sdk.darkside.test
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import cash.z.ecc.android.bip39.Mnemonics import cash.z.ecc.android.bip39.Mnemonics
import cash.z.ecc.android.bip39.toSeed 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.SdkSynchronizer
import cash.z.ecc.android.sdk.Synchronizer import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.db.entity.isPending import cash.z.ecc.android.sdk.db.entity.isPending
@ -67,12 +66,16 @@ class TestWallet(
runBlocking { DerivationTool.deriveSpendingKeys(seed, network = network)[0] } runBlocking { DerivationTool.deriveSpendingKeys(seed, network = network)[0] }
private val transparentAccountPrivateKey = private val transparentAccountPrivateKey =
runBlocking { DerivationTool.deriveTransparentAccountPrivateKey(seed, network = network) } runBlocking { DerivationTool.deriveTransparentAccountPrivateKey(seed, network = network) }
val initializer = runBlocking { val synchronizer: SdkSynchronizer = Synchronizer.newBlocking(
Initializer.new(context) { config -> context,
runBlocking { config.importWallet(seed, startHeight, network, endpoint, alias = alias) } network,
} alias,
} endpoint,
val synchronizer: SdkSynchronizer = runBlocking { Synchronizer.new(initializer) } as SdkSynchronizer seed,
startHeight
)
as
SdkSynchronizer
val service = (synchronizer.processor.downloader.lightWalletService as LightWalletGrpcService) val service = (synchronizer.processor.downloader.lightWalletService as LightWalletGrpcService)
val available get() = synchronizer.saplingBalances.value?.available val available get() = synchronizer.saplingBalances.value?.available

View File

@ -1,9 +1,9 @@
package cash.z.wallet.sdk.sample.demoapp package cash.z.wallet.sdk.sample.demoapp
import androidx.test.platform.app.InstrumentationRegistry 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
import cash.z.ecc.android.sdk.db.entity.isFailure 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.convertZecToZatoshi
import cash.z.ecc.android.sdk.ext.toHex import cash.z.ecc.android.sdk.ext.toHex
import cash.z.ecc.android.sdk.internal.TroubleshootingTwig 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.LightWalletEndpoint
import cash.z.ecc.android.sdk.model.Mainnet import cash.z.ecc.android.sdk.model.Mainnet
import cash.z.ecc.android.sdk.model.ZcashNetwork import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.model.defaultForNetwork
import cash.z.ecc.android.sdk.tool.DerivationTool import cash.z.ecc.android.sdk.tool.DerivationTool
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -160,8 +161,14 @@ class SampleCodeTest {
private val context = InstrumentationRegistry.getInstrumentation().targetContext private val context = InstrumentationRegistry.getInstrumentation().targetContext
private val synchronizer: Synchronizer = run { private val synchronizer: Synchronizer = run {
val initializer = runBlocking { Initializer.new(context) {} } val network = ZcashNetwork.fromResources(context)
Synchronizer.newBlocking(initializer) Synchronizer.newBlocking(
context,
network,
lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network),
seed = seed,
birthday = null
)
} }
@BeforeClass @BeforeClass

View File

@ -5,7 +5,6 @@ import android.view.LayoutInflater
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import cash.z.ecc.android.bip39.Mnemonics import cash.z.ecc.android.bip39.Mnemonics
import cash.z.ecc.android.bip39.toSeed 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.Synchronizer
import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentGetAddressBinding import cash.z.ecc.android.sdk.demoapp.databinding.FragmentGetAddressBinding
@ -49,19 +48,14 @@ class GetAddressFragment : BaseDemoFragment<FragmentGetAddressBinding>() {
).first() ).first()
} }
// using the ViewingKey to initialize val network = ZcashNetwork.fromResources(requireApplicationContext())
runBlocking { synchronizer = Synchronizer.newBlocking(
Initializer.new(requireApplicationContext(), null) { requireApplicationContext(),
val network = ZcashNetwork.fromResources(requireApplicationContext()) network,
it.newWallet( lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network),
viewingKey, seed = seed,
network = network, birthday = network.saplingActivationHeight
lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network) )
)
}
}.let { initializer ->
synchronizer = Synchronizer.newBlocking(initializer)
}
} }
private fun displayAddress() { private fun displayAddress() {

View File

@ -5,7 +5,6 @@ import android.view.LayoutInflater
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import cash.z.ecc.android.bip39.Mnemonics import cash.z.ecc.android.bip39.Mnemonics
import cash.z.ecc.android.bip39.toSeed 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.Synchronizer
import cash.z.ecc.android.sdk.block.CompactBlockProcessor import cash.z.ecc.android.sdk.block.CompactBlockProcessor
import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment 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.WalletBalance
import cash.z.ecc.android.sdk.model.ZcashNetwork import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.model.defaultForNetwork import cash.z.ecc.android.sdk.model.defaultForNetwork
import cash.z.ecc.android.sdk.tool.DerivationTool
import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.runBlocking
/** /**
* Displays the available balance && total balance associated with the seed defined by the default config. * Displays the available balance && total balance associated with the seed defined by the default config.
@ -46,27 +43,14 @@ class GetBalanceFragment : BaseDemoFragment<FragmentGetBalanceBinding>() {
// have the seed stored // have the seed stored
val seed = Mnemonics.MnemonicCode(seedPhrase).toSeed() val seed = Mnemonics.MnemonicCode(seedPhrase).toSeed()
// converting seed into viewingKey val network = ZcashNetwork.fromResources(requireApplicationContext())
val viewingKey = runBlocking { synchronizer = Synchronizer.newBlocking(
DerivationTool.deriveUnifiedFullViewingKeys( requireApplicationContext(),
seed, network,
ZcashNetwork.fromResources(requireApplicationContext()) lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network),
).first() seed = seed,
} birthday = null
)
// 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)
}
} }
override fun onResume() { override fun onResume() {

View File

@ -8,7 +8,6 @@ import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import cash.z.ecc.android.bip39.Mnemonics import cash.z.ecc.android.bip39.Mnemonics
import cash.z.ecc.android.bip39.toSeed 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.Synchronizer
import cash.z.ecc.android.sdk.block.CompactBlockProcessor 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.ConfirmedTransaction
@ -33,7 +32,6 @@ import kotlinx.coroutines.runBlocking
*/ */
@Suppress("TooManyFunctions") @Suppress("TooManyFunctions")
class ListTransactionsFragment : BaseDemoFragment<FragmentListTransactionsBinding>() { class ListTransactionsFragment : BaseDemoFragment<FragmentListTransactionsBinding>() {
private lateinit var initializer: Initializer
private lateinit var synchronizer: Synchronizer private lateinit var synchronizer: Synchronizer
private lateinit var adapter: TransactionAdapter private lateinit var adapter: TransactionAdapter
private lateinit var address: String private lateinit var address: String
@ -51,28 +49,20 @@ class ListTransactionsFragment : BaseDemoFragment<FragmentListTransactionsBindin
// Use a BIP-39 library to convert a seed phrase into a byte array. Most wallets already // Use a BIP-39 library to convert a seed phrase into a byte array. Most wallets already
// have the seed stored // have the seed stored
val seed = Mnemonics.MnemonicCode(seedPhrase).toSeed() val seed = Mnemonics.MnemonicCode(seedPhrase).toSeed()
val network = ZcashNetwork.fromResources(requireApplicationContext())
initializer = Initializer.newBlocking(
requireApplicationContext(),
Initializer.Config {
val network = ZcashNetwork.fromResources(requireApplicationContext())
runBlocking {
it.importWallet(
seed,
birthday = null,
network = network,
lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network)
)
}
}
)
address = runBlocking { address = runBlocking {
DerivationTool.deriveUnifiedAddress( DerivationTool.deriveUnifiedAddress(
seed, seed,
ZcashNetwork.fromResources(requireApplicationContext()) ZcashNetwork.fromResources(requireApplicationContext())
) )
} }
synchronizer = Synchronizer.newBlocking(initializer, seed) synchronizer = Synchronizer.newBlocking(
requireApplicationContext(),
network,
lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network),
seed = seed,
birthday = null
)
} }
private fun initTransactionUI() { private fun initTransactionUI() {

View File

@ -8,7 +8,6 @@ import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import cash.z.ecc.android.bip39.Mnemonics import cash.z.ecc.android.bip39.Mnemonics
import cash.z.ecc.android.bip39.toSeed 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.SdkSynchronizer
import cash.z.ecc.android.sdk.Synchronizer import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.block.CompactBlockProcessor import cash.z.ecc.android.sdk.block.CompactBlockProcessor
@ -28,7 +27,6 @@ import cash.z.ecc.android.sdk.tool.DerivationTool
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlin.math.max import kotlin.math.max
@ -47,7 +45,6 @@ import kotlin.math.max
@Suppress("TooManyFunctions") @Suppress("TooManyFunctions")
class ListUtxosFragment : BaseDemoFragment<FragmentListUtxosBinding>() { class ListUtxosFragment : BaseDemoFragment<FragmentListUtxosBinding>() {
private lateinit var seed: ByteArray private lateinit var seed: ByteArray
private lateinit var initializer: Initializer
private lateinit var synchronizer: Synchronizer private lateinit var synchronizer: Synchronizer
private lateinit var adapter: UtxoAdapter private lateinit var adapter: UtxoAdapter
private val address: String = "t1RwbKka1CnktvAJ1cSqdn7c6PXWG4tZqgd" private val address: String = "t1RwbKka1CnktvAJ1cSqdn7c6PXWG4tZqgd"
@ -66,20 +63,15 @@ class ListUtxosFragment : BaseDemoFragment<FragmentListUtxosBinding>() {
// Use a BIP-39 library to convert a seed phrase into a byte array. Most wallets already // Use a BIP-39 library to convert a seed phrase into a byte array. Most wallets already
// have the seed stored // have the seed stored
seed = Mnemonics.MnemonicCode(sharedViewModel.seedPhrase.value).toSeed() seed = Mnemonics.MnemonicCode(sharedViewModel.seedPhrase.value).toSeed()
initializer = runBlocking { val network = ZcashNetwork.fromResources(requireApplicationContext())
val network = ZcashNetwork.fromResources(requireApplicationContext()) synchronizer = Synchronizer.newBlocking(
Initializer.new(requireApplicationContext()) { requireApplicationContext(),
runBlocking { network,
it.newWallet( alias = "Demo_Utxos",
seed, lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network),
network = network, seed = seed,
lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network) birthday = null
) )
}
it.alias = "Demo_Utxos"
}
}
synchronizer = runBlocking { Synchronizer.new(initializer, seed) }
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {

View File

@ -8,7 +8,6 @@ import android.widget.TextView
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import cash.z.ecc.android.bip39.Mnemonics import cash.z.ecc.android.bip39.Mnemonics
import cash.z.ecc.android.bip39.toSeed 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.Synchronizer
import cash.z.ecc.android.sdk.block.CompactBlockProcessor import cash.z.ecc.android.sdk.block.CompactBlockProcessor
import cash.z.ecc.android.sdk.db.entity.PendingTransaction import cash.z.ecc.android.sdk.db.entity.PendingTransaction
@ -68,20 +67,14 @@ class SendFragment : BaseDemoFragment<FragmentSendBinding>() {
// have the seed stored // have the seed stored
val seed = Mnemonics.MnemonicCode(seedPhrase).toSeed() val seed = Mnemonics.MnemonicCode(seedPhrase).toSeed()
runBlocking { val network = ZcashNetwork.fromResources(requireApplicationContext())
Initializer.new(requireApplicationContext()) { synchronizer = Synchronizer.newBlocking(
val network = ZcashNetwork.fromResources(requireApplicationContext()) requireApplicationContext(),
runBlocking { network,
it.newWallet( lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network),
seed, seed = seed,
network = network, birthday = null
lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network) )
)
}
}
}.let { initializer ->
synchronizer = Synchronizer.newBlocking(initializer, seed)
}
spendingKey = runBlocking { spendingKey = runBlocking {
DerivationTool.deriveSpendingKeys(seed, ZcashNetwork.fromResources(requireApplicationContext())).first() DerivationTool.deriveSpendingKeys(seed, ZcashNetwork.fromResources(requireApplicationContext())).first()
} }

View File

@ -1,19 +1,11 @@
package cash.z.ecc.android.sdk.ext 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.OkHttpClient
import okhttp3.Request import okhttp3.Request
import org.json.JSONObject import org.json.JSONObject
import ru.gildor.coroutines.okhttp.await import ru.gildor.coroutines.okhttp.await
import kotlin.test.assertNotNull import kotlin.test.assertNotNull
fun Initializer.Config.seedPhrase(seedPhrase: String, network: ZcashNetwork) {
runBlocking { setSeed(SimpleMnemonics().toSeed(seedPhrase.toCharArray()), network) }
}
object BlockExplorer { object BlockExplorer {
suspend fun fetchLatestHeight(): Long { suspend fun fetchLatestHeight(): Long {
val client = OkHttpClient() val client = OkHttpClient()

View File

@ -1,5 +1,7 @@
package cash.z.ecc.android.sdk.integration 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.MaintainedTest
import cash.z.ecc.android.sdk.annotation.TestPurpose import cash.z.ecc.android.sdk.annotation.TestPurpose
import cash.z.ecc.android.sdk.db.DatabaseCoordinator import cash.z.ecc.android.sdk.db.DatabaseCoordinator
@ -13,7 +15,6 @@ import org.junit.Ignore
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.junit.runners.Parameterized import org.junit.runners.Parameterized
import kotlin.test.DefaultAsserter.assertEquals
import kotlin.test.DefaultAsserter.assertTrue import kotlin.test.DefaultAsserter.assertTrue
// TODO [#650]: https://github.com/zcash/zcash-android-wallet-sdk/issues/650 // TODO [#650]: https://github.com/zcash/zcash-android-wallet-sdk/issues/650
@ -36,44 +37,35 @@ class SanityTest(
@Test @Test
fun testFilePaths() { fun testFilePaths() {
val rustBackend = runBlocking {
DefaultSynchronizerFactory.defaultRustBackend(
ApplicationProvider.getApplicationContext(),
ZcashNetwork.Testnet,
"TestWallet",
TestWallet.Backups.SAMPLE_WALLET.testnetBirthday
)
}
assertTrue( assertTrue(
"$name has invalid DataDB file", "$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}" "no_backup/co.electricoin.zcash/TestWallet_${networkName}_${DatabaseCoordinator.DB_DATA_NAME}"
) )
) )
assertTrue( assertTrue(
"$name has invalid CacheDB file", "$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}" "no_backup/co.electricoin.zcash/TestWallet_${networkName}_${DatabaseCoordinator.DB_CACHE_NAME}"
) )
) )
assertTrue( assertTrue(
"$name has invalid CacheDB params dir", "$name has invalid CacheDB params dir",
wallet.initializer.rustBackend.pathParamsDir.endsWith( rustBackend.pathParamsDir.endsWith(
"cache/params" "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 @Test
@Ignore( @Ignore(
"This test needs to be refactored to a separate test module. It causes SSLHandshakeException: Chain " + "This test needs to be refactored to a separate test module. It causes SSLHandshakeException: Chain " +

View File

@ -1,13 +1,15 @@
package cash.z.ecc.android.sdk.integration package cash.z.ecc.android.sdk.integration
import androidx.test.core.app.ApplicationProvider
import androidx.test.filters.LargeTest import androidx.test.filters.LargeTest
import androidx.test.filters.MediumTest 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.MaintainedTest
import cash.z.ecc.android.sdk.annotation.TestPurpose import cash.z.ecc.android.sdk.annotation.TestPurpose
import cash.z.ecc.android.sdk.db.DatabaseCoordinator 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 cash.z.ecc.android.sdk.util.TestWallet
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Ignore import org.junit.Ignore
import org.junit.Test import org.junit.Test
@ -22,38 +24,32 @@ class SmokeTest {
@Test @Test
fun testFilePaths() { fun testFilePaths() {
val rustBackend = runBlocking {
DefaultSynchronizerFactory.defaultRustBackend(
ApplicationProvider.getApplicationContext(),
ZcashNetwork.Testnet,
"TestWallet",
TestWallet.Backups.SAMPLE_WALLET.testnetBirthday
)
}
assertTrue( assertTrue(
"Invalid DataDB file", "Invalid DataDB file",
wallet.initializer.rustBackend.dataDbFile.absolutePath.endsWith( rustBackend.dataDbFile.absolutePath.endsWith(
"no_backup/co.electricoin.zcash/TestWallet_testnet_${DatabaseCoordinator.DB_DATA_NAME}" "no_backup/co.electricoin.zcash/TestWallet_testnet_${DatabaseCoordinator.DB_DATA_NAME}"
) )
) )
assertTrue( assertTrue(
"Invalid CacheDB file", "Invalid CacheDB file",
wallet.initializer.rustBackend.cacheDbFile.absolutePath.endsWith( rustBackend.cacheDbFile.absolutePath.endsWith(
"no_backup/co.electricoin.zcash/TestWallet_testnet_${DatabaseCoordinator.DB_CACHE_NAME}" "no_backup/co.electricoin.zcash/TestWallet_testnet_${DatabaseCoordinator.DB_CACHE_NAME}"
) )
) )
assertTrue( assertTrue(
"Invalid CacheDB params dir", "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 // This test takes an extremely long time
// Does its runtime grow over time based on growth of the blockchain? // Does its runtime grow over time based on growth of the blockchain?
@Test @Test

View File

@ -2,7 +2,6 @@ package cash.z.ecc.android.sdk.integration
import androidx.test.filters.LargeTest import androidx.test.filters.LargeTest
import androidx.test.platform.app.InstrumentationRegistry 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
import cash.z.ecc.android.sdk.Synchronizer.Status.SYNCED import cash.z.ecc.android.sdk.Synchronizer.Status.SYNCED
import cash.z.ecc.android.sdk.db.entity.isSubmitSuccess import cash.z.ecc.android.sdk.db.entity.isSubmitSuccess
@ -63,7 +62,7 @@ class TestnetIntegrationTest : ScopedTest() {
@Test @Test
@Ignore("This test is broken") @Ignore("This test is broken")
fun getAddress() = runBlocking { 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 // 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 { companion object {
init { Twig.plant(TroubleshootingTwig()) } init {
Twig.plant(TroubleshootingTwig())
}
val lightWalletEndpoint = LightWalletEndpoint("lightwalletd.testnet.z.cash", 9087, true) val lightWalletEndpoint = LightWalletEndpoint("lightwalletd.testnet.z.cash", 9087, true)
private const val birthdayHeight = 963150L private const val birthdayHeight = 963150L
private const val targetHeight = 663250 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 seed = "cash.z.ecc.android.sdk.integration.IntegrationTest.seed.value.64bytes".toByteArray()
val address = "zs1m30y59wxut4zk9w24d6ujrdnfnl42hpy0ugvhgyhr8s0guszutqhdj05c7j472dndjstulph74m" val address = "zs1m30y59wxut4zk9w24d6ujrdnfnl42hpy0ugvhgyhr8s0guszutqhdj05c7j472dndjstulph74m"
val toAddress = "zs1vp7kvlqr4n9gpehztr76lcn6skkss9p8keqs3nv8avkdtjrcctrvmk9a7u494kluv756jeee5k0" val toAddress = "zs1vp7kvlqr4n9gpehztr76lcn6skkss9p8keqs3nv8avkdtjrcctrvmk9a7u494kluv756jeee5k0"
private val context = InstrumentationRegistry.getInstrumentation().context 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 private lateinit var synchronizer: Synchronizer
@JvmStatic @JvmStatic
@BeforeClass @BeforeClass
fun startUp() { 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) synchronizer.start(classScope)
} }
} }

View File

@ -1,7 +1,6 @@
package cash.z.ecc.android.sdk.util package cash.z.ecc.android.sdk.util
import androidx.test.platform.app.InstrumentationRegistry 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
import cash.z.ecc.android.sdk.internal.TroubleshootingTwig import cash.z.ecc.android.sdk.internal.TroubleshootingTwig
import cash.z.ecc.android.sdk.internal.Twig 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.model.defaultForNetwork
import cash.z.ecc.android.sdk.tool.CheckpointTool import cash.z.ecc.android.sdk.tool.CheckpointTool
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import okio.buffer
import okio.source
import org.junit.Before import org.junit.Before
import org.junit.Ignore import org.junit.Ignore
import org.junit.Test import org.junit.Test
@ -83,11 +80,7 @@ class BalancePrinterUtil {
mnemonics.toSeed(seedPhrase.toCharArray()) mnemonics.toSeed(seedPhrase.toCharArray())
}.collect { seed -> }.collect { seed ->
// TODO: clear the dataDb but leave the cacheDb // 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 what I need to do right now
- for each seed - for each seed
@ -103,7 +96,14 @@ class BalancePrinterUtil {
- can we be more stateless and thereby improve the flexibility of this code?!!! - can we be more stateless and thereby improve the flexibility of this code?!!!
*/ */
synchronizer?.stop() synchronizer?.stop()
synchronizer = Synchronizer.new(initializer).apply { synchronizer = Synchronizer.new(
context,
network,
lightWalletEndpoint = LightWalletEndpoint
.defaultForNetwork(network),
seed = seed,
birthday = birthdayHeight
).apply {
start() start()
} }
@ -142,16 +142,7 @@ class BalancePrinterUtil {
// } // }
@Throws(IOException::class) @Throws(IOException::class)
fun readLines() = flow<String> { fun readLines() = javaClass.getResourceAsStream("/utils/seeds.txt")!!.bufferedReader().lineSequence().asFlow()
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()
}
}
}
// private fun initWallet(seed: String): Wallet { // private fun initWallet(seed: String): Wallet {
// val spendingKeyProvider = Delegates.notNull<String>() // val spendingKeyProvider = Delegates.notNull<String>()

View File

@ -1,13 +1,14 @@
package cash.z.ecc.android.sdk.util package cash.z.ecc.android.sdk.util
import androidx.test.platform.app.InstrumentationRegistry 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.SdkSynchronizer
import cash.z.ecc.android.sdk.Synchronizer import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.internal.TroubleshootingTwig import cash.z.ecc.android.sdk.internal.TroubleshootingTwig
import cash.z.ecc.android.sdk.internal.Twig import cash.z.ecc.android.sdk.internal.Twig
import cash.z.ecc.android.sdk.model.BlockHeight 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.ZcashNetwork
import cash.z.ecc.android.sdk.model.defaultForNetwork
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -65,27 +66,17 @@ class DataDbScannerUtil {
@Test @Test
@Ignore("This test is broken") @Ignore("This test is broken")
fun scanExistingDb() { fun scanExistingDb() {
synchronizer = run { synchronizer = Synchronizer.newBlocking(
val initializer = runBlocking { context,
Initializer.new(context) { ZcashNetwork.Mainnet,
it.setBirthdayHeight( lightWalletEndpoint = LightWalletEndpoint
BlockHeight.new( .defaultForNetwork(ZcashNetwork.Mainnet),
ZcashNetwork.Mainnet, seed = byteArrayOf(),
birthdayHeight birthday = BlockHeight.new(
), ZcashNetwork.Mainnet,
false birthdayHeight
) )
} )
}
val synchronizer = runBlocking {
Synchronizer.new(
initializer
)
}
synchronizer
}
println("sync!") println("sync!")
synchronizer.start() synchronizer.start()

View File

@ -3,7 +3,6 @@ package cash.z.ecc.android.sdk.util
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import cash.z.ecc.android.bip39.Mnemonics import cash.z.ecc.android.bip39.Mnemonics
import cash.z.ecc.android.bip39.toSeed 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.SdkSynchronizer
import cash.z.ecc.android.sdk.Synchronizer import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.db.entity.isPending import cash.z.ecc.android.sdk.db.entity.isPending
@ -67,12 +66,14 @@ class TestWallet(
runBlocking { DerivationTool.deriveSpendingKeys(seed, network = network)[0] } runBlocking { DerivationTool.deriveSpendingKeys(seed, network = network)[0] }
private val transparentAccountPrivateKey = private val transparentAccountPrivateKey =
runBlocking { DerivationTool.deriveTransparentAccountPrivateKey(seed, network = network) } runBlocking { DerivationTool.deriveTransparentAccountPrivateKey(seed, network = network) }
val initializer = runBlocking { val synchronizer: SdkSynchronizer = Synchronizer.newBlocking(
Initializer.new(context) { config -> context,
runBlocking { config.importWallet(seed, startHeight, network, endpoint, alias = alias) } network,
} alias,
} lightWalletEndpoint = endpoint,
val synchronizer: SdkSynchronizer = Synchronizer.newBlocking(initializer) as SdkSynchronizer seed = seed,
startHeight
) as SdkSynchronizer
val service = (synchronizer.processor.downloader.lightWalletService as LightWalletGrpcService) val service = (synchronizer.processor.downloader.lightWalletService as LightWalletGrpcService)
val available get() = synchronizer.saplingBalances.value?.available val available get() = synchronizer.saplingBalances.value?.available
@ -109,7 +110,12 @@ class TestWallet(
return this 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") Twig.sprout("$alias sending")
synchronizer.sendToAddress(shieldedSpendingKey, amount, address, memo, fromAccountIndex) synchronizer.sendToAddress(shieldedSpendingKey, amount, address, memo, fromAccountIndex)
.takeWhile { it.isPending(null) } .takeWhile { it.isPending(null) }

View File

@ -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<UnifiedFullViewingKey>,
val overwriteVks: Boolean,
internal val checkpoint: Checkpoint
) {
suspend fun erase() = erase(context, network, alias)
@Suppress("TooManyFunctions")
class Config private constructor(
val viewingKeys: MutableList<UnifiedFullViewingKey> = 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."
}
}

View File

@ -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.CompactBlockDbStore
import cash.z.ecc.android.sdk.internal.block.CompactBlockDownloader 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.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.toHexReversed
import cash.z.ecc.android.sdk.internal.ext.tryNull import cash.z.ecc.android.sdk.internal.ext.tryNull
import cash.z.ecc.android.sdk.internal.isEmpty 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.LightWalletGrpcService
import cash.z.ecc.android.sdk.internal.service.LightWalletService import cash.z.ecc.android.sdk.internal.service.LightWalletService
import cash.z.ecc.android.sdk.internal.transaction.OutboundTransactionManager 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.transaction.WalletTransactionEncoder
import cash.z.ecc.android.sdk.internal.twig import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.internal.twigTask 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.BlockHeight
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
import cash.z.ecc.android.sdk.model.WalletBalance import cash.z.ecc.android.sdk.model.WalletBalance
import cash.z.ecc.android.sdk.model.Zatoshi import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.model.ZcashNetwork 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
import cash.z.ecc.android.sdk.type.AddressType.Shielded 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.Transparent
import cash.z.ecc.android.sdk.type.AddressType.Unified import cash.z.ecc.android.sdk.type.AddressType.Unified
import cash.z.ecc.android.sdk.type.ConsensusMatchType import cash.z.ecc.android.sdk.type.ConsensusMatchType
import cash.z.ecc.android.sdk.type.UnifiedFullViewingKey
import cash.z.wallet.sdk.rpc.Service import cash.z.wallet.sdk.rpc.Service
import io.grpc.ManagedChannel import io.grpc.ManagedChannel
import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineExceptionHandler
@ -81,6 +85,7 @@ import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.File
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext 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: // 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 // android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy
// can touch its views. and is probably related to FlowPagedList // can touch its views. and is probably related to FlowPagedList
// TODO [#242]: https://github.com/zcash/zcash-android-wallet-sdk/issues/242 // TODO [#242]: https://github.com/zcash/zcash-android-wallet-sdk/issues/242
private const val DEFAULT_PAGE_SIZE = 1000 private const val DEFAULT_PAGE_SIZE = 1000
suspend fun defaultTransactionRepository(initializer: Initializer, seed: ByteArray?): TransactionRepository =
@Suppress("LongParameterList")
internal suspend fun defaultTransactionRepository(
context: Context,
rustBackend: RustBackend,
zcashNetwork: ZcashNetwork,
checkpoint: Checkpoint,
viewingKeys: List<UnifiedFullViewingKey>,
seed: ByteArray?
): TransactionRepository =
PagedTransactionRepository.new( PagedTransactionRepository.new(
initializer.context, context,
initializer.network, zcashNetwork,
DEFAULT_PAGE_SIZE, DEFAULT_PAGE_SIZE,
initializer.rustBackend, rustBackend,
seed, seed,
initializer.checkpoint, checkpoint,
initializer.viewingKeys, viewingKeys,
initializer.overwriteVks false
) )
fun defaultBlockStore(initializer: Initializer): CompactBlockStore = internal fun defaultBlockStore(context: Context, rustBackend: RustBackend, zcashNetwork: ZcashNetwork):
CompactBlockStore =
CompactBlockDbStore.new( CompactBlockDbStore.new(
initializer.context, context,
initializer.network, zcashNetwork,
initializer.rustBackend.cacheDbFile rustBackend.cacheDbFile
) )
fun defaultService(initializer: Initializer): LightWalletService = fun defaultService(context: Context, lightWalletEndpoint: LightWalletEndpoint): LightWalletService =
LightWalletGrpcService.new(initializer.context, initializer.lightWalletEndpoint) LightWalletGrpcService.new(context, lightWalletEndpoint)
fun defaultEncoder( internal fun defaultEncoder(
initializer: Initializer, rustBackend: RustBackend,
repository: TransactionRepository repository: TransactionRepository
): TransactionEncoder = WalletTransactionEncoder(initializer.rustBackend, repository) ): TransactionEncoder = WalletTransactionEncoder(rustBackend, repository)
fun defaultDownloader( fun defaultDownloader(
service: LightWalletService, service: LightWalletService,
@ -841,31 +873,33 @@ object DefaultSynchronizerFactory {
): CompactBlockDownloader = CompactBlockDownloader(service, blockStore) ): CompactBlockDownloader = CompactBlockDownloader(service, blockStore)
suspend fun defaultTxManager( suspend fun defaultTxManager(
initializer: Initializer, context: Context,
zcashNetwork: ZcashNetwork,
alias: String,
encoder: TransactionEncoder, encoder: TransactionEncoder,
service: LightWalletService service: LightWalletService
): OutboundTransactionManager { ): OutboundTransactionManager {
val databaseFile = DatabaseCoordinator.getInstance(initializer.context).pendingTransactionsDbFile( val databaseFile = DatabaseCoordinator.getInstance(context).pendingTransactionsDbFile(
initializer.network, zcashNetwork,
initializer.alias alias
) )
return PersistentTransactionManager( return PersistentTransactionManager(
initializer.context, context,
encoder, encoder,
service, service,
databaseFile databaseFile
) )
} }
fun defaultProcessor( internal fun defaultProcessor(
initializer: Initializer, rustBackend: RustBackend,
downloader: CompactBlockDownloader, downloader: CompactBlockDownloader,
repository: TransactionRepository repository: TransactionRepository
): CompactBlockProcessor = CompactBlockProcessor( ): CompactBlockProcessor = CompactBlockProcessor(
downloader, downloader,
repository, repository,
initializer.rustBackend, rustBackend,
initializer.rustBackend.birthdayHeight rustBackend.birthdayHeight
) )
} }

View File

@ -1,13 +1,19 @@
package cash.z.ecc.android.sdk package cash.z.ecc.android.sdk
import android.content.Context
import cash.z.ecc.android.sdk.block.CompactBlockProcessor 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.ConfirmedTransaction
import cash.z.ecc.android.sdk.db.entity.PendingTransaction import cash.z.ecc.android.sdk.db.entity.PendingTransaction
import cash.z.ecc.android.sdk.ext.ZcashSdk import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.model.BlockHeight 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.WalletBalance
import cash.z.ecc.android.sdk.model.Zatoshi import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.model.ZcashNetwork 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.AddressType
import cash.z.ecc.android.sdk.type.ConsensusMatchType import cash.z.ecc.android.sdk.type.ConsensusMatchType
import cash.z.wallet.sdk.rpc.Service import cash.z.wallet.sdk.rpc.Service
@ -463,29 +469,68 @@ interface Synchronizer {
/** /**
* Primary method that SDK clients will use to construct a 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 * @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 * 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 * mainly responsible for initializing the databases associated with this synchronizer and loading
* the rust backend. * the rust backend.
* @param seed the wallet's seed phrase. This only needs to be provided if this method returns an * @param seed the wallet's seed phrase. This is required the first time a new wallet is set up. For
* error indicating that the seed phrase is required for a database migration. * 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( suspend fun new(
initializer: Initializer, context: Context,
seed: ByteArray, zcashNetwork: ZcashNetwork,
alias: String = "zcash",
lightWalletEndpoint: LightWalletEndpoint,
seed: ByteArray?,
birthday: BlockHeight?
): Synchronizer { ): Synchronizer {
val repository = DefaultSynchronizerFactory.defaultTransactionRepository(initializer, seed) val applicationContext = context.applicationContext
val blockStore = DefaultSynchronizerFactory.defaultBlockStore(initializer)
val service = DefaultSynchronizerFactory.defaultService(initializer) validateAlias(alias)
val encoder = DefaultSynchronizerFactory.defaultEncoder(initializer, repository)
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 downloader = DefaultSynchronizerFactory.defaultDownloader(service, blockStore)
val txManager = val txManager =
DefaultSynchronizerFactory.defaultTxManager(initializer, encoder, service) DefaultSynchronizerFactory.defaultTxManager(applicationContext, zcashNetwork, alias, encoder, service)
val processor = val processor =
DefaultSynchronizerFactory.defaultProcessor(initializer, downloader, repository) DefaultSynchronizerFactory.defaultProcessor(rustBackend, downloader, repository)
return SdkSynchronizer( return SdkSynchronizer(
repository, repository,
@ -501,8 +546,57 @@ interface Synchronizer {
* This is a blocking call, so it should not be called from the main thread. * This is a blocking call, so it should not be called from the main thread.
*/ */
@JvmStatic @JvmStatic
fun newBlocking(initializer: Initializer, seed: ByteArray): Synchronizer = runBlocking { @Suppress("LongParameterList")
new (initializer, seed) 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."
} }
} }

View File

@ -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.TransactionEntity
import cash.z.ecc.android.sdk.db.entity.Utxo import cash.z.ecc.android.sdk.db.entity.Utxo
import cash.z.ecc.android.sdk.internal.twig import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.type.UnifiedAddressAccount
// //
// Database // Database

View File

@ -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.ConfirmedTransaction
import cash.z.ecc.android.sdk.db.entity.EncodedTransaction import cash.z.ecc.android.sdk.db.entity.EncodedTransaction
import cash.z.ecc.android.sdk.model.BlockHeight import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.type.UnifiedAddressAccount
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
/** /**