[#742] Prevent multiple synchronizer instances
Co-authored-by: Honza <rychnovsky.honza@gmail.com>
This commit is contained in:
parent
97c0628798
commit
2ab6d038ed
|
@ -40,6 +40,8 @@ Change Log
|
|||
- `Initializer.Config.setViewingKeys`
|
||||
- `cash.z.ecc.android.sdk`:
|
||||
- `Synchronizer.Companion.new` now takes many of the arguments previously passed to `Initializer`. In addition, an optional `seed` argument is required for first-time initialization or if `Synchronizer.new` throws an exception indicating that an internal migration requires the wallet seed. (This second case will be true the first time existing clients upgrade to this new version of the SDK).
|
||||
- `Synchronizer.new()` now returns an instance that implements the `Closeable` interface. `Synchronizer.stop()` is effectively renamed to `Synchronizer.close()`
|
||||
- `Synchronizer` ensures that multiple instances cannot be running concurrently with the same network and alias
|
||||
- `Synchronizer.sendToAddress` now takes a `UnifiedSpendingKey` instead of an encoded
|
||||
Sapling extended spending key, and the `fromAccountIndex` argument is now implicit in
|
||||
the `UnifiedSpendingKey`.
|
||||
|
@ -51,6 +53,7 @@ Change Log
|
|||
### Removed
|
||||
- `cash.z.ecc.android.sdk`:
|
||||
- `Initializer` (use `Synchronizer.new` instead)
|
||||
- `Synchronizer.start()` - Synchronizer is now started automatically when constructing a new instance.
|
||||
- `Synchronizer.getAddress` (use `Synchronizer.getUnifiedAddress` instead).
|
||||
- `Synchronizer.getShieldedAddress` (use `Synchronizer.getSaplingAddress` instead)
|
||||
- `Synchronizer.cancel`
|
||||
|
|
|
@ -1,12 +1,24 @@
|
|||
Troubleshooting Migrations
|
||||
==========
|
||||
|
||||
Migration to Version 1.10
|
||||
Migration to Version 1.11
|
||||
---------------------------------
|
||||
The way the SDK is initialized has changed. The `Initializer` object has been removed and `Synchronizer.new` now takes a longer parameter list which includes the parameters previously passed to `Initializer`.
|
||||
|
||||
SDK initialization also now requires access to the seed bytes at two times: 1. during new wallet creation and 2. during upgrade of an existing wallet to SDK 1.10 due to internal data migrations. To handle case #2, client should wrap `Synchronizer.new()` with a try-catch for `InitializerException.SeedRequired`. Clients can pass `null` to try to initialize the SDK without the seed, then try again if the exception is thrown to indicate the seed is needed. This pattern future-proofs initialization, as the seed may be required by future SDK updates.
|
||||
|
||||
`Synchronizer.stop()` has been removed. `Synchronizer.new()` now returns an instance that implements the `Closeable` interface. This effectively means that calls to `stop()` are replaced with `close()`. This change also enables greater safety within client applications, as the Closeable interface can be hidden from global synchronizer instances. For exmaple:
|
||||
```
|
||||
val synchronizerFlow: Flow<Synchronizer> = callbackFlow<Synchronizer> {
|
||||
val closeableSynchronizer: CloseableSynchronizer = Synchronizer.new(...)
|
||||
|
||||
send(closeableSynchronizer)
|
||||
awaitClose {
|
||||
closeableSynchronizer.close()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
To improve type safety of the public API, Zcash account indexes are now represented by an `Account` object. The SDK currently only supports the default account, `Account.DEFAULT`. Migration will effectively require replacing APIs with an account `0` with `Account.DEFAULT`.
|
||||
|
||||
To support Network Upgrade 5, the way keys are generated has changed.
|
||||
|
|
|
@ -97,7 +97,6 @@ class InboundTxTests : ScopedTest() {
|
|||
.stageEmptyBlocks(firstBlock + 1, 100)
|
||||
.applyTipHeight(BlockHeight.new(ZcashNetwork.Mainnet, targetTxBlock.value - 1))
|
||||
|
||||
sithLord.synchronizer.start(classScope)
|
||||
sithLord.await()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,7 +42,6 @@ class ReorgSetupTest : ScopedTest() {
|
|||
@JvmStatic
|
||||
fun startOnce() {
|
||||
sithLord.enterTheDarkside()
|
||||
sithLord.synchronizer.start(classScope)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -60,7 +60,6 @@ class ReorgSmallTest : ScopedTest() {
|
|||
validator.onReorg { _, _ ->
|
||||
hadReorg = true
|
||||
}
|
||||
sithLord.synchronizer.start(classScope)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ open class DarksideTest : ScopedTest() {
|
|||
fun runOnce(block: () -> Unit) {
|
||||
if (!ranOnce) {
|
||||
sithLord.enterTheDarkside()
|
||||
sithLord.synchronizer.start(classScope)
|
||||
block()
|
||||
ranOnce = true
|
||||
}
|
||||
|
|
|
@ -97,12 +97,8 @@ class TestWallet(
|
|||
throw TimeoutException("Failed to sync wallet within ${timeout}ms")
|
||||
}
|
||||
}
|
||||
if (!synchronizer.isStarted) {
|
||||
twig("Starting sync")
|
||||
synchronizer.start(walletScope)
|
||||
} else {
|
||||
twig("Awaiting next SYNCED status")
|
||||
}
|
||||
|
||||
twig("Awaiting next SYNCED status")
|
||||
|
||||
// block until synced
|
||||
synchronizer.status.first { it == Synchronizer.Status.SYNCED }
|
||||
|
@ -154,7 +150,7 @@ class TestWallet(
|
|||
twig("Scheduling a stop in ${timeout}ms")
|
||||
walletScope.launch {
|
||||
delay(timeout)
|
||||
synchronizer.stop()
|
||||
synchronizer.close()
|
||||
}
|
||||
}
|
||||
synchronizer.status.first { it == Synchronizer.Status.STOPPED }
|
||||
|
|
|
@ -4,15 +4,36 @@ import android.app.Application
|
|||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import cash.z.ecc.android.bip39.Mnemonics
|
||||
import cash.z.ecc.android.bip39.toSeed
|
||||
import cash.z.ecc.android.sdk.Synchronizer
|
||||
import cash.z.ecc.android.sdk.demoapp.util.fromResources
|
||||
import cash.z.ecc.android.sdk.ext.BenchmarkingExt
|
||||
import cash.z.ecc.android.sdk.ext.onFirst
|
||||
import cash.z.ecc.android.sdk.fixture.BlockRangeFixture
|
||||
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.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import java.util.UUID
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* Shared mutable state for the demo
|
||||
|
@ -36,6 +57,54 @@ class SharedViewModel(application: Application) : AndroidViewModel(application)
|
|||
// publicly, this is read-only
|
||||
val birthdayHeight: StateFlow<BlockHeight?> get() = _blockHeight
|
||||
|
||||
private val lockoutMutex = Mutex()
|
||||
|
||||
private val lockoutIdFlow = MutableStateFlow<UUID?>(null)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private val synchronizerOrLockout: Flow<InternalSynchronizerStatus> = lockoutIdFlow.flatMapLatest { lockoutId ->
|
||||
if (null != lockoutId) {
|
||||
flowOf(InternalSynchronizerStatus.Lockout(lockoutId))
|
||||
} else {
|
||||
callbackFlow<InternalSynchronizerStatus> {
|
||||
// Use a BIP-39 library to convert a seed phrase into a byte array. Most wallets already
|
||||
// have the seed stored
|
||||
val seedBytes = Mnemonics.MnemonicCode(seedPhrase.value).toSeed()
|
||||
|
||||
val network = ZcashNetwork.fromResources(application)
|
||||
val synchronizer = Synchronizer.new(
|
||||
application,
|
||||
network,
|
||||
lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network),
|
||||
seed = seedBytes,
|
||||
birthday = if (BenchmarkingExt.isBenchmarking()) {
|
||||
BlockRangeFixture.new().start
|
||||
} else {
|
||||
birthdayHeight.value
|
||||
}
|
||||
)
|
||||
|
||||
send(InternalSynchronizerStatus.Available(synchronizer))
|
||||
awaitClose {
|
||||
synchronizer.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Note that seed and birthday shouldn't be changed once a synchronizer is first collected
|
||||
val synchronizerFlow: StateFlow<Synchronizer?> = synchronizerOrLockout.map {
|
||||
when (it) {
|
||||
is InternalSynchronizerStatus.Available -> it.synchronizer
|
||||
is InternalSynchronizerStatus.Lockout -> null
|
||||
}
|
||||
}.stateIn(
|
||||
viewModelScope,
|
||||
SharingStarted.WhileSubscribed(DEFAULT_ANDROID_STATE_TIMEOUT.inWholeMilliseconds, 0),
|
||||
initialValue =
|
||||
null
|
||||
)
|
||||
|
||||
fun updateSeedPhrase(newPhrase: String?): Boolean {
|
||||
return if (isValidSeedPhrase(newPhrase)) {
|
||||
_seedPhrase.value = newPhrase!!
|
||||
|
@ -47,11 +116,22 @@ class SharedViewModel(application: Application) : AndroidViewModel(application)
|
|||
|
||||
fun resetSDK() {
|
||||
viewModelScope.launch {
|
||||
with(getApplication<Application>()) {
|
||||
Synchronizer.erase(
|
||||
appContext = applicationContext,
|
||||
network = ZcashNetwork.fromResources(applicationContext)
|
||||
)
|
||||
lockoutMutex.withLock {
|
||||
val lockoutId = UUID.randomUUID()
|
||||
lockoutIdFlow.value = lockoutId
|
||||
|
||||
synchronizerOrLockout
|
||||
.filterIsInstance<InternalSynchronizerStatus.Lockout>()
|
||||
.filter { it.id == lockoutId }
|
||||
.onFirst {
|
||||
val didDelete = Synchronizer.erase(
|
||||
appContext = getApplication(),
|
||||
network = ZcashNetwork.fromResources(getApplication())
|
||||
)
|
||||
twig("SDK erase result: $didDelete")
|
||||
}
|
||||
|
||||
lockoutIdFlow.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -74,4 +154,13 @@ class SharedViewModel(application: Application) : AndroidViewModel(application)
|
|||
false
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class InternalSynchronizerStatus {
|
||||
class Available(val synchronizer: Synchronizer) : InternalSynchronizerStatus()
|
||||
class Lockout(val id: UUID) : InternalSynchronizerStatus()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DEFAULT_ANDROID_STATE_TIMEOUT = 5.seconds
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,22 +2,20 @@ package cash.z.ecc.android.sdk.demoapp.demos.getaddress
|
|||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import cash.z.ecc.android.bip39.Mnemonics
|
||||
import cash.z.ecc.android.bip39.toSeed
|
||||
import cash.z.ecc.android.sdk.Synchronizer
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment
|
||||
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentGetAddressBinding
|
||||
import cash.z.ecc.android.sdk.demoapp.ext.requireApplicationContext
|
||||
import cash.z.ecc.android.sdk.demoapp.util.ProvideAddressBenchmarkTrace
|
||||
import cash.z.ecc.android.sdk.demoapp.util.fromResources
|
||||
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.model.defaultForNetwork
|
||||
import cash.z.ecc.android.sdk.tool.DerivationTool
|
||||
import cash.z.ecc.android.sdk.type.UnifiedFullViewingKey
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
/**
|
||||
* Displays the address associated with the seed defined by the default config. To modify the seed
|
||||
|
@ -25,62 +23,36 @@ import kotlinx.coroutines.runBlocking
|
|||
*/
|
||||
class GetAddressFragment : BaseDemoFragment<FragmentGetAddressBinding>() {
|
||||
|
||||
private lateinit var synchronizer: Synchronizer
|
||||
private lateinit var viewingKey: UnifiedFullViewingKey
|
||||
private lateinit var seed: ByteArray
|
||||
|
||||
/**
|
||||
* Initialize the required values that would normally live outside the demo but are repeated
|
||||
* here for completeness so that each demo file can serve as a standalone example.
|
||||
*/
|
||||
private fun setup() {
|
||||
// defaults to the value of `DemoConfig.seedWords` but can also be set by the user
|
||||
val seedPhrase = sharedViewModel.seedPhrase.value
|
||||
|
||||
// Use a BIP-39 library to convert a seed phrase into a byte array. Most wallets already
|
||||
// have the seed stored
|
||||
seed = Mnemonics.MnemonicCode(seedPhrase).toSeed()
|
||||
|
||||
// converting seed into viewingKey
|
||||
viewingKey = runBlocking {
|
||||
DerivationTool.deriveUnifiedFullViewingKeys(
|
||||
seed,
|
||||
ZcashNetwork.fromResources(requireApplicationContext())
|
||||
).first()
|
||||
}
|
||||
|
||||
val network = ZcashNetwork.fromResources(requireApplicationContext())
|
||||
synchronizer = Synchronizer.newBlocking(
|
||||
requireApplicationContext(),
|
||||
network,
|
||||
lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network),
|
||||
seed = seed,
|
||||
birthday = network.saplingActivationHeight
|
||||
)
|
||||
}
|
||||
|
||||
private fun displayAddress() {
|
||||
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
|
||||
binding.unifiedAddress.apply {
|
||||
reportTraceEvent(ProvideAddressBenchmarkTrace.Event.UNIFIED_ADDRESS_START)
|
||||
val uaddress = synchronizer.getUnifiedAddress()
|
||||
reportTraceEvent(ProvideAddressBenchmarkTrace.Event.UNIFIED_ADDRESS_END)
|
||||
text = uaddress
|
||||
setOnClickListener { copyToClipboard(uaddress) }
|
||||
}
|
||||
binding.saplingAddress.apply {
|
||||
reportTraceEvent(ProvideAddressBenchmarkTrace.Event.SAPLING_ADDRESS_START)
|
||||
val sapling = synchronizer.getSaplingAddress()
|
||||
reportTraceEvent(ProvideAddressBenchmarkTrace.Event.SAPLING_ADDRESS_END)
|
||||
text = sapling
|
||||
setOnClickListener { copyToClipboard(sapling) }
|
||||
}
|
||||
binding.transparentAddress.apply {
|
||||
reportTraceEvent(ProvideAddressBenchmarkTrace.Event.TRANSPARENT_ADDRESS_START)
|
||||
val transparent = synchronizer.getTransparentAddress()
|
||||
reportTraceEvent(ProvideAddressBenchmarkTrace.Event.TRANSPARENT_ADDRESS_END)
|
||||
text = transparent
|
||||
setOnClickListener { copyToClipboard(transparent) }
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
launch {
|
||||
sharedViewModel.synchronizerFlow.filterNotNull().collect { synchronizer ->
|
||||
binding.unifiedAddress.apply {
|
||||
reportTraceEvent(ProvideAddressBenchmarkTrace.Event.UNIFIED_ADDRESS_START)
|
||||
val uaddress = synchronizer.getUnifiedAddress()
|
||||
reportTraceEvent(ProvideAddressBenchmarkTrace.Event.UNIFIED_ADDRESS_END)
|
||||
text = uaddress
|
||||
setOnClickListener { copyToClipboard(uaddress) }
|
||||
}
|
||||
binding.saplingAddress.apply {
|
||||
reportTraceEvent(ProvideAddressBenchmarkTrace.Event.SAPLING_ADDRESS_START)
|
||||
val sapling = synchronizer.getSaplingAddress()
|
||||
reportTraceEvent(ProvideAddressBenchmarkTrace.Event.SAPLING_ADDRESS_END)
|
||||
text = sapling
|
||||
setOnClickListener { copyToClipboard(sapling) }
|
||||
}
|
||||
binding.transparentAddress.apply {
|
||||
reportTraceEvent(ProvideAddressBenchmarkTrace.Event.TRANSPARENT_ADDRESS_START)
|
||||
val transparent = synchronizer.getTransparentAddress()
|
||||
reportTraceEvent(ProvideAddressBenchmarkTrace.Event.TRANSPARENT_ADDRESS_END)
|
||||
text = transparent
|
||||
setOnClickListener { copyToClipboard(transparent) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -92,11 +64,11 @@ class GetAddressFragment : BaseDemoFragment<FragmentGetAddressBinding>() {
|
|||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
reportTraceEvent(ProvideAddressBenchmarkTrace.Event.ADDRESS_SCREEN_START)
|
||||
setup()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
displayAddress()
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,9 @@ import android.os.Bundle
|
|||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.View
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import cash.z.ecc.android.bip39.Mnemonics
|
||||
import cash.z.ecc.android.bip39.toSeed
|
||||
import cash.z.ecc.android.sdk.Synchronizer
|
||||
|
@ -15,19 +17,17 @@ import cash.z.ecc.android.sdk.demoapp.databinding.FragmentGetBalanceBinding
|
|||
import cash.z.ecc.android.sdk.demoapp.ext.requireApplicationContext
|
||||
import cash.z.ecc.android.sdk.demoapp.util.SyncBlockchainBenchmarkTrace
|
||||
import cash.z.ecc.android.sdk.demoapp.util.fromResources
|
||||
import cash.z.ecc.android.sdk.ext.BenchmarkingExt
|
||||
import cash.z.ecc.android.sdk.ext.ZcashSdk
|
||||
import cash.z.ecc.android.sdk.ext.collectWith
|
||||
import cash.z.ecc.android.sdk.ext.convertZatoshiToZecString
|
||||
import cash.z.ecc.android.sdk.fixture.BlockRangeFixture
|
||||
import cash.z.ecc.android.sdk.internal.twig
|
||||
import cash.z.ecc.android.sdk.model.Account
|
||||
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
|
||||
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.model.defaultForNetwork
|
||||
import cash.z.ecc.android.sdk.tool.DerivationTool
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
|
@ -37,8 +37,6 @@ import kotlinx.coroutines.launch
|
|||
@Suppress("TooManyFunctions")
|
||||
class GetBalanceFragment : BaseDemoFragment<FragmentGetBalanceBinding>() {
|
||||
|
||||
private lateinit var synchronizer: Synchronizer
|
||||
|
||||
override fun inflateBinding(layoutInflater: LayoutInflater): FragmentGetBalanceBinding =
|
||||
FragmentGetBalanceBinding.inflate(layoutInflater)
|
||||
|
||||
|
@ -46,7 +44,6 @@ class GetBalanceFragment : BaseDemoFragment<FragmentGetBalanceBinding>() {
|
|||
super.onCreate(savedInstanceState)
|
||||
reportTraceEvent(SyncBlockchainBenchmarkTrace.Event.BALANCE_SCREEN_START)
|
||||
setHasOptionsMenu(true)
|
||||
setup()
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
|
@ -60,28 +57,6 @@ class GetBalanceFragment : BaseDemoFragment<FragmentGetBalanceBinding>() {
|
|||
reportTraceEvent(SyncBlockchainBenchmarkTrace.Event.BALANCE_SCREEN_END)
|
||||
}
|
||||
|
||||
private fun setup() {
|
||||
// defaults to the value of `DemoConfig.seedWords` but can also be set by the user
|
||||
val seedPhrase = sharedViewModel.seedPhrase.value
|
||||
|
||||
// Use a BIP-39 library to convert a seed phrase into a byte array. Most wallets already
|
||||
// have the seed stored
|
||||
val seed = Mnemonics.MnemonicCode(seedPhrase).toSeed()
|
||||
|
||||
val network = ZcashNetwork.fromResources(requireApplicationContext())
|
||||
synchronizer = Synchronizer.newBlocking(
|
||||
requireApplicationContext(),
|
||||
network,
|
||||
lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network),
|
||||
seed = seed,
|
||||
birthday = if (BenchmarkingExt.isBenchmarking()) {
|
||||
BlockRangeFixture.new().start
|
||||
} else {
|
||||
sharedViewModel.birthdayHeight.value
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
|
@ -92,26 +67,62 @@ class GetBalanceFragment : BaseDemoFragment<FragmentGetBalanceBinding>() {
|
|||
binding.shield.apply {
|
||||
setOnClickListener {
|
||||
lifecycleScope.launch {
|
||||
synchronizer.shieldFunds(DerivationTool.deriveUnifiedSpendingKey(seed, network, Account.DEFAULT))
|
||||
sharedViewModel.synchronizerFlow.value?.shieldFunds(
|
||||
DerivationTool.deriveUnifiedSpendingKey(
|
||||
seed,
|
||||
network,
|
||||
Account.DEFAULT
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
// the lifecycleScope is used to dispose of the synchronize when the fragment dies
|
||||
synchronizer.start(lifecycleScope)
|
||||
monitorChanges()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private fun monitorChanges() {
|
||||
synchronizer.status.collectWith(lifecycleScope, ::onStatus)
|
||||
synchronizer.progress.collectWith(lifecycleScope, ::onProgress)
|
||||
synchronizer.processorInfo.collectWith(lifecycleScope, ::onProcessorInfoUpdated)
|
||||
synchronizer.orchardBalances.collectWith(lifecycleScope, ::onOrchardBalance)
|
||||
synchronizer.saplingBalances.collectWith(lifecycleScope, ::onSaplingBalance)
|
||||
synchronizer.transparentBalances.collectWith(lifecycleScope, ::onTransparentBalance)
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
launch {
|
||||
sharedViewModel.synchronizerFlow
|
||||
.filterNotNull()
|
||||
.flatMapLatest { it.status }
|
||||
.collect { onStatus(it) }
|
||||
}
|
||||
launch {
|
||||
sharedViewModel.synchronizerFlow
|
||||
.filterNotNull()
|
||||
.flatMapLatest { it.progress }
|
||||
.collect { onProgress(it) }
|
||||
}
|
||||
launch {
|
||||
sharedViewModel.synchronizerFlow
|
||||
.filterNotNull()
|
||||
.flatMapLatest { it.processorInfo }
|
||||
.collect { onProcessorInfoUpdated(it) }
|
||||
}
|
||||
launch {
|
||||
sharedViewModel.synchronizerFlow
|
||||
.filterNotNull()
|
||||
.flatMapLatest { it.saplingBalances }
|
||||
.collect { onSaplingBalance(it) }
|
||||
}
|
||||
launch {
|
||||
sharedViewModel.synchronizerFlow
|
||||
.filterNotNull()
|
||||
.flatMapLatest { it.orchardBalances }
|
||||
.collect { onOrchardBalance(it) }
|
||||
}
|
||||
launch {
|
||||
sharedViewModel.synchronizerFlow
|
||||
.filterNotNull()
|
||||
.flatMapLatest { it.transparentBalances }
|
||||
.collect { onTransparentBalance(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onOrchardBalance(
|
||||
|
@ -181,9 +192,11 @@ class GetBalanceFragment : BaseDemoFragment<FragmentGetBalanceBinding>() {
|
|||
traceEvents?.forEach { reportTraceEvent(it) }
|
||||
|
||||
binding.textStatus.text = "Status: $status"
|
||||
onOrchardBalance(synchronizer.orchardBalances.value)
|
||||
onSaplingBalance(synchronizer.saplingBalances.value)
|
||||
onTransparentBalance(synchronizer.transparentBalances.value)
|
||||
sharedViewModel.synchronizerFlow.value?.let { synchronizer ->
|
||||
onOrchardBalance(synchronizer.orchardBalances.value)
|
||||
onSaplingBalance(synchronizer.saplingBalances.value)
|
||||
onTransparentBalance(synchronizer.transparentBalances.value)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
|
|
|
@ -4,27 +4,21 @@ import android.os.Bundle
|
|||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
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.Synchronizer
|
||||
import cash.z.ecc.android.sdk.block.CompactBlockProcessor
|
||||
import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment
|
||||
import cash.z.ecc.android.sdk.demoapp.R
|
||||
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentListTransactionsBinding
|
||||
import cash.z.ecc.android.sdk.demoapp.ext.requireApplicationContext
|
||||
import cash.z.ecc.android.sdk.demoapp.util.fromResources
|
||||
import cash.z.ecc.android.sdk.ext.collectWith
|
||||
import cash.z.ecc.android.sdk.internal.twig
|
||||
import cash.z.ecc.android.sdk.model.Account
|
||||
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
|
||||
import cash.z.ecc.android.sdk.model.TransactionOverview
|
||||
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.runBlocking
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* List all transactions related to the given seed, since the given birthday. This begins by
|
||||
|
@ -35,40 +29,11 @@ import kotlinx.coroutines.runBlocking
|
|||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
class ListTransactionsFragment : BaseDemoFragment<FragmentListTransactionsBinding>() {
|
||||
private lateinit var synchronizer: Synchronizer
|
||||
private lateinit var adapter: TransactionAdapter
|
||||
private lateinit var address: String
|
||||
private var status: Synchronizer.Status? = null
|
||||
private val isSynced get() = status == Synchronizer.Status.SYNCED
|
||||
|
||||
/**
|
||||
* Initialize the required values that would normally live outside the demo but are repeated
|
||||
* here for completeness so that each demo file can serve as a standalone example.
|
||||
*/
|
||||
private fun setup() {
|
||||
// defaults to the value of `DemoConfig.seedWords` but can also be set by the user
|
||||
var seedPhrase = sharedViewModel.seedPhrase.value
|
||||
|
||||
// Use a BIP-39 library to convert a seed phrase into a byte array. Most wallets already
|
||||
// have the seed stored
|
||||
val seed = Mnemonics.MnemonicCode(seedPhrase).toSeed()
|
||||
val network = ZcashNetwork.fromResources(requireApplicationContext())
|
||||
address = runBlocking {
|
||||
DerivationTool.deriveUnifiedAddress(
|
||||
seed,
|
||||
ZcashNetwork.fromResources(requireApplicationContext()),
|
||||
Account.DEFAULT
|
||||
)
|
||||
}
|
||||
synchronizer = Synchronizer.newBlocking(
|
||||
requireApplicationContext(),
|
||||
network,
|
||||
lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network),
|
||||
seed = seed,
|
||||
birthday = sharedViewModel.birthdayHeight.value
|
||||
)
|
||||
}
|
||||
|
||||
private fun initTransactionUI() {
|
||||
binding.recyclerTransactions.layoutManager =
|
||||
LinearLayoutManager(activity, LinearLayoutManager.VERTICAL, false)
|
||||
|
@ -76,14 +41,35 @@ class ListTransactionsFragment : BaseDemoFragment<FragmentListTransactionsBindin
|
|||
binding.recyclerTransactions.adapter = adapter
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private fun monitorChanges() {
|
||||
// the lifecycleScope is used to stop everything when the fragment dies
|
||||
synchronizer.status.collectWith(lifecycleScope, ::onStatus)
|
||||
synchronizer.processorInfo.collectWith(lifecycleScope, ::onProcessorInfoUpdated)
|
||||
synchronizer.progress.collectWith(lifecycleScope, ::onProgress)
|
||||
|
||||
synchronizer.clearedTransactions.collectWith(lifecycleScope) {
|
||||
onTransactionsUpdated(it)
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
launch {
|
||||
sharedViewModel.synchronizerFlow
|
||||
.filterNotNull()
|
||||
.flatMapLatest { it.status }
|
||||
.collect { onStatus(it) }
|
||||
}
|
||||
launch {
|
||||
sharedViewModel.synchronizerFlow
|
||||
.filterNotNull()
|
||||
.flatMapLatest { it.progress }
|
||||
.collect { onProgress(it) }
|
||||
}
|
||||
launch {
|
||||
sharedViewModel.synchronizerFlow
|
||||
.filterNotNull()
|
||||
.flatMapLatest { it.processorInfo }
|
||||
.collect { onProcessorInfoUpdated(it) }
|
||||
}
|
||||
launch {
|
||||
sharedViewModel.synchronizerFlow
|
||||
.filterNotNull()
|
||||
.flatMapLatest { it.clearedTransactions }
|
||||
.collect { onTransactionsUpdated(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -139,19 +125,10 @@ class ListTransactionsFragment : BaseDemoFragment<FragmentListTransactionsBindin
|
|||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val view = super.onCreateView(inflater, container, savedInstanceState)
|
||||
setup()
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
initTransactionUI()
|
||||
monitorChanges()
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
|
@ -160,13 +137,6 @@ class ListTransactionsFragment : BaseDemoFragment<FragmentListTransactionsBindin
|
|||
menu.setGroupVisible(R.id.main_menu_group, false)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
// the lifecycleScope is used to dispose of the synchronizer when the fragment dies
|
||||
synchronizer.start(lifecycleScope)
|
||||
monitorChanges()
|
||||
}
|
||||
|
||||
//
|
||||
// Base Fragment overrides
|
||||
//
|
||||
|
|
|
@ -4,10 +4,10 @@ import android.content.Context
|
|||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
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.SdkSynchronizer
|
||||
import cash.z.ecc.android.sdk.Synchronizer
|
||||
import cash.z.ecc.android.sdk.block.CompactBlockProcessor
|
||||
|
@ -16,17 +16,16 @@ import cash.z.ecc.android.sdk.demoapp.databinding.FragmentListUtxosBinding
|
|||
import cash.z.ecc.android.sdk.demoapp.ext.requireApplicationContext
|
||||
import cash.z.ecc.android.sdk.demoapp.util.fromResources
|
||||
import cash.z.ecc.android.sdk.demoapp.util.mainActivity
|
||||
import cash.z.ecc.android.sdk.ext.collectWith
|
||||
import cash.z.ecc.android.sdk.internal.twig
|
||||
import cash.z.ecc.android.sdk.model.Account
|
||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
|
||||
import cash.z.ecc.android.sdk.model.TransactionOverview
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.model.defaultForNetwork
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.math.max
|
||||
|
@ -45,8 +44,6 @@ import kotlin.math.max
|
|||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
class ListUtxosFragment : BaseDemoFragment<FragmentListUtxosBinding>() {
|
||||
private lateinit var seed: ByteArray
|
||||
private lateinit var synchronizer: Synchronizer
|
||||
private lateinit var adapter: UtxoAdapter
|
||||
private val address: String = "t1RwbKka1CnktvAJ1cSqdn7c6PXWG4tZqgd"
|
||||
private var status: Synchronizer.Status? = null
|
||||
|
@ -56,30 +53,6 @@ class ListUtxosFragment : BaseDemoFragment<FragmentListUtxosBinding>() {
|
|||
override fun inflateBinding(layoutInflater: LayoutInflater): FragmentListUtxosBinding =
|
||||
FragmentListUtxosBinding.inflate(layoutInflater)
|
||||
|
||||
/**
|
||||
* Initialize the required values that would normally live outside the demo but are repeated
|
||||
* here for completeness so that each demo file can serve as a standalone example.
|
||||
*/
|
||||
private fun setup() {
|
||||
// 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()
|
||||
val network = ZcashNetwork.fromResources(requireApplicationContext())
|
||||
synchronizer = Synchronizer.newBlocking(
|
||||
requireApplicationContext(),
|
||||
network,
|
||||
alias = "Demo_Utxos",
|
||||
lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network),
|
||||
seed = seed,
|
||||
birthday = sharedViewModel.birthdayHeight.value
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setup()
|
||||
}
|
||||
|
||||
private fun initUi() {
|
||||
binding.inputAddress.setText(address)
|
||||
binding.inputRangeStart.setText(
|
||||
|
@ -96,57 +69,59 @@ class ListUtxosFragment : BaseDemoFragment<FragmentListUtxosBinding>() {
|
|||
}
|
||||
|
||||
private fun downloadTransactions() {
|
||||
binding.textStatus.text = "loading..."
|
||||
binding.textStatus.post {
|
||||
val network = ZcashNetwork.fromResources(requireApplicationContext())
|
||||
binding.textStatus.requestFocus()
|
||||
val addressToUse = binding.inputAddress.text.toString()
|
||||
val startToUse = max(
|
||||
binding.inputRangeStart.text.toString().toLongOrNull()
|
||||
?: network.saplingActivationHeight.value,
|
||||
network.saplingActivationHeight.value
|
||||
)
|
||||
val endToUse = binding.inputRangeEnd.text.toString().toLongOrNull()
|
||||
?: getUxtoEndHeight(requireApplicationContext()).value
|
||||
var allStart = now
|
||||
twig("loading transactions in range $startToUse..$endToUse")
|
||||
val txids = lightWalletService?.getTAddressTransactions(
|
||||
addressToUse,
|
||||
BlockHeight.new(network, startToUse)..BlockHeight.new(network, endToUse)
|
||||
)
|
||||
var delta = now - allStart
|
||||
updateStatus("found ${txids?.size} transactions in ${delta}ms.", false)
|
||||
sharedViewModel.synchronizerFlow.value?.let { synchronizer ->
|
||||
binding.textStatus.text = "loading..."
|
||||
binding.textStatus.post {
|
||||
val network = ZcashNetwork.fromResources(requireApplicationContext())
|
||||
binding.textStatus.requestFocus()
|
||||
val addressToUse = binding.inputAddress.text.toString()
|
||||
val startToUse = max(
|
||||
binding.inputRangeStart.text.toString().toLongOrNull()
|
||||
?: network.saplingActivationHeight.value,
|
||||
network.saplingActivationHeight.value
|
||||
)
|
||||
val endToUse = binding.inputRangeEnd.text.toString().toLongOrNull()
|
||||
?: getUxtoEndHeight(requireApplicationContext()).value
|
||||
var allStart = now
|
||||
twig("loading transactions in range $startToUse..$endToUse")
|
||||
val txids = lightWalletService?.getTAddressTransactions(
|
||||
addressToUse,
|
||||
BlockHeight.new(network, startToUse)..BlockHeight.new(network, endToUse)
|
||||
)
|
||||
var delta = now - allStart
|
||||
updateStatus("found ${txids?.size} transactions in ${delta}ms.", false)
|
||||
|
||||
txids?.map {
|
||||
// Disabled during migration to newer SDK version; this appears to have been
|
||||
// leveraging non-public APIs in the SDK so perhaps should be removed
|
||||
// it.data.apply {
|
||||
// try {
|
||||
// runBlocking { initializer.rustBackend.decryptAndStoreTransaction(toByteArray()) }
|
||||
// } catch (t: Throwable) {
|
||||
// twig("failed to decrypt and store transaction due to: $t")
|
||||
// }
|
||||
// }
|
||||
}?.let { _ ->
|
||||
// Disabled during migration to newer SDK version; this appears to have been
|
||||
// leveraging non-public APIs in the SDK so perhaps should be removed
|
||||
// val parseStart = now
|
||||
// val tList = LocalRpcTypes.TransactionDataList.newBuilder().addAllData(txData).build()
|
||||
// val parsedTransactions = initializer.rustBackend.parseTransactionDataList(tList)
|
||||
// delta = now - parseStart
|
||||
// updateStatus("parsed txs in ${delta}ms.")
|
||||
}
|
||||
(synchronizer as SdkSynchronizer).refreshTransactions()
|
||||
delta = now - allStart
|
||||
updateStatus("Total time ${delta}ms.")
|
||||
txids?.map {
|
||||
// Disabled during migration to newer SDK version; this appears to have been
|
||||
// leveraging non-public APIs in the SDK so perhaps should be removed
|
||||
// it.data.apply {
|
||||
// try {
|
||||
// runBlocking { initializer.rustBackend.decryptAndStoreTransaction(toByteArray()) }
|
||||
// } catch (t: Throwable) {
|
||||
// twig("failed to decrypt and store transaction due to: $t")
|
||||
// }
|
||||
// }
|
||||
}?.let { _ ->
|
||||
// Disabled during migration to newer SDK version; this appears to have been
|
||||
// leveraging non-public APIs in the SDK so perhaps should be removed
|
||||
// val parseStart = now
|
||||
// val tList = LocalRpcTypes.TransactionDataList.newBuilder().addAllData(txData).build()
|
||||
// val parsedTransactions = initializer.rustBackend.parseTransactionDataList(tList)
|
||||
// delta = now - parseStart
|
||||
// updateStatus("parsed txs in ${delta}ms.")
|
||||
}
|
||||
(synchronizer as SdkSynchronizer).refreshTransactions()
|
||||
delta = now - allStart
|
||||
updateStatus("Total time ${delta}ms.")
|
||||
|
||||
lifecycleScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
finalCount = (synchronizer as SdkSynchronizer).getTransactionCount().toInt()
|
||||
withContext(Dispatchers.Main) {
|
||||
@Suppress("MagicNumber")
|
||||
delay(100)
|
||||
updateStatus("Also found ${finalCount - initialCount} shielded txs")
|
||||
lifecycleScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
finalCount = synchronizer.getTransactionCount()
|
||||
withContext(Dispatchers.Main) {
|
||||
@Suppress("MagicNumber")
|
||||
delay(100)
|
||||
updateStatus("Also found ${finalCount - initialCount} shielded txs")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -167,72 +142,58 @@ class ListUtxosFragment : BaseDemoFragment<FragmentListUtxosBinding>() {
|
|||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
initUi()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
resetInBackground()
|
||||
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
|
||||
binding.inputAddress.setText(
|
||||
synchronizer.getTransparentAddress(Account.DEFAULT)
|
||||
)
|
||||
}
|
||||
monitorStatus()
|
||||
}
|
||||
|
||||
var initialCount: Int = 0
|
||||
var finalCount: Int = 0
|
||||
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
private fun resetInBackground() {
|
||||
try {
|
||||
lifecycleScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
synchronizer.prepare()
|
||||
initialCount = (synchronizer as SdkSynchronizer).getTransactionCount().toInt()
|
||||
}
|
||||
|
||||
onTransactionsUpdated(synchronizer.clearedTransactions.first())
|
||||
}
|
||||
|
||||
// synchronizer.receivedTransactions.collectWith(lifecycleScope, ::onTransactionsUpdated)
|
||||
} catch (t: Throwable) {
|
||||
twig("failed to start the synchronizer!!! due to : $t")
|
||||
}
|
||||
}
|
||||
|
||||
fun onResetComplete() {
|
||||
initTransactionUi()
|
||||
startSynchronizer()
|
||||
monitorStatus()
|
||||
}
|
||||
|
||||
fun onClear() {
|
||||
synchronizer.stop()
|
||||
}
|
||||
|
||||
private fun initTransactionUi() {
|
||||
binding.recyclerTransactions.layoutManager =
|
||||
LinearLayoutManager(activity, LinearLayoutManager.VERTICAL, false)
|
||||
adapter = UtxoAdapter()
|
||||
binding.recyclerTransactions.adapter = adapter
|
||||
// lifecycleScope.launch {
|
||||
// address = synchronizer.getAddress()
|
||||
// synchronizer.receivedTransactions.onEach {
|
||||
// onTransactionsUpdated(it)
|
||||
// }.launchIn(this)
|
||||
// }
|
||||
}
|
||||
|
||||
private fun startSynchronizer() {
|
||||
lifecycleScope.apply {
|
||||
synchronizer.start(this)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private fun monitorStatus() {
|
||||
synchronizer.status.collectWith(lifecycleScope, ::onStatus)
|
||||
synchronizer.processorInfo.collectWith(lifecycleScope, ::onProcessorInfoUpdated)
|
||||
synchronizer.progress.collectWith(lifecycleScope, ::onProgress)
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
launch {
|
||||
sharedViewModel.synchronizerFlow
|
||||
.filterNotNull()
|
||||
.flatMapLatest { it.status }
|
||||
.collect { onStatus(it) }
|
||||
}
|
||||
launch {
|
||||
sharedViewModel.synchronizerFlow
|
||||
.filterNotNull()
|
||||
.flatMapLatest { it.progress }
|
||||
.collect { onProgress(it) }
|
||||
}
|
||||
launch {
|
||||
sharedViewModel.synchronizerFlow
|
||||
.filterNotNull()
|
||||
.flatMapLatest { it.processorInfo }
|
||||
.collect { onProcessorInfoUpdated(it) }
|
||||
}
|
||||
launch {
|
||||
sharedViewModel.synchronizerFlow
|
||||
.filterNotNull()
|
||||
.flatMapLatest { it.clearedTransactions }
|
||||
.collect { onTransactionsUpdated(it) }
|
||||
}
|
||||
launch {
|
||||
sharedViewModel.synchronizerFlow
|
||||
.filterNotNull()
|
||||
.collect {
|
||||
binding.inputAddress.setText(
|
||||
it.getTransparentAddress(Account.DEFAULT)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onProcessorInfoUpdated(info: CompactBlockProcessor.ProcessorInfo) {
|
||||
|
@ -262,10 +223,12 @@ class ListUtxosFragment : BaseDemoFragment<FragmentListUtxosBinding>() {
|
|||
override fun onActionButtonClicked() {
|
||||
lifecycleScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
twig("current count: ${(synchronizer as SdkSynchronizer).getTransactionCount()}")
|
||||
twig("refreshing transactions")
|
||||
(synchronizer as SdkSynchronizer).refreshTransactions()
|
||||
twig("current count: ${(synchronizer as SdkSynchronizer).getTransactionCount()}")
|
||||
sharedViewModel.synchronizerFlow.value?.let { synchronizer ->
|
||||
twig("current count: ${(synchronizer as SdkSynchronizer).getTransactionCount()}")
|
||||
twig("refreshing transactions")
|
||||
synchronizer.refreshTransactions()
|
||||
twig("current count: ${synchronizer.getTransactionCount()}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,17 +6,15 @@ import android.view.Menu
|
|||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import cash.z.ecc.android.bip39.Mnemonics
|
||||
import cash.z.ecc.android.bip39.toSeed
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import cash.z.ecc.android.sdk.Synchronizer
|
||||
import cash.z.ecc.android.sdk.block.CompactBlockProcessor
|
||||
import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment
|
||||
import cash.z.ecc.android.sdk.demoapp.DemoConstants
|
||||
import cash.z.ecc.android.sdk.demoapp.R
|
||||
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentSendBinding
|
||||
import cash.z.ecc.android.sdk.demoapp.ext.requireApplicationContext
|
||||
import cash.z.ecc.android.sdk.demoapp.util.fromResources
|
||||
import cash.z.ecc.android.sdk.demoapp.util.mainActivity
|
||||
import cash.z.ecc.android.sdk.ext.collectWith
|
||||
import cash.z.ecc.android.sdk.ext.convertZatoshiToZecString
|
||||
|
@ -24,22 +22,19 @@ import cash.z.ecc.android.sdk.ext.convertZecToZatoshi
|
|||
import cash.z.ecc.android.sdk.ext.toZecString
|
||||
import cash.z.ecc.android.sdk.internal.Twig
|
||||
import cash.z.ecc.android.sdk.internal.twig
|
||||
import cash.z.ecc.android.sdk.model.Account
|
||||
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
|
||||
import cash.z.ecc.android.sdk.model.PendingTransaction
|
||||
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
|
||||
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.model.isCreated
|
||||
import cash.z.ecc.android.sdk.model.isCreating
|
||||
import cash.z.ecc.android.sdk.model.isFailedEncoding
|
||||
import cash.z.ecc.android.sdk.model.isFailedSubmit
|
||||
import cash.z.ecc.android.sdk.model.isMined
|
||||
import cash.z.ecc.android.sdk.model.isSubmitSuccess
|
||||
import cash.z.ecc.android.sdk.tool.DerivationTool
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
/**
|
||||
* Demonstrates sending funds to an address. This is the most complex example that puts all of the
|
||||
|
@ -51,7 +46,6 @@ import kotlinx.coroutines.runBlocking
|
|||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
class SendFragment : BaseDemoFragment<FragmentSendBinding>() {
|
||||
private lateinit var synchronizer: Synchronizer
|
||||
|
||||
private lateinit var amountInput: TextView
|
||||
private lateinit var addressInput: TextView
|
||||
|
@ -60,35 +54,6 @@ class SendFragment : BaseDemoFragment<FragmentSendBinding>() {
|
|||
// but since this is a demo, we'll derive it on the fly
|
||||
private lateinit var spendingKey: UnifiedSpendingKey
|
||||
|
||||
/**
|
||||
* Initialize the required values that would normally live outside the demo but are repeated
|
||||
* here for completeness so that each demo file can serve as a standalone example.
|
||||
*/
|
||||
private fun setup() {
|
||||
// defaults to the value of `DemoConfig.seedWords` but can also be set by the user
|
||||
var seedPhrase = sharedViewModel.seedPhrase.value
|
||||
|
||||
// Use a BIP-39 library to convert a seed phrase into a byte array. Most wallets already
|
||||
// have the seed stored
|
||||
val seed = Mnemonics.MnemonicCode(seedPhrase).toSeed()
|
||||
|
||||
val network = ZcashNetwork.fromResources(requireApplicationContext())
|
||||
synchronizer = Synchronizer.newBlocking(
|
||||
requireApplicationContext(),
|
||||
network,
|
||||
lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network),
|
||||
seed = seed,
|
||||
birthday = sharedViewModel.birthdayHeight.value
|
||||
)
|
||||
spendingKey = runBlocking {
|
||||
DerivationTool.deriveUnifiedSpendingKey(
|
||||
seed,
|
||||
ZcashNetwork.fromResources(requireApplicationContext()),
|
||||
Account.DEFAULT
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Observable properties (done without livedata or flows for simplicity)
|
||||
//
|
||||
|
@ -124,11 +89,36 @@ class SendFragment : BaseDemoFragment<FragmentSendBinding>() {
|
|||
binding.buttonSend.setOnClickListener(::onSend)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private fun monitorChanges() {
|
||||
synchronizer.status.collectWith(lifecycleScope, ::onStatus)
|
||||
synchronizer.progress.collectWith(lifecycleScope, ::onProgress)
|
||||
synchronizer.processorInfo.collectWith(lifecycleScope, ::onProcessorInfoUpdated)
|
||||
synchronizer.saplingBalances.collectWith(lifecycleScope, ::onBalance)
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
launch {
|
||||
sharedViewModel.synchronizerFlow
|
||||
.filterNotNull()
|
||||
.flatMapLatest { it.status }
|
||||
.collect { onStatus(it) }
|
||||
}
|
||||
launch {
|
||||
sharedViewModel.synchronizerFlow
|
||||
.filterNotNull()
|
||||
.flatMapLatest { it.progress }
|
||||
.collect { onProgress(it) }
|
||||
}
|
||||
launch {
|
||||
sharedViewModel.synchronizerFlow
|
||||
.filterNotNull()
|
||||
.flatMapLatest { it.processorInfo }
|
||||
.collect { onProcessorInfoUpdated(it) }
|
||||
}
|
||||
launch {
|
||||
sharedViewModel.synchronizerFlow
|
||||
.filterNotNull()
|
||||
.flatMapLatest { it.saplingBalances }
|
||||
.collect { onBalance(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
@ -176,12 +166,12 @@ class SendFragment : BaseDemoFragment<FragmentSendBinding>() {
|
|||
val amount = amountInput.text.toString().toDouble().convertZecToZatoshi()
|
||||
val toAddress = addressInput.text.toString().trim()
|
||||
lifecycleScope.launch {
|
||||
synchronizer.sendToAddress(
|
||||
sharedViewModel.synchronizerFlow.value?.sendToAddress(
|
||||
spendingKey,
|
||||
amount,
|
||||
toAddress,
|
||||
"Funds from Demo App"
|
||||
).collectWith(lifecycleScope, ::onPendingTxUpdated)
|
||||
)?.collectWith(lifecycleScope, ::onPendingTxUpdated)
|
||||
}
|
||||
|
||||
mainActivity()?.hideKeyboard()
|
||||
|
@ -246,13 +236,13 @@ class SendFragment : BaseDemoFragment<FragmentSendBinding>() {
|
|||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val view = super.onCreateView(inflater, container, savedInstanceState)
|
||||
setup()
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
initSendUi()
|
||||
monitorChanges()
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
|
@ -261,13 +251,6 @@ class SendFragment : BaseDemoFragment<FragmentSendBinding>() {
|
|||
menu.setGroupVisible(R.id.main_menu_group, false)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
// the lifecycleScope is used to dispose of the synchronizer when the fragment dies
|
||||
synchronizer.start(lifecycleScope)
|
||||
monitorChanges()
|
||||
}
|
||||
|
||||
//
|
||||
// BaseDemoFragment overrides
|
||||
//
|
||||
|
|
|
@ -22,7 +22,7 @@ ZCASH_ASCII_GPG_KEY=
|
|||
# Configures whether release is an unstable snapshot, therefore published to the snapshot repository.
|
||||
IS_SNAPSHOT=true
|
||||
|
||||
LIBRARY_VERSION=1.10.0-beta01
|
||||
LIBRARY_VERSION=1.11.0-beta01
|
||||
|
||||
# Kotlin compiler warnings can be considered errors, failing the build.
|
||||
ZCASH_IS_TREAT_WARNINGS_AS_ERRORS=true
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
package cash.z.ecc.android.sdk
|
||||
|
||||
import cash.z.ecc.android.sdk.integration.SanityTest
|
||||
import cash.z.ecc.android.sdk.integration.SmokeTest
|
||||
import cash.z.ecc.android.sdk.integration.service.ChangeServiceTest
|
||||
import cash.z.ecc.android.sdk.internal.transaction.PersistentTransactionManagerTest
|
||||
import cash.z.ecc.android.sdk.jni.BranchIdTest
|
||||
import cash.z.ecc.android.sdk.jni.TransparentTest
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Suite
|
||||
|
||||
/**
|
||||
* Suite of tests to run before submitting a pull request.
|
||||
*
|
||||
* For now, these are just the tests that are known to be recently updated and that pass. In the
|
||||
* near future this suite will contain only fast running tests that can be used to quickly validate
|
||||
* that a PR hasn't broken anything major.
|
||||
*/
|
||||
@RunWith(Suite::class)
|
||||
@Suite.SuiteClasses(
|
||||
// Fast tests that only run locally and don't require darksidewalletd or lightwalletd
|
||||
BranchIdTest::class,
|
||||
TransparentTest::class,
|
||||
PersistentTransactionManagerTest::class,
|
||||
|
||||
// potentially exclude because these are long-running (and hit external srvcs)
|
||||
SanityTest::class,
|
||||
|
||||
// potentially exclude because these hit external services
|
||||
ChangeServiceTest::class,
|
||||
SmokeTest::class
|
||||
)
|
||||
class PullRequestSuite
|
|
@ -1,138 +0,0 @@
|
|||
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.ext.BlockExplorer
|
||||
import cash.z.ecc.android.sdk.internal.SaplingParamTool
|
||||
import cash.z.ecc.android.sdk.internal.db.DatabaseCoordinator
|
||||
import cash.z.ecc.android.sdk.internal.twig
|
||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.util.TestWallet
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
import kotlin.test.DefaultAsserter.assertTrue
|
||||
|
||||
// TODO [#650]: https://github.com/zcash/zcash-android-wallet-sdk/issues/650
|
||||
|
||||
/**
|
||||
* This test is intended to run to make sure that basic things are functional and pinpoint what is
|
||||
* not working. It was originally developed after a major refactor to find what broke.
|
||||
*/
|
||||
@MaintainedTest(TestPurpose.COMMIT)
|
||||
@RunWith(Parameterized::class)
|
||||
class SanityTest(
|
||||
private val wallet: TestWallet,
|
||||
private val encoding: String,
|
||||
private val birthday: BlockHeight
|
||||
|
||||
) {
|
||||
|
||||
val networkName = wallet.networkName
|
||||
val name = "$networkName wallet"
|
||||
|
||||
@Test
|
||||
fun testFilePaths() {
|
||||
val rustBackend = runBlocking {
|
||||
DefaultSynchronizerFactory.defaultRustBackend(
|
||||
ApplicationProvider.getApplicationContext(),
|
||||
wallet.network,
|
||||
"TestWallet",
|
||||
birthday,
|
||||
SaplingParamTool.new(ApplicationProvider.getApplicationContext())
|
||||
)
|
||||
}
|
||||
|
||||
assertTrue(
|
||||
"$name has invalid DataDB file actual=${rustBackend.dataDbFile.absolutePath}" +
|
||||
"expected suffix=no_backup/co.electricoin.zcash/TestWallet_${networkName}_${DatabaseCoordinator.DB_DATA_NAME}",
|
||||
rustBackend.dataDbFile.absolutePath.endsWith(
|
||||
"no_backup/co.electricoin.zcash/TestWallet_${networkName}_${DatabaseCoordinator.DB_DATA_NAME}"
|
||||
)
|
||||
)
|
||||
assertTrue(
|
||||
"$name has invalid CacheDB file $rustBackend.cacheDbFile.absolutePath",
|
||||
rustBackend.cacheDbFile.absolutePath.endsWith(
|
||||
"no_backup/co.electricoin.zcash/TestWallet_${networkName}_${DatabaseCoordinator.DB_CACHE_NAME}"
|
||||
)
|
||||
)
|
||||
assertTrue(
|
||||
"$name has invalid params dir ${rustBackend.saplingParamDir.absolutePath}",
|
||||
rustBackend.saplingParamDir.absolutePath.endsWith(
|
||||
"no_backup/co.electricoin.zcash"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore(
|
||||
"This test needs to be refactored to a separate test module. It causes SSLHandshakeException: Chain " +
|
||||
"validation failed on CI"
|
||||
)
|
||||
fun testLatestHeight() = runBlocking {
|
||||
if (wallet.networkName == "mainnet") {
|
||||
val expectedHeight = BlockExplorer.fetchLatestHeight()
|
||||
|
||||
// Fetch height directly because the synchronizer hasn't started, yet. Then we test the
|
||||
// result, only if there is no server communication problem.
|
||||
val downloaderHeight = runCatching {
|
||||
return@runCatching wallet.service.getLatestBlockHeight()
|
||||
}.onFailure {
|
||||
twig(it)
|
||||
}.getOrElse { return@runBlocking }
|
||||
|
||||
assertTrue(
|
||||
"${wallet.endpoint} ${wallet.networkName} Lightwalletd is too far behind. Downloader height $downloaderHeight is more than 10 blocks behind block explorer height $expectedHeight",
|
||||
expectedHeight - 10 < downloaderHeight.value
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore(
|
||||
"This test needs to be refactored to a separate test module. It causes SSLHandshakeException: Chain " +
|
||||
"validation failed on CI"
|
||||
)
|
||||
fun testSingleBlockDownload() = runBlocking {
|
||||
// Fetch height directly because the synchronizer hasn't started, yet. Then we test the
|
||||
// result, only if there is no server communication problem.
|
||||
val height = BlockHeight.new(wallet.network, 1_000_000)
|
||||
val block = runCatching {
|
||||
return@runCatching wallet.service.getBlockRange(height..height).first()
|
||||
}.onFailure {
|
||||
twig(it)
|
||||
}.getOrElse { return@runBlocking }
|
||||
|
||||
runCatching {
|
||||
wallet.service.getLatestBlockHeight()
|
||||
}.getOrNull() ?: return@runBlocking
|
||||
assertTrue(
|
||||
"$networkName failed to return a proper block. Height was ${block.height} but we expected $height",
|
||||
block.height == height.value
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
@Parameterized.Parameters
|
||||
fun wallets() = listOf(
|
||||
// Testnet wallet
|
||||
arrayOf(
|
||||
TestWallet(TestWallet.Backups.SAMPLE_WALLET),
|
||||
"uviewtest1m3cyp6tdy3rewtpqazdxlsqkmu7xjedtqmp4da8mvxm87h4as38v5kz4ulw7x7nmgv5d8uwk743a5zt7aurtz2z2g74fu740ecp5fhdgakm6hgzr5jzcl75cmddlufmjpykrpkzj84yz8j5qe9c5935qt2tvd9dpx3m0zw5dwn3t2dtsdyqvy5jstf88w799qre549yyxw7dvk3murm3568ah6wqg5tdjka2ujtgct4q62hw7mfcxcyaeu8l6882hxkt9x4025mx3w35whcrmpxy8fqsh62esatczj8awxtrgnj8h2vj65r8595qt9jl4gz84w4mja74tymt8xxaguckeam",
|
||||
BlockHeight.new(ZcashNetwork.Testnet, 1330000)
|
||||
),
|
||||
// Mainnet wallet
|
||||
arrayOf(
|
||||
TestWallet(TestWallet.Backups.SAMPLE_WALLET, ZcashNetwork.Mainnet),
|
||||
"uview1n8j8hckdh4rpxsa8qswmcv8mgu6g3f4l4se6ympej3qr6k5k5xlw47u02s3h2sy5aplkzuwysvum2p6weakvyc72udsuvplaq8r5jkw5h6cjfp26j8rudam7suzu6lwalzakpps2jv2x5v08gf3la02dtdlq75ca7k4urg6t0yncyly5wu26t6mfdfvxvhckr2qxzcwllnh947gn6wzg92f0mlhfds239q50gm4398n02anm23qgk8st49u0wmmw7flathr49h2twxvfm6gauasuq6z2fvs3t8g9ut4duk7tp7ry88dwacsutxzpwnm674y06mf3mz3tnu8s2fx4vatmcs9",
|
||||
BlockHeight.new(ZcashNetwork.Mainnet, 1000000)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,29 +1,20 @@
|
|||
package cash.z.ecc.android.sdk.integration
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.filters.LargeTest
|
||||
import androidx.test.filters.MediumTest
|
||||
import androidx.test.filters.SmallTest
|
||||
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.internal.SaplingParamTool
|
||||
import cash.z.ecc.android.sdk.internal.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.assertTrue
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* This test is intended to run to make sure that basic things are functional and pinpoint what is
|
||||
* not working. It was originally developed after a major refactor to find what broke.
|
||||
*/
|
||||
@MaintainedTest(TestPurpose.COMMIT)
|
||||
@MediumTest
|
||||
class SmokeTest {
|
||||
class SynchronizerFactoryTest {
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun testFilePaths() {
|
||||
val rustBackend = runBlocking {
|
||||
DefaultSynchronizerFactory.defaultRustBackend(
|
||||
|
@ -53,17 +44,4 @@ class SmokeTest {
|
|||
)
|
||||
)
|
||||
}
|
||||
|
||||
// This test takes an extremely long time
|
||||
// Does its runtime grow over time based on growth of the blockchain?
|
||||
@Test
|
||||
@LargeTest
|
||||
@Ignore("This test is extremely slow and times out before the timeout given")
|
||||
fun testSync() = runBlocking<Unit> {
|
||||
wallet.sync(300_000L)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val wallet = TestWallet(TestWallet.Backups.SAMPLE_WALLET)
|
||||
}
|
||||
}
|
|
@ -144,7 +144,6 @@ class TestnetIntegrationTest : ScopedTest() {
|
|||
seed = seed,
|
||||
birthday = BlockHeight.new(ZcashNetwork.Testnet, birthdayHeight)
|
||||
)
|
||||
synchronizer.start(classScope)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
package cash.z.ecc.android.sdk.internal
|
||||
|
||||
import androidx.test.filters.SmallTest
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import cash.z.ecc.android.bip39.Mnemonics
|
||||
import cash.z.ecc.android.sdk.Synchronizer
|
||||
import cash.z.ecc.android.sdk.fixture.WalletFixture
|
||||
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.test.runTest
|
||||
import java.util.UUID
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class SdkSynchronizerTest {
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
|
||||
fun cannot_instantiate_in_parallel() = runTest {
|
||||
// Random alias so that repeated invocations of this test will have a clean starting state
|
||||
val alias = UUID.randomUUID().toString()
|
||||
|
||||
// In the future, inject fake networking component so that it doesn't require hitting the network
|
||||
Synchronizer.new(
|
||||
InstrumentationRegistry.getInstrumentation().context,
|
||||
ZcashNetwork.Mainnet,
|
||||
alias,
|
||||
LightWalletEndpoint.defaultForNetwork(ZcashNetwork.Mainnet),
|
||||
Mnemonics.MnemonicCode(WalletFixture.SEED_PHRASE).toEntropy(),
|
||||
birthday = null
|
||||
).use {
|
||||
assertFailsWith<IllegalStateException> {
|
||||
Synchronizer.new(
|
||||
InstrumentationRegistry.getInstrumentation().context,
|
||||
ZcashNetwork.Mainnet,
|
||||
alias,
|
||||
LightWalletEndpoint.defaultForNetwork(ZcashNetwork.Mainnet),
|
||||
Mnemonics.MnemonicCode(WalletFixture.SEED_PHRASE).toEntropy(),
|
||||
birthday = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
|
||||
fun can_instantiate_in_serial() = runTest {
|
||||
// Random alias so that repeated invocations of this test will have a clean starting state
|
||||
val alias = UUID.randomUUID().toString()
|
||||
|
||||
// In the future, inject fake networking component so that it doesn't require hitting the network
|
||||
Synchronizer.new(
|
||||
InstrumentationRegistry.getInstrumentation().context,
|
||||
ZcashNetwork.Mainnet,
|
||||
alias,
|
||||
LightWalletEndpoint.defaultForNetwork(ZcashNetwork.Mainnet),
|
||||
Mnemonics.MnemonicCode(WalletFixture.SEED_PHRASE).toEntropy(),
|
||||
birthday = null
|
||||
).use {}
|
||||
|
||||
// Second instance should succeed because first one was closed
|
||||
Synchronizer.new(
|
||||
InstrumentationRegistry.getInstrumentation().context,
|
||||
ZcashNetwork.Mainnet,
|
||||
alias,
|
||||
LightWalletEndpoint.defaultForNetwork(ZcashNetwork.Mainnet),
|
||||
Mnemonics.MnemonicCode(WalletFixture.SEED_PHRASE).toEntropy(),
|
||||
birthday = null
|
||||
).use {}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package cash.z.ecc.android.sdk.util
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import cash.z.ecc.android.sdk.CloseableSynchronizer
|
||||
import cash.z.ecc.android.sdk.Synchronizer
|
||||
import cash.z.ecc.android.sdk.internal.TroubleshootingTwig
|
||||
import cash.z.ecc.android.sdk.internal.Twig
|
||||
|
@ -46,7 +47,7 @@ class BalancePrinterUtil {
|
|||
// private val rustBackend = RustBackend.init(context, cacheDbName, dataDbName)
|
||||
|
||||
private lateinit var birthday: Checkpoint
|
||||
private var synchronizer: Synchronizer? = null
|
||||
private var synchronizer: CloseableSynchronizer? = null
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
|
@ -94,7 +95,7 @@ class BalancePrinterUtil {
|
|||
- I might need to consider how state is impacting this design
|
||||
- can we be more stateless and thereby improve the flexibility of this code?!!!
|
||||
*/
|
||||
synchronizer?.stop()
|
||||
synchronizer?.close()
|
||||
synchronizer = Synchronizer.new(
|
||||
context,
|
||||
network,
|
||||
|
@ -102,9 +103,7 @@ class BalancePrinterUtil {
|
|||
.defaultForNetwork(network),
|
||||
seed = seed,
|
||||
birthday = birthdayHeight
|
||||
).apply {
|
||||
start()
|
||||
}
|
||||
)
|
||||
|
||||
// deleteDb(dataDbPath)
|
||||
// initWallet(seed)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package cash.z.ecc.android.sdk.util
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import cash.z.ecc.android.sdk.CloseableSynchronizer
|
||||
import cash.z.ecc.android.sdk.SdkSynchronizer
|
||||
import cash.z.ecc.android.sdk.Synchronizer
|
||||
import cash.z.ecc.android.sdk.internal.TroubleshootingTwig
|
||||
|
@ -40,7 +41,7 @@ class DataDbScannerUtil {
|
|||
// private val rustBackend = RustBackend.init(context, cacheDbName, dataDbName)
|
||||
|
||||
private val birthdayHeight = 600_000L
|
||||
private lateinit var synchronizer: Synchronizer
|
||||
private lateinit var synchronizer: CloseableSynchronizer
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
|
@ -79,7 +80,6 @@ class DataDbScannerUtil {
|
|||
)
|
||||
|
||||
println("sync!")
|
||||
synchronizer.start()
|
||||
val scope = (synchronizer as SdkSynchronizer).coroutineScope
|
||||
|
||||
scope.launch {
|
||||
|
@ -92,7 +92,7 @@ class DataDbScannerUtil {
|
|||
println("going to sleep!")
|
||||
Thread.sleep(125000)
|
||||
println("I'm back and I'm out!")
|
||||
synchronizer.stop()
|
||||
runBlocking { synchronizer.close() }
|
||||
}
|
||||
//
|
||||
// @Test
|
||||
|
|
|
@ -95,12 +95,8 @@ class TestWallet(
|
|||
throw TimeoutException("Failed to sync wallet within ${timeout}ms")
|
||||
}
|
||||
}
|
||||
if (!synchronizer.isStarted) {
|
||||
twig("Starting sync")
|
||||
synchronizer.start(walletScope)
|
||||
} else {
|
||||
twig("Awaiting next SYNCED status")
|
||||
}
|
||||
|
||||
twig("Awaiting next SYNCED status")
|
||||
|
||||
// block until synced
|
||||
synchronizer.status.first { it == Synchronizer.Status.SYNCED }
|
||||
|
@ -156,7 +152,7 @@ class TestWallet(
|
|||
twig("Scheduling a stop in ${timeout}ms")
|
||||
walletScope.launch {
|
||||
delay(timeout)
|
||||
synchronizer.stop()
|
||||
synchronizer.close()
|
||||
}
|
||||
}
|
||||
synchronizer.status.first { it == Synchronizer.Status.STOPPED }
|
||||
|
|
|
@ -17,7 +17,6 @@ import cash.z.ecc.android.sdk.block.CompactBlockProcessor.State.Scanned
|
|||
import cash.z.ecc.android.sdk.block.CompactBlockProcessor.State.Scanning
|
||||
import cash.z.ecc.android.sdk.block.CompactBlockProcessor.State.Stopped
|
||||
import cash.z.ecc.android.sdk.block.CompactBlockProcessor.State.Validating
|
||||
import cash.z.ecc.android.sdk.exception.SynchronizerException
|
||||
import cash.z.ecc.android.sdk.ext.ConsensusBranchId
|
||||
import cash.z.ecc.android.sdk.ext.ZcashSdk
|
||||
import cash.z.ecc.android.sdk.internal.SaplingParamTool
|
||||
|
@ -85,8 +84,8 @@ import kotlinx.coroutines.flow.map
|
|||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
|
||||
/**
|
||||
* A Synchronizer that attempts to remain operational, despite any number of errors that can occur.
|
||||
|
@ -96,18 +95,90 @@ import kotlin.coroutines.EmptyCoroutineContext
|
|||
* pieces can be tied together. Its goal is to allow a developer to focus on their app rather than
|
||||
* the nuances of how Zcash works.
|
||||
*
|
||||
* @property synchronizerKey Identifies the synchronizer's on-disk state
|
||||
* @property storage exposes flows of wallet transaction information.
|
||||
* @property txManager manages and tracks outbound transactions.
|
||||
* @property processor saves the downloaded compact blocks to the cache and then scans those blocks for
|
||||
* data related to this wallet.
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
class SdkSynchronizer internal constructor(
|
||||
class SdkSynchronizer private constructor(
|
||||
private val synchronizerKey: SynchronizerKey,
|
||||
private val storage: DerivedDataRepository,
|
||||
private val txManager: OutboundTransactionManager,
|
||||
val processor: CompactBlockProcessor,
|
||||
private val rustBackend: RustBackend
|
||||
) : Synchronizer {
|
||||
) : CloseableSynchronizer {
|
||||
|
||||
companion object {
|
||||
private sealed class InstanceState {
|
||||
object Active : InstanceState()
|
||||
data class ShuttingDown(val job: Job) : InstanceState()
|
||||
}
|
||||
|
||||
private val instances: MutableMap<SynchronizerKey, InstanceState> =
|
||||
ConcurrentHashMap<SynchronizerKey, InstanceState>()
|
||||
|
||||
/**
|
||||
* @throws IllegalStateException If multiple instances of synchronizer with the same network+alias are
|
||||
* active at the same time. Call `close` to finish one synchronizer before starting another one with the same
|
||||
* network+alias.
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
internal suspend fun new(
|
||||
zcashNetwork: ZcashNetwork,
|
||||
alias: String,
|
||||
repository: DerivedDataRepository,
|
||||
txManager: OutboundTransactionManager,
|
||||
processor: CompactBlockProcessor,
|
||||
rustBackend: RustBackend
|
||||
): CloseableSynchronizer {
|
||||
val synchronizerKey = SynchronizerKey(zcashNetwork, alias)
|
||||
|
||||
waitForShutdown(synchronizerKey)
|
||||
checkForExistingSynchronizers(synchronizerKey)
|
||||
|
||||
return SdkSynchronizer(
|
||||
synchronizerKey,
|
||||
repository,
|
||||
txManager,
|
||||
processor,
|
||||
rustBackend
|
||||
).apply {
|
||||
instances[synchronizerKey] = InstanceState.Active
|
||||
|
||||
start()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun waitForShutdown(synchronizerKey: SynchronizerKey) {
|
||||
instances[synchronizerKey]?.let {
|
||||
if (it is InstanceState.ShuttingDown) {
|
||||
twig("Waiting for prior synchronizer instance to shut down") // $NON-NLS-1$
|
||||
it.job.join()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkForExistingSynchronizers(synchronizerKey: SynchronizerKey) {
|
||||
check(!instances.containsKey(synchronizerKey)) {
|
||||
"Another synchronizer with $synchronizerKey is currently active" // $NON-NLS-1$
|
||||
}
|
||||
}
|
||||
|
||||
internal suspend fun erase(
|
||||
appContext: Context,
|
||||
network: ZcashNetwork,
|
||||
alias: String
|
||||
): Boolean {
|
||||
val key = SynchronizerKey(network, alias)
|
||||
|
||||
waitForShutdown(key)
|
||||
checkForExistingSynchronizers(key)
|
||||
|
||||
return DatabaseCoordinator.getInstance(appContext).deleteDatabases(network, alias)
|
||||
}
|
||||
}
|
||||
|
||||
// pools
|
||||
private val _orchardBalances = MutableStateFlow<WalletBalance?>(null)
|
||||
|
@ -116,25 +187,7 @@ class SdkSynchronizer internal constructor(
|
|||
|
||||
private val _status = MutableStateFlow<Synchronizer.Status>(DISCONNECTED)
|
||||
|
||||
/**
|
||||
* The lifespan of this Synchronizer. This scope is initialized once the Synchronizer starts
|
||||
* because it will be a child of the parentScope that gets passed into the [start] function.
|
||||
* Everything launched by this Synchronizer will be cancelled once the Synchronizer or its
|
||||
* parentScope stops. This coordinates with [isStarted] so that it fails early
|
||||
* rather than silently, whenever the scope is used before the Synchronizer has been started.
|
||||
*/
|
||||
var coroutineScope: CoroutineScope = CoroutineScope(EmptyCoroutineContext)
|
||||
get() {
|
||||
if (!isStarted) {
|
||||
throw SynchronizerException.NotYetStarted
|
||||
} else {
|
||||
return field
|
||||
}
|
||||
}
|
||||
set(value) {
|
||||
field = value
|
||||
if (value.coroutineContext !is EmptyCoroutineContext) isStarted = true
|
||||
}
|
||||
var coroutineScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
||||
|
||||
/**
|
||||
* The channel that this Synchronizer uses to communicate with lightwalletd. In most cases, this
|
||||
|
@ -145,8 +198,6 @@ class SdkSynchronizer internal constructor(
|
|||
*/
|
||||
val channel: ManagedChannel get() = (processor.downloader.lightWalletService as LightWalletGrpcService).channel
|
||||
|
||||
override var isStarted = false
|
||||
|
||||
//
|
||||
// Balances
|
||||
//
|
||||
|
@ -256,54 +307,31 @@ class SdkSynchronizer internal constructor(
|
|||
override val latestBirthdayHeight
|
||||
get() = processor.birthdayHeight
|
||||
|
||||
override suspend fun prepare(): Synchronizer = apply {
|
||||
// Do nothing; this could likely be removed
|
||||
internal fun start() {
|
||||
coroutineScope.onReady()
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts this synchronizer within the given scope. For simplicity, attempting to start an
|
||||
* instance that has already been started will throw a [SynchronizerException.FalseStart]
|
||||
* exception. This reduces the complexity of managing resources that must be recycled. Instead,
|
||||
* each synchronizer is designed to have a long lifespan and should be started from an activity,
|
||||
* application or session.
|
||||
*
|
||||
* @param parentScope the scope to use for this synchronizer, typically something with a
|
||||
* lifecycle such as an Activity for single-activity apps or a logged in user session. This
|
||||
* scope is only used for launching this synchronizer's job as a child. If no scope is provided,
|
||||
* then this synchronizer and all of its coroutines will run until stop is called, which is not
|
||||
* recommended since it can leak resources. That type of behavior is more useful for tests.
|
||||
*
|
||||
* @return an instance of this class so that this function can be used fluidly.
|
||||
*/
|
||||
override fun start(parentScope: CoroutineScope?): Synchronizer {
|
||||
if (isStarted) throw SynchronizerException.FalseStart
|
||||
// base this scope on the parent so that when the parent's job cancels, everything here
|
||||
// cancels as well also use a supervisor job so that one failure doesn't bring down the
|
||||
// whole synchronizer
|
||||
val supervisorJob = SupervisorJob(parentScope?.coroutineContext?.get(Job))
|
||||
CoroutineScope(supervisorJob + Dispatchers.Main).let { scope ->
|
||||
coroutineScope = scope
|
||||
scope.onReady()
|
||||
}
|
||||
return this
|
||||
}
|
||||
override fun close() {
|
||||
// Note that stopping will continue asynchronously. Race conditions with starting a new synchronizer are
|
||||
// avoided with a delay during startup.
|
||||
|
||||
/**
|
||||
* Stop this synchronizer and all of its child jobs. Once a synchronizer has been stopped it
|
||||
* should not be restarted and attempting to do so will result in an error. Also, this function
|
||||
* will throw an exception if the synchronizer was never previously started.
|
||||
*/
|
||||
override fun stop() {
|
||||
coroutineScope.launch {
|
||||
val shutdownJob = coroutineScope.launch {
|
||||
// log everything to help troubleshoot shutdowns that aren't graceful
|
||||
twig("Synchronizer::stop: STARTING")
|
||||
twig("Synchronizer::stop: processor.stop()")
|
||||
processor.stop()
|
||||
}
|
||||
|
||||
instances[synchronizerKey] = InstanceState.ShuttingDown(shutdownJob)
|
||||
|
||||
shutdownJob.invokeOnCompletion {
|
||||
twig("Synchronizer::stop: coroutineScope.cancel()")
|
||||
coroutineScope.cancel()
|
||||
twig("Synchronizer::stop: _status.cancel()")
|
||||
_status.value = STOPPED
|
||||
twig("Synchronizer::stop: COMPLETE")
|
||||
|
||||
instances.remove(synchronizerKey)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -407,7 +435,6 @@ class SdkSynchronizer internal constructor(
|
|||
|
||||
private fun CoroutineScope.onReady() = launch(CoroutineExceptionHandler(::onCriticalError)) {
|
||||
twig("Preparing to start...")
|
||||
prepare()
|
||||
|
||||
twig("Synchronizer (${this@SdkSynchronizer}) Ready. Starting processor!")
|
||||
var lastScanTime = 0L
|
||||
|
@ -725,25 +752,6 @@ class SdkSynchronizer internal constructor(
|
|||
serverBranchId?.let { ConsensusBranchId.fromHex(it) }
|
||||
)
|
||||
}
|
||||
|
||||
interface Erasable {
|
||||
/**
|
||||
* Erase content related to this SDK.
|
||||
*
|
||||
* @param appContext the application context.
|
||||
* @param network the network corresponding to the data being erased. Data is segmented by
|
||||
* network in order to prevent contamination.
|
||||
* @param alias identifier for SDK content. It is possible for multiple synchronizers to
|
||||
* exist with different aliases.
|
||||
*
|
||||
* @return true when content was found for the given alias. False otherwise.
|
||||
*/
|
||||
suspend fun erase(
|
||||
appContext: Context,
|
||||
network: ZcashNetwork,
|
||||
alias: String = ZcashSdk.DEFAULT_ALIAS
|
||||
): Boolean
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -753,20 +761,6 @@ class SdkSynchronizer internal constructor(
|
|||
*/
|
||||
internal object DefaultSynchronizerFactory {
|
||||
|
||||
fun new(
|
||||
repository: DerivedDataRepository,
|
||||
txManager: OutboundTransactionManager,
|
||||
processor: CompactBlockProcessor,
|
||||
rustBackend: RustBackend
|
||||
): Synchronizer {
|
||||
return SdkSynchronizer(
|
||||
repository,
|
||||
txManager,
|
||||
processor,
|
||||
rustBackend
|
||||
)
|
||||
}
|
||||
|
||||
internal suspend fun defaultRustBackend(
|
||||
context: Context,
|
||||
network: ZcashNetwork,
|
||||
|
@ -850,3 +844,5 @@ internal object DefaultSynchronizerFactory {
|
|||
rustBackend.birthdayHeight
|
||||
)
|
||||
}
|
||||
|
||||
internal data class SynchronizerKey(val zcashNetwork: ZcashNetwork, val alias: String)
|
||||
|
|
|
@ -21,10 +21,10 @@ 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
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.io.Closeable
|
||||
|
||||
/**
|
||||
* Primary interface for interacting with the SDK. Defines the contract that specific
|
||||
|
@ -35,45 +35,6 @@ import kotlinx.coroutines.runBlocking
|
|||
@Suppress("TooManyFunctions")
|
||||
interface Synchronizer {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
//
|
||||
|
||||
/**
|
||||
* Return true when this synchronizer has been started.
|
||||
*/
|
||||
var isStarted: Boolean
|
||||
|
||||
/**
|
||||
* Prepare the synchronizer to start. Must be called before start. This gives a clear point
|
||||
* where setup and maintenance can occur for various Synchronizers. One that uses a database
|
||||
* would take this opportunity to do data migrations or key migrations.
|
||||
*/
|
||||
suspend fun prepare(): Synchronizer
|
||||
|
||||
/**
|
||||
* Starts this synchronizer within the given scope.
|
||||
*
|
||||
* @param parentScope the scope to use for this synchronizer, typically something with a
|
||||
* lifecycle such as an Activity. Implementations should leverage structured concurrency and
|
||||
* cancel all jobs when this scope completes.
|
||||
*
|
||||
* @return an instance of the class so that this function can be used fluidly.
|
||||
*/
|
||||
fun start(parentScope: CoroutineScope? = null): Synchronizer
|
||||
|
||||
/**
|
||||
* Stop this synchronizer. Implementations should ensure that calling this method cancels all
|
||||
* jobs that were created by this instance.
|
||||
*
|
||||
* Note that in most cases, there is no need to call [stop] because the Synchronizer will
|
||||
* automatically stop whenever the parentScope is cancelled. For instance, if that scope is
|
||||
* bound to the lifecycle of the activity, the Synchronizer will stop when the activity stops.
|
||||
* However, if no scope is provided to the start method, then the Synchronizer must be stopped
|
||||
* with this function.
|
||||
*/
|
||||
fun stop()
|
||||
|
||||
//
|
||||
// Flows
|
||||
//
|
||||
|
@ -495,6 +456,9 @@ interface Synchronizer {
|
|||
* sync times. After sync completes, the birthday can be determined from [Synchronizer.latestBirthdayHeight].
|
||||
* @throws InitializerException.SeedRequired Indicates clients need to call this method again, providing the
|
||||
* seed bytes.
|
||||
* @throws IllegalStateException If multiple instances of synchronizer with the same network+alias are
|
||||
* active at the same time. Call `close` to finish one synchronizer before starting another one with the same
|
||||
* network+alias.
|
||||
*/
|
||||
/*
|
||||
* If customized initialization is required (e.g. for dependency injection or testing), see
|
||||
|
@ -508,7 +472,7 @@ interface Synchronizer {
|
|||
lightWalletEndpoint: LightWalletEndpoint,
|
||||
seed: ByteArray?,
|
||||
birthday: BlockHeight?
|
||||
): Synchronizer {
|
||||
): CloseableSynchronizer {
|
||||
val applicationContext = context.applicationContext
|
||||
|
||||
validateAlias(alias)
|
||||
|
@ -566,7 +530,9 @@ interface Synchronizer {
|
|||
)
|
||||
val processor = DefaultSynchronizerFactory.defaultProcessor(rustBackend, downloader, repository)
|
||||
|
||||
return SdkSynchronizer(
|
||||
return SdkSynchronizer.new(
|
||||
zcashNetwork,
|
||||
alias,
|
||||
repository,
|
||||
txManager,
|
||||
processor,
|
||||
|
@ -589,7 +555,7 @@ interface Synchronizer {
|
|||
lightWalletEndpoint: LightWalletEndpoint,
|
||||
seed: ByteArray?,
|
||||
birthday: BlockHeight?
|
||||
): Synchronizer = runBlocking {
|
||||
): CloseableSynchronizer = runBlocking {
|
||||
new(context, zcashNetwork, alias, lightWalletEndpoint, seed, birthday)
|
||||
}
|
||||
|
||||
|
@ -611,10 +577,12 @@ interface Synchronizer {
|
|||
appContext: Context,
|
||||
network: ZcashNetwork,
|
||||
alias: String = ZcashSdk.DEFAULT_ALIAS
|
||||
): Boolean = DatabaseCoordinator.getInstance(appContext).deleteDatabases(network, alias)
|
||||
): Boolean = SdkSynchronizer.erase(appContext, network, alias)
|
||||
}
|
||||
}
|
||||
|
||||
interface CloseableSynchronizer : Synchronizer, Closeable
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
@ -623,15 +591,14 @@ interface Synchronizer {
|
|||
* @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.
|
||||
* contains something other than alphanumeric characters. Underscores and hyphens are allowed.
|
||||
*/
|
||||
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 == '_' }
|
||||
alias.length in ZcashSdk.ALIAS_MIN_LENGTH..ZcashSdk.ALIAS_MAX_LENGTH &&
|
||||
alias.all { it.isLetterOrDigit() || it == '_' || 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."
|
||||
"characters and only contain letters, digits, hyphens, and underscores."
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue