[#1208][#1215] Pass proper `recoverUntil`

- Closes #1208
- CLoses #1215
This commit is contained in:
Honza Rychnovský 2023-09-08 16:56:02 +02:00 committed by Honza
parent 9207dd0b58
commit c6032b47bd
17 changed files with 142 additions and 31 deletions

View File

@ -23,6 +23,8 @@
### Changed
- `CompactBlockProcessor.quickRewind()` and `CompactBlockProcessor.rewindToNearestHeight()` now might fail due to
internal changes in getting scanned height. Thus, these functions return `Boolean` results.
- `Synchronizer.new()` requires a new `walletInitMode` parameter of type `WalletInitMode`, which describes wallet
initialization mode. See related function and sealed class documentation.
### Fixed
- `Synchronizer.getMemos()` now correctly returns a flow of strings for sent and received transactions. Issue **#1154**.

View File

@ -40,6 +40,5 @@ interface Derivation {
companion object {
const val DEFAULT_NUMBER_OF_ACCOUNTS = 1
val DEFAULT_RECOVERY_UNTIL_HEIGHT = null
}
}

View File

@ -23,5 +23,8 @@ class JniScanProgress(
require(numerator.toFloat().div(denominator) >= 0f) {
"Result of ${numerator.toFloat()}/$denominator is outside of allowed range"
}
require(numerator.toFloat().div(denominator) <= 1f) {
"Result of ${numerator.toFloat()}/$denominator is outside of allowed range"
}
}
}

View File

@ -5,6 +5,7 @@ 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.WalletInitMode
import cash.z.ecc.android.sdk.internal.Twig
import cash.z.ecc.android.sdk.model.Account
import cash.z.ecc.android.sdk.model.BlockHeight
@ -65,7 +66,9 @@ class TestWallet(
alias,
endpoint,
seed,
startHeight
startHeight,
// Using existing wallet init mode as simplification for the test
walletInitMode = WalletInitMode.ExistingWallet
) as SdkSynchronizer
val available get() = synchronizer.saplingBalances.value?.available

View File

@ -2,6 +2,7 @@ package cash.z.wallet.sdk.sample.demoapp
import androidx.test.platform.app.InstrumentationRegistry
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.WalletInitMode
import cash.z.ecc.android.sdk.demoapp.util.fromResources
import cash.z.ecc.android.sdk.ext.convertZecToZatoshi
import cash.z.ecc.android.sdk.ext.toHex
@ -202,7 +203,9 @@ class SampleCodeTest {
network,
lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network),
seed = seed,
birthday = null
birthday = null,
// Using existing wallet init mode as simplification for the test
walletInitMode = WalletInitMode.ExistingWallet
)
}

View File

@ -40,7 +40,7 @@ class ComposeActivity : ComponentActivity() {
}
SecretState.None -> {
Seed(
ZcashNetwork.fromResources(applicationContext),
zcashNetwork = ZcashNetwork.fromResources(applicationContext),
onExistingWallet = { walletViewModel.persistExistingWallet(it) },
onNewWallet = { walletViewModel.persistNewWallet() }
)

View File

@ -6,6 +6,7 @@ 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.WalletInitMode
import cash.z.ecc.android.sdk.demoapp.util.fromResources
import cash.z.ecc.android.sdk.ext.onFirst
import cash.z.ecc.android.sdk.internal.Twig
@ -82,6 +83,8 @@ class SharedViewModel(application: Application) : AndroidViewModel(application)
} else {
birthdayHeight.value
},
// We use restore mode as this is always initialization with an older seed
walletInitMode = WalletInitMode.RestoreWallet,
alias = OLD_UI_SYNCHRONIZER_ALIAS
)

View File

@ -7,6 +7,7 @@ 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.WalletCoordinator
import cash.z.ecc.android.sdk.WalletInitMode
import cash.z.ecc.android.sdk.block.processor.CompactBlockProcessor
import cash.z.ecc.android.sdk.demoapp.getInstance
import cash.z.ecc.android.sdk.demoapp.preference.EncryptedPreferenceKeys
@ -48,7 +49,6 @@ import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import kotlin.time.Duration.Companion.seconds
import kotlin.time.ExperimentalTime
// To make this more multiplatform compatible, we need to remove the dependency on Context
// for loading the preferences.
@ -101,7 +101,7 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
null
)
@OptIn(ExperimentalCoroutinesApi::class, ExperimentalTime::class)
@OptIn(ExperimentalCoroutinesApi::class)
val walletSnapshot: StateFlow<WalletSnapshot?> = synchronizer
.flatMapLatest {
if (null == it) {
@ -141,10 +141,11 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
*/
fun persistNewWallet() {
val application = getApplication<Application>()
PersistableWallet.walletInitMode = WalletInitMode.NewWallet
viewModelScope.launch {
val newWallet = PersistableWallet.new(application, ZcashNetwork.fromResources(application))
persistExistingWallet(newWallet)
persistWallet(newWallet)
}
}
@ -153,6 +154,14 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
* to see the side effects. This would be used for a user restoring a wallet from a backup.
*/
fun persistExistingWallet(persistableWallet: PersistableWallet) {
PersistableWallet.walletInitMode = WalletInitMode.RestoreWallet
persistWallet(persistableWallet)
}
/**
* Persists a wallet asynchronously. Clients observe [secretState] to see the side effects.
*/
private fun persistWallet(persistableWallet: PersistableWallet) {
val application = getApplication<Application>()
viewModelScope.launch {

View File

@ -79,7 +79,8 @@ class WalletCoordinator(
zcashNetwork = persistableWallet.network,
lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(persistableWallet.network),
birthday = persistableWallet.birthday,
seed = persistableWallet.seedPhrase.toByteArray()
seed = persistableWallet.seedPhrase.toByteArray(),
walletInitMode = PersistableWallet.walletInitMode,
)
trySend(InternalSynchronizerStatus.Available(closeableSynchronizer))

View File

@ -3,6 +3,7 @@ package cash.z.ecc.android.sdk.model
import android.app.Application
import cash.z.ecc.android.bip39.Mnemonics
import cash.z.ecc.android.bip39.toEntropy
import cash.z.ecc.android.sdk.WalletInitMode
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.json.JSONObject
@ -41,6 +42,10 @@ data class PersistableWallet(
internal const val KEY_BIRTHDAY = "birthday"
internal const val KEY_SEED_PHRASE = "seed_phrase"
// Note: This is not the ideal way to hold such a value. But we also want to avoid persisting the wallet
// initialization mode with the persistable wallet.
var walletInitMode: WalletInitMode = WalletInitMode.ExistingWallet
fun from(jsonObject: JSONObject): PersistableWallet {
when (val version = jsonObject.getInt(KEY_VERSION)) {
VERSION_1 -> {
@ -56,7 +61,11 @@ data class PersistableWallet(
}
val seedPhrase = jsonObject.getString(KEY_SEED_PHRASE)
return PersistableWallet(network, birthday, SeedPhrase.new(seedPhrase))
return PersistableWallet(
network = network,
birthday = birthday,
seedPhrase = SeedPhrase.new(seedPhrase)
)
}
else -> {
throw IllegalArgumentException("Unsupported version $version")
@ -72,7 +81,11 @@ data class PersistableWallet(
val seedPhrase = newSeedPhrase()
return PersistableWallet(zcashNetwork, birthday, seedPhrase)
return PersistableWallet(
zcashNetwork,
birthday,
seedPhrase
)
}
}
}

View File

@ -4,6 +4,7 @@ import androidx.test.filters.LargeTest
import androidx.test.platform.app.InstrumentationRegistry
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.Synchronizer.Status.SYNCED
import cash.z.ecc.android.sdk.WalletInitMode
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.ext.onFirst
import cash.z.ecc.android.sdk.internal.Twig
@ -141,7 +142,9 @@ class TestnetIntegrationTest : ScopedTest() {
lightWalletEndpoint =
lightWalletEndpoint,
seed = seed,
birthday = BlockHeight.new(ZcashNetwork.Testnet, birthdayHeight)
birthday = BlockHeight.new(ZcashNetwork.Testnet, birthdayHeight),
// Using existing wallet init mode as simplification for the test
walletInitMode = WalletInitMode.ExistingWallet
)
}
}

View File

@ -4,6 +4,7 @@ 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.WalletInitMode
import cash.z.ecc.android.sdk.fixture.WalletFixture
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.model.defaultForNetwork
@ -29,7 +30,9 @@ class SdkSynchronizerTest {
alias,
LightWalletEndpoint.defaultForNetwork(ZcashNetwork.Mainnet),
Mnemonics.MnemonicCode(WalletFixture.SEED_PHRASE).toEntropy(),
birthday = null
birthday = null,
// Using existing wallet init mode as simplification for the test
walletInitMode = WalletInitMode.ExistingWallet
).use {
assertFailsWith<IllegalStateException> {
Synchronizer.new(
@ -38,7 +41,9 @@ class SdkSynchronizerTest {
alias,
LightWalletEndpoint.defaultForNetwork(ZcashNetwork.Mainnet),
Mnemonics.MnemonicCode(WalletFixture.SEED_PHRASE).toEntropy(),
birthday = null
birthday = null,
// Using existing wallet init mode as simplification for the test
walletInitMode = WalletInitMode.ExistingWallet
)
}
}
@ -51,6 +56,8 @@ class SdkSynchronizerTest {
// Random alias so that repeated invocations of this test will have a clean starting state
val alias = UUID.randomUUID().toString()
// TODO [#1094]: Consider fake SDK sync related components
// TODO [#1094]: https://github.com/zcash/zcash-android-wallet-sdk/issues/1094
// In the future, inject fake networking component so that it doesn't require hitting the network
Synchronizer.new(
InstrumentationRegistry.getInstrumentation().context,
@ -58,7 +65,9 @@ class SdkSynchronizerTest {
alias,
LightWalletEndpoint.defaultForNetwork(ZcashNetwork.Mainnet),
Mnemonics.MnemonicCode(WalletFixture.SEED_PHRASE).toEntropy(),
birthday = null
birthday = null,
// Using existing wallet init mode as simplification for the test
walletInitMode = WalletInitMode.ExistingWallet
).use {}
// Second instance should succeed because first one was closed
@ -68,7 +77,9 @@ class SdkSynchronizerTest {
alias,
LightWalletEndpoint.defaultForNetwork(ZcashNetwork.Mainnet),
Mnemonics.MnemonicCode(WalletFixture.SEED_PHRASE).toEntropy(),
birthday = null
birthday = null,
// Using existing wallet init mode as simplification for the test
walletInitMode = WalletInitMode.ExistingWallet
).use {}
}
}

View File

@ -3,6 +3,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.WalletInitMode
import cash.z.ecc.android.sdk.internal.Twig
import cash.z.ecc.android.sdk.internal.ext.deleteSuspend
import cash.z.ecc.android.sdk.internal.model.Checkpoint
@ -96,7 +97,9 @@ class BalancePrinterUtil {
lightWalletEndpoint = LightWalletEndpoint
.defaultForNetwork(network),
seed = seed,
birthday = birthdayHeight
birthday = birthdayHeight,
// Using existing wallet init mode as simplification for the test
walletInitMode = WalletInitMode.ExistingWallet
)
// deleteDb(dataDbPath)

View File

@ -4,6 +4,7 @@ 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.WalletInitMode
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.model.defaultForNetwork
@ -72,7 +73,9 @@ class DataDbScannerUtil {
birthday = BlockHeight.new(
ZcashNetwork.Mainnet,
birthdayHeight
)
),
// Using existing wallet init mode as simplification for the test
walletInitMode = WalletInitMode.ExistingWallet
)
println("sync!")

View File

@ -5,6 +5,7 @@ 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.WalletInitMode
import cash.z.ecc.android.sdk.internal.Twig
import cash.z.ecc.android.sdk.internal.deriveUnifiedSpendingKey
import cash.z.ecc.android.sdk.internal.jni.RustDerivationTool
@ -66,7 +67,9 @@ class TestWallet(
alias,
lightWalletEndpoint = endpoint,
seed = seed,
startHeight
startHeight,
// Using existing wallet init mode as simplification for the test
walletInitMode = WalletInitMode.ExistingWallet
) as SdkSynchronizer
val available get() = synchronizer.saplingBalances.value?.available

View File

@ -5,7 +5,9 @@ import cash.z.ecc.android.sdk.block.processor.CompactBlockProcessor
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.internal.Derivation
import cash.z.ecc.android.sdk.internal.SaplingParamTool
import cash.z.ecc.android.sdk.internal.Twig
import cash.z.ecc.android.sdk.internal.db.DatabaseCoordinator
import cash.z.ecc.android.sdk.internal.model.ext.toBlockHeight
import cash.z.ecc.android.sdk.model.Account
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.PercentDecimal
@ -19,6 +21,7 @@ import cash.z.ecc.android.sdk.tool.CheckpointTool
import cash.z.ecc.android.sdk.type.AddressType
import cash.z.ecc.android.sdk.type.ConsensusMatchType
import co.electriccoin.lightwallet.client.model.LightWalletEndpoint
import co.electriccoin.lightwallet.client.model.Response
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.runBlocking
@ -425,6 +428,12 @@ interface Synchronizer {
* to create the wallet. If that value is unknown, null is acceptable but will result in longer
* sync times. After sync completes, the birthday can be determined from [Synchronizer.latestBirthdayHeight].
*
* @param walletInitMode a required parameter with one of [WalletInitMode] values. Use
* [WalletInitMode.NewWallet] when starting synchronizer for a newly created wallet. Or use
* [WalletInitMode.RestoreWallet] when restoring an existing wallet that was created at some point in the
* past. Or use the last [WalletInitMode.ExistingWallet] type for a wallet which is already initialized
* and needs follow-up block synchronization.
*
* @throws InitializerException.SeedRequired Indicates clients need to call this method again, providing the
* seed bytes.
*
@ -443,7 +452,8 @@ interface Synchronizer {
alias: String = ZcashSdk.DEFAULT_ALIAS,
lightWalletEndpoint: LightWalletEndpoint,
seed: ByteArray?,
birthday: BlockHeight?
birthday: BlockHeight?,
walletInitMode: WalletInitMode
): CloseableSynchronizer {
val applicationContext = context.applicationContext
@ -472,20 +482,39 @@ interface Synchronizer {
DefaultSynchronizerFactory
.defaultCompactBlockRepository(coordinator.fsBlockDbRoot(zcashNetwork, alias), backend)
val service = DefaultSynchronizerFactory.defaultService(applicationContext, lightWalletEndpoint)
val downloader = DefaultSynchronizerFactory.defaultDownloader(service, blockStore)
val chainTip = when (walletInitMode) {
is WalletInitMode.RestoreWallet -> {
when (val response = downloader.getLatestBlockHeight()) {
is Response.Success -> {
Twig.info { "Chain tip for recovery until param fetched: ${response.result.value}" }
runCatching { response.result.toBlockHeight(zcashNetwork) }.getOrNull()
}
is Response.Failure -> {
Twig.error { "Chain tip fetch for recovery until failed with: ${response.toThrowable()}" }
null
}
}
}
else -> {
null
}
}
val repository = DefaultSynchronizerFactory.defaultDerivedDataRepository(
applicationContext,
backend,
coordinator.dataDbFile(zcashNetwork, alias),
zcashNetwork,
loadedCheckpoint,
seed,
Derivation.DEFAULT_NUMBER_OF_ACCOUNTS,
Derivation.DEFAULT_RECOVERY_UNTIL_HEIGHT,
context = applicationContext,
rustBackend = backend,
databaseFile = coordinator.dataDbFile(zcashNetwork, alias),
zcashNetwork = zcashNetwork,
checkpoint = loadedCheckpoint,
seed = seed,
numberOfAccounts = Derivation.DEFAULT_NUMBER_OF_ACCOUNTS,
recoverUntil = chainTip,
)
val service = DefaultSynchronizerFactory.defaultService(applicationContext, lightWalletEndpoint)
val encoder = DefaultSynchronizerFactory.defaultEncoder(backend, saplingParamTool, repository)
val downloader = DefaultSynchronizerFactory.defaultDownloader(service, blockStore)
val txManager = DefaultSynchronizerFactory.defaultTxManager(
encoder,
service
@ -521,9 +550,10 @@ interface Synchronizer {
alias: String = ZcashSdk.DEFAULT_ALIAS,
lightWalletEndpoint: LightWalletEndpoint,
seed: ByteArray?,
birthday: BlockHeight?
birthday: BlockHeight?,
walletInitMode: WalletInitMode
): CloseableSynchronizer = runBlocking {
new(context, zcashNetwork, alias, lightWalletEndpoint, seed, birthday)
new(context, zcashNetwork, alias, lightWalletEndpoint, seed, birthday, walletInitMode)
}
/**
@ -548,6 +578,23 @@ interface Synchronizer {
}
}
/**
* Sealed class describing wallet initialization mode.
*
* Use [NewWallet] type if the seed was just created as part of a
* new wallet initialization.
*
* Use [RestoreWallet] type if an existed wallet is initialized
* from a restored seed with older birthday height.
*
* Use [ExistingWallet] type if the wallet is already initialized.
*/
sealed class WalletInitMode {
data object NewWallet : WalletInitMode()
data object RestoreWallet : WalletInitMode()
data object ExistingWallet : WalletInitMode()
}
interface CloseableSynchronizer : Synchronizer, Closeable
/**

View File

@ -340,6 +340,8 @@ class CompactBlockProcessor internal constructor(
"Failed while processing blocks at height: ${result.failedAtHeight} with: " +
"${result.error}"
}
// TODO [#1222]: Enrich BlockProcessingResult.SyncFailure with root cause
// TODO [#1222]: https://github.com/zcash/zcash-android-wallet-sdk/issues/1222
checkErrorResult(result.failedAtHeight)
}
is BlockProcessingResult.Success -> {
@ -754,6 +756,9 @@ class CompactBlockProcessor internal constructor(
object Success : BlockProcessingResult()
object Reconnecting : BlockProcessingResult()
object RestartSynchronization : BlockProcessingResult()
// TODO [#1222]: Enrich BlockProcessingResult.SyncFailure with root cause
// TODO [#1222]: https://github.com/zcash/zcash-android-wallet-sdk/issues/1222
data class SyncFailure(val failedAtHeight: BlockHeight?, val error: Throwable) : BlockProcessingResult()
}