diff --git a/CHANGELOG.md b/CHANGELOG.md index d9263ee7..8b4aacf6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,9 @@ Change Log - `Initializer.Config.newWallet` - `Initializer.Config.setViewingKeys` - `cash.z.ecc.android.sdk`: + - `Synchronizer.Companion.new` now takes a `seed` argument. A non-null value should be + provided if `Synchronizer.Companion.new` throws an error that a database migration + requires the wallet seed. - `Synchronizer.shieldFunds` now takes a transparent account private key (representing all transparent secret keys within an account) instead of a transparent secret key. diff --git a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getbalance/GetBalanceFragment.kt b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getbalance/GetBalanceFragment.kt index 8222e513..94dfa413 100644 --- a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getbalance/GetBalanceFragment.kt +++ b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getbalance/GetBalanceFragment.kt @@ -65,7 +65,7 @@ class GetBalanceFragment : BaseDemoFragment() { ) } }.let { initializer -> - synchronizer = Synchronizer.newBlocking(initializer) + synchronizer = Synchronizer.newBlocking(initializer, seed) } } diff --git a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listtransactions/ListTransactionsFragment.kt b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listtransactions/ListTransactionsFragment.kt index 9d29a260..1bfb97cf 100644 --- a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listtransactions/ListTransactionsFragment.kt +++ b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listtransactions/ListTransactionsFragment.kt @@ -72,7 +72,7 @@ class ListTransactionsFragment : BaseDemoFragment() { it.alias = "Demo_Utxos" } } - synchronizer = runBlocking { Synchronizer.new(initializer) } + synchronizer = runBlocking { Synchronizer.new(initializer, seed) } } override fun onCreate(savedInstanceState: Bundle?) { diff --git a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/send/SendFragment.kt b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/send/SendFragment.kt index 939a8bfa..ab8cf2ed 100644 --- a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/send/SendFragment.kt +++ b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/send/SendFragment.kt @@ -80,7 +80,7 @@ class SendFragment : BaseDemoFragment() { } } }.let { initializer -> - synchronizer = Synchronizer.newBlocking(initializer) + synchronizer = Synchronizer.newBlocking(initializer, seed) } spendingKey = runBlocking { DerivationTool.deriveSpendingKeys(seed, ZcashNetwork.fromResources(requireApplicationContext())).first() diff --git a/sdk-lib/Cargo.lock b/sdk-lib/Cargo.lock index d607412e..100045e6 100644 --- a/sdk-lib/Cargo.lock +++ b/sdk-lib/Cargo.lock @@ -2025,7 +2025,9 @@ dependencies = [ "jni", "log", "log-panics", + "schemer", "secp256k1", + "secrecy", "zcash_client_backend", "zcash_client_sqlite", "zcash_primitives", diff --git a/sdk-lib/Cargo.toml b/sdk-lib/Cargo.toml index 1d1e6739..bfa1dd8a 100644 --- a/sdk-lib/Cargo.toml +++ b/sdk-lib/Cargo.toml @@ -18,7 +18,9 @@ hex = "0.4" jni = { version = "0.17", default-features = false } log = "0.4" log-panics = "2.0.0" +schemer = "0.2" secp256k1 = "0.21" +secrecy = "0.8" zcash_client_backend = { version = "0.5", features = ["transparent-inputs"] } zcash_client_sqlite = { version = "0.3", features = ["transparent-inputs"] } zcash_primitives = "0.7" diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/SdkSynchronizer.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/SdkSynchronizer.kt index 92e7c4f7..0cde3692 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/SdkSynchronizer.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/SdkSynchronizer.kt @@ -797,12 +797,13 @@ object DefaultSynchronizerFactory { // can touch its views. and is probably related to FlowPagedList // TODO [#242]: https://github.com/zcash/zcash-android-wallet-sdk/issues/242 private const val DEFAULT_PAGE_SIZE = 1000 - suspend fun defaultTransactionRepository(initializer: Initializer): TransactionRepository = + suspend fun defaultTransactionRepository(initializer: Initializer, seed: ByteArray?): TransactionRepository = PagedTransactionRepository.new( initializer.context, initializer.network, DEFAULT_PAGE_SIZE, initializer.rustBackend, + seed, initializer.checkpoint, initializer.viewingKeys, initializer.overwriteVks diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/Synchronizer.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/Synchronizer.kt index fe0905fb..d3862471 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/Synchronizer.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/Synchronizer.kt @@ -444,11 +444,14 @@ interface Synchronizer { * Synchronizer requires. It contains all information necessary to build a synchronizer and it is * mainly responsible for initializing the databases associated with this synchronizer and loading * the rust backend. + * @param seed the wallet's seed phrase. This only needs to be provided if this method returns an + * error indicating that the seed phrase is required for a database migration. */ suspend fun new( - initializer: Initializer + initializer: Initializer, + seed: ByteArray, ): Synchronizer { - val repository = DefaultSynchronizerFactory.defaultTransactionRepository(initializer) + val repository = DefaultSynchronizerFactory.defaultTransactionRepository(initializer, seed) val blockStore = DefaultSynchronizerFactory.defaultBlockStore(initializer) val service = DefaultSynchronizerFactory.defaultService(initializer) val encoder = DefaultSynchronizerFactory.defaultEncoder(initializer, repository) @@ -472,6 +475,8 @@ interface Synchronizer { * This is a blocking call, so it should not be called from the main thread. */ @JvmStatic - fun newBlocking(initializer: Initializer): Synchronizer = runBlocking { new(initializer) } + fun newBlocking(initializer: Initializer, seed: ByteArray): Synchronizer = runBlocking { + new (initializer, seed) + } } } diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/exception/Exceptions.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/exception/Exceptions.kt index 68f8b792..824f2242 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/exception/Exceptions.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/exception/Exceptions.kt @@ -175,6 +175,10 @@ sealed class BirthdayException(message: String, cause: Throwable? = null) : SdkE * Exceptions thrown by the initializer. */ sealed class InitializerException(message: String, cause: Throwable? = null) : SdkException(message, cause) { + object SeedRequired : InitializerException( + "A pending database migration requires the wallet's seed. Call this initialization " + + "method again with the seed." + ) class FalseStart(cause: Throwable?) : InitializerException("Failed to initialize accounts due to: $cause", cause) class AlreadyInitializedException(cause: Throwable, dbPath: String) : InitializerException( "Failed to initialize the blocks table" + diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/PagedTransactionRepository.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/PagedTransactionRepository.kt index 5a1e5c15..4e3abf46 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/PagedTransactionRepository.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/PagedTransactionRepository.kt @@ -5,6 +5,7 @@ import androidx.paging.PagedList import androidx.room.RoomDatabase import cash.z.ecc.android.sdk.db.commonDatabaseBuilder import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction +import cash.z.ecc.android.sdk.exception.InitializerException.SeedRequired import cash.z.ecc.android.sdk.ext.ZcashSdk import cash.z.ecc.android.sdk.internal.SdkDispatchers import cash.z.ecc.android.sdk.internal.SdkExecutors @@ -122,11 +123,12 @@ internal class PagedTransactionRepository private constructor( zcashNetwork: ZcashNetwork, pageSize: Int = 10, rustBackend: RustBackend, + seed: ByteArray?, birthday: Checkpoint, viewingKeys: List, overwriteVks: Boolean = false ): PagedTransactionRepository { - initMissingDatabases(rustBackend, birthday, viewingKeys) + initMissingDatabases(rustBackend, seed, birthday, viewingKeys) val db = buildDatabase(appContext.applicationContext, rustBackend.dataDbFile) applyKeyMigrations(rustBackend, overwriteVks, viewingKeys) @@ -172,10 +174,11 @@ internal class PagedTransactionRepository private constructor( */ private suspend fun initMissingDatabases( rustBackend: RustBackend, + seed: ByteArray?, birthday: Checkpoint, viewingKeys: List ) { - maybeCreateDataDb(rustBackend) + maybeCreateDataDb(rustBackend, seed) maybeInitBlocksTable(rustBackend, birthday) maybeInitAccountsTable(rustBackend, viewingKeys) } @@ -183,9 +186,15 @@ internal class PagedTransactionRepository private constructor( /** * Create the dataDb and its table, if it doesn't exist. */ - private suspend fun maybeCreateDataDb(rustBackend: RustBackend) { - tryWarn("Warning: did not create dataDb. It probably already exists.") { - rustBackend.initDataDb() + private suspend fun maybeCreateDataDb(rustBackend: RustBackend, seed: ByteArray?) { + tryWarn( + "Warning: did not create dataDb. It probably already exists.", + unlessContains = "requires the wallet's seed" + ) { + val res = rustBackend.initDataDb(seed) + if (res == 1) { + throw SeedRequired + } twig("Initialized wallet for first run file: ${rustBackend.dataDbFile}") } } diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/jni/RustBackend.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/jni/RustBackend.kt index f5584c81..30ef210d 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/jni/RustBackend.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/jni/RustBackend.kt @@ -44,9 +44,10 @@ internal class RustBackend private constructor( // Wrapper Functions // - override suspend fun initDataDb() = withContext(SdkDispatchers.DATABASE_IO) { + override suspend fun initDataDb(seed: ByteArray?) = withContext(SdkDispatchers.DATABASE_IO) { initDataDb( dataDbFile.absolutePath, + seed, networkId = network.id ) } @@ -384,7 +385,7 @@ internal class RustBackend private constructor( // @JvmStatic - private external fun initDataDb(dbDataPath: String, networkId: Int): Boolean + private external fun initDataDb(dbDataPath: String, seed: ByteArray?, networkId: Int): Int @JvmStatic private external fun initAccountsTableWithKeys( diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/jni/RustBackendWelding.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/jni/RustBackendWelding.kt index 3d7e8dd0..f3fcaf93 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/jni/RustBackendWelding.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/jni/RustBackendWelding.kt @@ -40,7 +40,7 @@ internal interface RustBackendWelding { suspend fun initBlocksTable(checkpoint: Checkpoint): Boolean - suspend fun initDataDb(): Boolean + suspend fun initDataDb(seed: ByteArray?): Int fun isValidShieldedAddr(addr: String): Boolean diff --git a/sdk-lib/src/main/rust/lib.rs b/sdk-lib/src/main/rust/lib.rs index ef009051..d0e870f3 100644 --- a/sdk-lib/src/main/rust/lib.rs +++ b/sdk-lib/src/main/rust/lib.rs @@ -17,7 +17,9 @@ use jni::{ JNIEnv, }; use log::Level; +use schemer::MigratorError; use secp256k1::PublicKey; +use secrecy::SecretVec; use zcash_client_backend::keys::UnifiedSpendingKey; use zcash_client_backend::{ address::RecipientAddress, @@ -36,6 +38,7 @@ use zcash_client_backend::{ keys::{sapling, UnifiedFullViewingKey}, wallet::{OvkPolicy, WalletTransparentOutput}, }; +use zcash_client_sqlite::wallet::init::WalletMigrationError; #[allow(deprecated)] use zcash_client_sqlite::wallet::{delete_utxos_above, get_rewind_height}; use zcash_client_sqlite::{ @@ -106,26 +109,40 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_initLogs( print_debug_state() } +/// Sets up the internal structure of the data database. +/// +/// If `seed` is `null`, database migrations will be attempted without it. +/// +/// Returns 0 if successful, 1 if the seed must be provided in order to execute the requested +/// migrations, or -1 otherwise. #[no_mangle] pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_initDataDb( env: JNIEnv<'_>, _: JClass<'_>, db_data: JString<'_>, + seed: jbyteArray, network_id: jint, -) -> jboolean { +) -> jint { let res = panic::catch_unwind(|| { let network = parse_network(network_id as u32)?; let db_path = utils::java_string_to_rust(&env, db_data); - let seed = None; // TODO Update - WalletDb::for_path(db_path, network) - .map_err(|e| format_err!("Error while opening data DB: {}", e)) - .and_then(|mut db| { - init_wallet_db(&mut db, seed) - .map_err(|e| format_err!("Error while initializing data DB: {}", e)) - }) - .map(|()| JNI_TRUE) + + let mut db_data = WalletDb::for_path(db_path, network) + .map_err(|e| format_err!("Error while opening data DB: {}", e))?; + + let seed = (!seed.is_null()).then(|| SecretVec::new(env.convert_byte_array(seed).unwrap())); + + match init_wallet_db(&mut db_data, seed) { + Ok(()) => Ok(0), + Err(MigratorError::Migration { error, .. }) + if matches!(error, WalletMigrationError::SeedRequired) => + { + Ok(1) + } + Err(e) => Err(format_err!("Error while initializing data DB: {}", e)), + } }); - unwrap_exc_or(&env, res, JNI_FALSE) + unwrap_exc_or(&env, res, -1) } #[no_mangle]