Add a `seed` argument to `Synchronizer.Companion.new`
This enables callers to recover from an error that indicates the seed is needed for a database migration.
This commit is contained in:
parent
b19ee179c5
commit
c6fd783317
|
@ -27,6 +27,9 @@ Change Log
|
||||||
- `Initializer.Config.newWallet`
|
- `Initializer.Config.newWallet`
|
||||||
- `Initializer.Config.setViewingKeys`
|
- `Initializer.Config.setViewingKeys`
|
||||||
- `cash.z.ecc.android.sdk`:
|
- `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
|
- `Synchronizer.shieldFunds` now takes a transparent account private key (representing
|
||||||
all transparent secret keys within an account) instead of a transparent secret key.
|
all transparent secret keys within an account) instead of a transparent secret key.
|
||||||
|
|
||||||
|
|
|
@ -65,7 +65,7 @@ class GetBalanceFragment : BaseDemoFragment<FragmentGetBalanceBinding>() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}.let { initializer ->
|
}.let { initializer ->
|
||||||
synchronizer = Synchronizer.newBlocking(initializer)
|
synchronizer = Synchronizer.newBlocking(initializer, seed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -72,7 +72,7 @@ class ListTransactionsFragment : BaseDemoFragment<FragmentListTransactionsBindin
|
||||||
ZcashNetwork.fromResources(requireApplicationContext())
|
ZcashNetwork.fromResources(requireApplicationContext())
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
synchronizer = Synchronizer.newBlocking(initializer)
|
synchronizer = Synchronizer.newBlocking(initializer, seed)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initTransactionUI() {
|
private fun initTransactionUI() {
|
||||||
|
|
|
@ -79,7 +79,7 @@ class ListUtxosFragment : BaseDemoFragment<FragmentListUtxosBinding>() {
|
||||||
it.alias = "Demo_Utxos"
|
it.alias = "Demo_Utxos"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
synchronizer = runBlocking { Synchronizer.new(initializer) }
|
synchronizer = runBlocking { Synchronizer.new(initializer, seed) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
|
|
@ -80,7 +80,7 @@ class SendFragment : BaseDemoFragment<FragmentSendBinding>() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.let { initializer ->
|
}.let { initializer ->
|
||||||
synchronizer = Synchronizer.newBlocking(initializer)
|
synchronizer = Synchronizer.newBlocking(initializer, seed)
|
||||||
}
|
}
|
||||||
spendingKey = runBlocking {
|
spendingKey = runBlocking {
|
||||||
DerivationTool.deriveSpendingKeys(seed, ZcashNetwork.fromResources(requireApplicationContext())).first()
|
DerivationTool.deriveSpendingKeys(seed, ZcashNetwork.fromResources(requireApplicationContext())).first()
|
||||||
|
|
|
@ -2025,7 +2025,9 @@ dependencies = [
|
||||||
"jni",
|
"jni",
|
||||||
"log",
|
"log",
|
||||||
"log-panics",
|
"log-panics",
|
||||||
|
"schemer",
|
||||||
"secp256k1",
|
"secp256k1",
|
||||||
|
"secrecy",
|
||||||
"zcash_client_backend",
|
"zcash_client_backend",
|
||||||
"zcash_client_sqlite",
|
"zcash_client_sqlite",
|
||||||
"zcash_primitives",
|
"zcash_primitives",
|
||||||
|
|
|
@ -18,7 +18,9 @@ hex = "0.4"
|
||||||
jni = { version = "0.17", default-features = false }
|
jni = { version = "0.17", default-features = false }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
log-panics = "2.0.0"
|
log-panics = "2.0.0"
|
||||||
|
schemer = "0.2"
|
||||||
secp256k1 = "0.21"
|
secp256k1 = "0.21"
|
||||||
|
secrecy = "0.8"
|
||||||
zcash_client_backend = { version = "0.5", features = ["transparent-inputs"] }
|
zcash_client_backend = { version = "0.5", features = ["transparent-inputs"] }
|
||||||
zcash_client_sqlite = { version = "0.3", features = ["transparent-inputs"] }
|
zcash_client_sqlite = { version = "0.3", features = ["transparent-inputs"] }
|
||||||
zcash_primitives = "0.7"
|
zcash_primitives = "0.7"
|
||||||
|
|
|
@ -797,12 +797,13 @@ object DefaultSynchronizerFactory {
|
||||||
// can touch its views. and is probably related to FlowPagedList
|
// can touch its views. and is probably related to FlowPagedList
|
||||||
// TODO [#242]: https://github.com/zcash/zcash-android-wallet-sdk/issues/242
|
// TODO [#242]: https://github.com/zcash/zcash-android-wallet-sdk/issues/242
|
||||||
private const val DEFAULT_PAGE_SIZE = 1000
|
private const val DEFAULT_PAGE_SIZE = 1000
|
||||||
suspend fun defaultTransactionRepository(initializer: Initializer): TransactionRepository =
|
suspend fun defaultTransactionRepository(initializer: Initializer, seed: ByteArray?): TransactionRepository =
|
||||||
PagedTransactionRepository.new(
|
PagedTransactionRepository.new(
|
||||||
initializer.context,
|
initializer.context,
|
||||||
initializer.network,
|
initializer.network,
|
||||||
DEFAULT_PAGE_SIZE,
|
DEFAULT_PAGE_SIZE,
|
||||||
initializer.rustBackend,
|
initializer.rustBackend,
|
||||||
|
seed,
|
||||||
initializer.checkpoint,
|
initializer.checkpoint,
|
||||||
initializer.viewingKeys,
|
initializer.viewingKeys,
|
||||||
initializer.overwriteVks
|
initializer.overwriteVks
|
||||||
|
|
|
@ -444,11 +444,14 @@ interface Synchronizer {
|
||||||
* Synchronizer requires. It contains all information necessary to build a synchronizer and it is
|
* Synchronizer requires. It contains all information necessary to build a synchronizer and it is
|
||||||
* mainly responsible for initializing the databases associated with this synchronizer and loading
|
* mainly responsible for initializing the databases associated with this synchronizer and loading
|
||||||
* the rust backend.
|
* the rust backend.
|
||||||
|
* @param seed the wallet's seed phrase. This only needs to be provided if this method returns an
|
||||||
|
* error indicating that the seed phrase is required for a database migration.
|
||||||
*/
|
*/
|
||||||
suspend fun new(
|
suspend fun new(
|
||||||
initializer: Initializer
|
initializer: Initializer,
|
||||||
|
seed: ByteArray,
|
||||||
): Synchronizer {
|
): Synchronizer {
|
||||||
val repository = DefaultSynchronizerFactory.defaultTransactionRepository(initializer)
|
val repository = DefaultSynchronizerFactory.defaultTransactionRepository(initializer, seed)
|
||||||
val blockStore = DefaultSynchronizerFactory.defaultBlockStore(initializer)
|
val blockStore = DefaultSynchronizerFactory.defaultBlockStore(initializer)
|
||||||
val service = DefaultSynchronizerFactory.defaultService(initializer)
|
val service = DefaultSynchronizerFactory.defaultService(initializer)
|
||||||
val encoder = DefaultSynchronizerFactory.defaultEncoder(initializer, repository)
|
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.
|
* This is a blocking call, so it should not be called from the main thread.
|
||||||
*/
|
*/
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun newBlocking(initializer: Initializer): Synchronizer = runBlocking { new(initializer) }
|
fun newBlocking(initializer: Initializer, seed: ByteArray): Synchronizer = runBlocking {
|
||||||
|
new (initializer, seed)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -175,6 +175,10 @@ sealed class BirthdayException(message: String, cause: Throwable? = null) : SdkE
|
||||||
* Exceptions thrown by the initializer.
|
* Exceptions thrown by the initializer.
|
||||||
*/
|
*/
|
||||||
sealed class InitializerException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
|
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 FalseStart(cause: Throwable?) : InitializerException("Failed to initialize accounts due to: $cause", cause)
|
||||||
class AlreadyInitializedException(cause: Throwable, dbPath: String) : InitializerException(
|
class AlreadyInitializedException(cause: Throwable, dbPath: String) : InitializerException(
|
||||||
"Failed to initialize the blocks table" +
|
"Failed to initialize the blocks table" +
|
||||||
|
|
|
@ -5,6 +5,7 @@ import androidx.paging.PagedList
|
||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
import cash.z.ecc.android.sdk.db.commonDatabaseBuilder
|
import cash.z.ecc.android.sdk.db.commonDatabaseBuilder
|
||||||
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
|
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.ext.ZcashSdk
|
||||||
import cash.z.ecc.android.sdk.internal.SdkDispatchers
|
import cash.z.ecc.android.sdk.internal.SdkDispatchers
|
||||||
import cash.z.ecc.android.sdk.internal.SdkExecutors
|
import cash.z.ecc.android.sdk.internal.SdkExecutors
|
||||||
|
@ -122,11 +123,12 @@ internal class PagedTransactionRepository private constructor(
|
||||||
zcashNetwork: ZcashNetwork,
|
zcashNetwork: ZcashNetwork,
|
||||||
pageSize: Int = 10,
|
pageSize: Int = 10,
|
||||||
rustBackend: RustBackend,
|
rustBackend: RustBackend,
|
||||||
|
seed: ByteArray?,
|
||||||
birthday: Checkpoint,
|
birthday: Checkpoint,
|
||||||
viewingKeys: List<UnifiedFullViewingKey>,
|
viewingKeys: List<UnifiedFullViewingKey>,
|
||||||
overwriteVks: Boolean = false
|
overwriteVks: Boolean = false
|
||||||
): PagedTransactionRepository {
|
): PagedTransactionRepository {
|
||||||
initMissingDatabases(rustBackend, birthday, viewingKeys)
|
initMissingDatabases(rustBackend, seed, birthday, viewingKeys)
|
||||||
|
|
||||||
val db = buildDatabase(appContext.applicationContext, rustBackend.dataDbFile)
|
val db = buildDatabase(appContext.applicationContext, rustBackend.dataDbFile)
|
||||||
applyKeyMigrations(rustBackend, overwriteVks, viewingKeys)
|
applyKeyMigrations(rustBackend, overwriteVks, viewingKeys)
|
||||||
|
@ -172,10 +174,11 @@ internal class PagedTransactionRepository private constructor(
|
||||||
*/
|
*/
|
||||||
private suspend fun initMissingDatabases(
|
private suspend fun initMissingDatabases(
|
||||||
rustBackend: RustBackend,
|
rustBackend: RustBackend,
|
||||||
|
seed: ByteArray?,
|
||||||
birthday: Checkpoint,
|
birthday: Checkpoint,
|
||||||
viewingKeys: List<UnifiedFullViewingKey>
|
viewingKeys: List<UnifiedFullViewingKey>
|
||||||
) {
|
) {
|
||||||
maybeCreateDataDb(rustBackend)
|
maybeCreateDataDb(rustBackend, seed)
|
||||||
maybeInitBlocksTable(rustBackend, birthday)
|
maybeInitBlocksTable(rustBackend, birthday)
|
||||||
maybeInitAccountsTable(rustBackend, viewingKeys)
|
maybeInitAccountsTable(rustBackend, viewingKeys)
|
||||||
}
|
}
|
||||||
|
@ -183,9 +186,15 @@ internal class PagedTransactionRepository private constructor(
|
||||||
/**
|
/**
|
||||||
* Create the dataDb and its table, if it doesn't exist.
|
* Create the dataDb and its table, if it doesn't exist.
|
||||||
*/
|
*/
|
||||||
private suspend fun maybeCreateDataDb(rustBackend: RustBackend) {
|
private suspend fun maybeCreateDataDb(rustBackend: RustBackend, seed: ByteArray?) {
|
||||||
tryWarn("Warning: did not create dataDb. It probably already exists.") {
|
tryWarn(
|
||||||
rustBackend.initDataDb()
|
"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}")
|
twig("Initialized wallet for first run file: ${rustBackend.dataDbFile}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,9 +44,10 @@ internal class RustBackend private constructor(
|
||||||
// Wrapper Functions
|
// Wrapper Functions
|
||||||
//
|
//
|
||||||
|
|
||||||
override suspend fun initDataDb() = withContext(SdkDispatchers.DATABASE_IO) {
|
override suspend fun initDataDb(seed: ByteArray?) = withContext(SdkDispatchers.DATABASE_IO) {
|
||||||
initDataDb(
|
initDataDb(
|
||||||
dataDbFile.absolutePath,
|
dataDbFile.absolutePath,
|
||||||
|
seed,
|
||||||
networkId = network.id
|
networkId = network.id
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -384,7 +385,7 @@ internal class RustBackend private constructor(
|
||||||
//
|
//
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
private external fun initDataDb(dbDataPath: String, networkId: Int): Boolean
|
private external fun initDataDb(dbDataPath: String, seed: ByteArray?, networkId: Int): Int
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
private external fun initAccountsTableWithKeys(
|
private external fun initAccountsTableWithKeys(
|
||||||
|
|
|
@ -40,7 +40,7 @@ internal interface RustBackendWelding {
|
||||||
|
|
||||||
suspend fun initBlocksTable(checkpoint: Checkpoint): Boolean
|
suspend fun initBlocksTable(checkpoint: Checkpoint): Boolean
|
||||||
|
|
||||||
suspend fun initDataDb(): Boolean
|
suspend fun initDataDb(seed: ByteArray?): Int
|
||||||
|
|
||||||
fun isValidShieldedAddr(addr: String): Boolean
|
fun isValidShieldedAddr(addr: String): Boolean
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,9 @@ use jni::{
|
||||||
JNIEnv,
|
JNIEnv,
|
||||||
};
|
};
|
||||||
use log::Level;
|
use log::Level;
|
||||||
|
use schemer::MigratorError;
|
||||||
use secp256k1::PublicKey;
|
use secp256k1::PublicKey;
|
||||||
|
use secrecy::SecretVec;
|
||||||
use zcash_client_backend::keys::UnifiedSpendingKey;
|
use zcash_client_backend::keys::UnifiedSpendingKey;
|
||||||
use zcash_client_backend::{
|
use zcash_client_backend::{
|
||||||
address::RecipientAddress,
|
address::RecipientAddress,
|
||||||
|
@ -36,6 +38,7 @@ use zcash_client_backend::{
|
||||||
keys::{sapling, UnifiedFullViewingKey},
|
keys::{sapling, UnifiedFullViewingKey},
|
||||||
wallet::{OvkPolicy, WalletTransparentOutput},
|
wallet::{OvkPolicy, WalletTransparentOutput},
|
||||||
};
|
};
|
||||||
|
use zcash_client_sqlite::wallet::init::WalletMigrationError;
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
use zcash_client_sqlite::wallet::{delete_utxos_above, get_rewind_height};
|
use zcash_client_sqlite::wallet::{delete_utxos_above, get_rewind_height};
|
||||||
use zcash_client_sqlite::{
|
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()
|
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]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_initDataDb(
|
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_initDataDb(
|
||||||
env: JNIEnv<'_>,
|
env: JNIEnv<'_>,
|
||||||
_: JClass<'_>,
|
_: JClass<'_>,
|
||||||
db_data: JString<'_>,
|
db_data: JString<'_>,
|
||||||
|
seed: jbyteArray,
|
||||||
network_id: jint,
|
network_id: jint,
|
||||||
) -> jboolean {
|
) -> jint {
|
||||||
let res = panic::catch_unwind(|| {
|
let res = panic::catch_unwind(|| {
|
||||||
let network = parse_network(network_id as u32)?;
|
let network = parse_network(network_id as u32)?;
|
||||||
let db_path = utils::java_string_to_rust(&env, db_data);
|
let db_path = utils::java_string_to_rust(&env, db_data);
|
||||||
let seed = None; // TODO Update
|
|
||||||
WalletDb::for_path(db_path, network)
|
let mut db_data = WalletDb::for_path(db_path, network)
|
||||||
.map_err(|e| format_err!("Error while opening data DB: {}", e))
|
.map_err(|e| format_err!("Error while opening data DB: {}", e))?;
|
||||||
.and_then(|mut db| {
|
|
||||||
init_wallet_db(&mut db, seed)
|
let seed = (!seed.is_null()).then(|| SecretVec::new(env.convert_byte_array(seed).unwrap()));
|
||||||
.map_err(|e| format_err!("Error while initializing data DB: {}", e))
|
|
||||||
})
|
match init_wallet_db(&mut db_data, seed) {
|
||||||
.map(|()| JNI_TRUE)
|
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]
|
#[no_mangle]
|
||||||
|
|
Loading…
Reference in New Issue