diff --git a/backend-lib/Cargo.lock b/backend-lib/Cargo.lock index ddd59ba7c..f4c38742a 100644 --- a/backend-lib/Cargo.lock +++ b/backend-lib/Cargo.lock @@ -1130,8 +1130,7 @@ dependencies = [ [[package]] name = "equihash" version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab579d7cf78477773b03e80bc2f89702ef02d7112c711d54ca93dcdce68533d5" +source = "git+https://github.com/zcash/librustzcash?rev=c8aff5bd06ca7854175234dd88034ff392b36d23#c8aff5bd06ca7854175234dd88034ff392b36d23" dependencies = [ "blake2b_simd", "byteorder", @@ -1167,8 +1166,7 @@ dependencies = [ [[package]] name = "f4jumble" version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a83e8d7fd0c526af4aad893b7c9fe41e2699ed8a776a6c74aecdeafe05afc75" +source = "git+https://github.com/zcash/librustzcash?rev=c8aff5bd06ca7854175234dd88034ff392b36d23#c8aff5bd06ca7854175234dd88034ff392b36d23" dependencies = [ "blake2b_simd", ] @@ -2933,6 +2931,7 @@ dependencies = [ "libsqlite3-sys", "smallvec", "time", + "uuid", ] [[package]] @@ -4728,6 +4727,9 @@ name = "uuid" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" +dependencies = [ + "getrandom", +] [[package]] name = "valuable" @@ -5222,6 +5224,7 @@ dependencies = [ "tor-rtcompat", "tracing", "tracing-subscriber", + "uuid", "xz2", "zcash_address", "zcash_client_backend", @@ -5234,8 +5237,7 @@ dependencies = [ [[package]] name = "zcash_address" version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ff95eac82f71286a79c750e674550d64fb2b7aadaef7b89286b2917f645457d" +source = "git+https://github.com/zcash/librustzcash?rev=c8aff5bd06ca7854175234dd88034ff392b36d23#c8aff5bd06ca7854175234dd88034ff392b36d23" dependencies = [ "bech32", "bs58", @@ -5247,8 +5249,7 @@ dependencies = [ [[package]] name = "zcash_client_backend" version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e29a9975a741754e9d68c1326df809049712d3d3e831df9983eb631b8c2c2257" +source = "git+https://github.com/zcash/librustzcash?rev=c8aff5bd06ca7854175234dd88034ff392b36d23#c8aff5bd06ca7854175234dd88034ff392b36d23" dependencies = [ "arti-client", "base64", @@ -5307,8 +5308,7 @@ dependencies = [ [[package]] name = "zcash_client_sqlite" version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7ae0c0db930b76831161205be4d93443417a600c7212a27e50df9288c407581" +source = "git+https://github.com/zcash/librustzcash?rev=c8aff5bd06ca7854175234dd88034ff392b36d23#c8aff5bd06ca7854175234dd88034ff392b36d23" dependencies = [ "bip32", "bs58", @@ -5345,8 +5345,7 @@ dependencies = [ [[package]] name = "zcash_encoding" version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "052d8230202f0a018cd9b5d1b56b94cd25e18eccc2d8665073bcea8261ab87fc" +source = "git+https://github.com/zcash/librustzcash?rev=c8aff5bd06ca7854175234dd88034ff392b36d23#c8aff5bd06ca7854175234dd88034ff392b36d23" dependencies = [ "byteorder", "nonempty", @@ -5355,8 +5354,7 @@ dependencies = [ [[package]] name = "zcash_keys" version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf15baad7e4a87cca57af718766a0d5bb03e0cbf118e3ef6462697a4664b88c" +source = "git+https://github.com/zcash/librustzcash?rev=c8aff5bd06ca7854175234dd88034ff392b36d23#c8aff5bd06ca7854175234dd88034ff392b36d23" dependencies = [ "bech32", "bip32", @@ -5397,8 +5395,7 @@ dependencies = [ [[package]] name = "zcash_primitives" version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c1573280a8d545009381af602c714ab53c43088d089e0d17e89740184f1712b" +source = "git+https://github.com/zcash/librustzcash?rev=c8aff5bd06ca7854175234dd88034ff392b36d23#c8aff5bd06ca7854175234dd88034ff392b36d23" dependencies = [ "aes", "bip32", @@ -5436,8 +5433,7 @@ dependencies = [ [[package]] name = "zcash_proofs" version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d83453ba15e77d8f1806ed8558b7ab2b27f0a57dab037b1c8d0c8fb57820cf" +source = "git+https://github.com/zcash/librustzcash?rev=c8aff5bd06ca7854175234dd88034ff392b36d23#c8aff5bd06ca7854175234dd88034ff392b36d23" dependencies = [ "bellman", "blake2b_simd", @@ -5459,8 +5455,7 @@ dependencies = [ [[package]] name = "zcash_protocol" version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4bbb28b59321f47454e69c2d95c11c227bb1a21bfa3381bd43c4ac98f5caee1" +source = "git+https://github.com/zcash/librustzcash?rev=c8aff5bd06ca7854175234dd88034ff392b36d23#c8aff5bd06ca7854175234dd88034ff392b36d23" dependencies = [ "document-features", "memuse", @@ -5531,8 +5526,7 @@ dependencies = [ [[package]] name = "zip321" version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3e613defb0940acef1f54774b51c7f48f2fa705613dd800870dc69f35cd2ea" +source = "git+https://github.com/zcash/librustzcash?rev=c8aff5bd06ca7854175234dd88034ff392b36d23#c8aff5bd06ca7854175234dd88034ff392b36d23" dependencies = [ "base64", "nom", diff --git a/backend-lib/Cargo.toml b/backend-lib/Cargo.toml index f67c3e396..ebecedf9a 100644 --- a/backend-lib/Cargo.toml +++ b/backend-lib/Cargo.toml @@ -32,6 +32,7 @@ rayon = "1.7" # JNI anyhow = "1" jni = { version = "0.21", default-features = false } +uuid = "1" # Logging log-panics = "2.0.0" @@ -69,3 +70,10 @@ xz2 = { version = "0.1", features = ["static"] } name = "zcashwalletsdk" path = "src/main/rust/lib.rs" crate-type = ["staticlib", "cdylib"] + +[patch.crates-io] +zcash_address = { git = "https://github.com/zcash/librustzcash", rev = "c8aff5bd06ca7854175234dd88034ff392b36d23" } +zcash_client_backend = { git = "https://github.com/zcash/librustzcash", rev = "c8aff5bd06ca7854175234dd88034ff392b36d23" } +zcash_client_sqlite = { git = "https://github.com/zcash/librustzcash", rev = "c8aff5bd06ca7854175234dd88034ff392b36d23" } +zcash_primitives = { git = "https://github.com/zcash/librustzcash", rev = "c8aff5bd06ca7854175234dd88034ff392b36d23" } +zcash_proofs = { git = "https://github.com/zcash/librustzcash", rev = "c8aff5bd06ca7854175234dd88034ff392b36d23" } diff --git a/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/Backend.kt b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/Backend.kt index 4fe09ba63..5ad5804c2 100644 --- a/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/Backend.kt +++ b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/Backend.kt @@ -24,7 +24,7 @@ interface Backend { suspend fun initBlockMetaDb(): Int suspend fun proposeTransfer( - accountIndex: Int, + accountUuid: ByteArray, to: String, value: Long, memo: ByteArray? = null @@ -35,12 +35,12 @@ interface Backend { */ @Throws(RuntimeException::class) suspend fun proposeTransferFromUri( - accountIndex: Int, + accountUuid: ByteArray, uri: String ): ProposalUnsafe suspend fun proposeShielding( - accountIndex: Int, + accountUuid: ByteArray, shieldingThreshold: Long, memo: ByteArray? = null, transparentReceiver: String? = null @@ -109,13 +109,13 @@ interface Backend { fun isValidTexAddr(addr: String): Boolean @Throws(RuntimeException::class) - suspend fun getCurrentAddress(accountIndex: Int): String + suspend fun getCurrentAddress(accountUuid: ByteArray): String fun getTransparentReceiver(ua: String): String? fun getSaplingReceiver(ua: String): String? - suspend fun listTransparentReceivers(accountIndex: Int): List + suspend fun listTransparentReceivers(accountUuid: ByteArray): List fun getBranchIdForHeight(height: Long): Long diff --git a/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/jni/RustBackend.kt b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/jni/RustBackend.kt index 6f7d526e2..6194c4f94 100644 --- a/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/jni/RustBackend.kt +++ b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/jni/RustBackend.kt @@ -114,11 +114,11 @@ class RustBackend private constructor( ) } - override suspend fun getCurrentAddress(accountIndex: Int) = + override suspend fun getCurrentAddress(accountUuid: ByteArray) = withContext(SdkDispatchers.DATABASE_IO) { getCurrentAddress( dataDbFile.absolutePath, - accountIndex, + accountUuid, networkId = networkId ) } @@ -127,11 +127,11 @@ class RustBackend private constructor( override fun getSaplingReceiver(ua: String) = getSaplingReceiverForUnifiedAddress(ua) - override suspend fun listTransparentReceivers(accountIndex: Int): List { + override suspend fun listTransparentReceivers(accountUuid: ByteArray): List { return withContext(SdkDispatchers.DATABASE_IO) { listTransparentReceivers( dbDataPath = dataDbFile.absolutePath, - accountIndex = accountIndex, + accountUuid = accountUuid, networkId = networkId ).asList() } @@ -318,14 +318,14 @@ class RustBackend private constructor( } override suspend fun proposeTransferFromUri( - accountIndex: Int, + accountUuid: ByteArray, uri: String ): ProposalUnsafe = withContext(SdkDispatchers.DATABASE_IO) { ProposalUnsafe.parse( proposeTransferFromUri( dataDbFile.absolutePath, - accountIndex, + accountUuid, uri, networkId = networkId, ) @@ -333,7 +333,7 @@ class RustBackend private constructor( } override suspend fun proposeTransfer( - accountIndex: Int, + accountUuid: ByteArray, to: String, value: Long, memo: ByteArray? @@ -342,7 +342,7 @@ class RustBackend private constructor( ProposalUnsafe.parse( proposeTransfer( dataDbFile.absolutePath, - accountIndex, + accountUuid, to, value, memo, @@ -352,7 +352,7 @@ class RustBackend private constructor( } override suspend fun proposeShielding( - accountIndex: Int, + accountUuid: ByteArray, shieldingThreshold: Long, memo: ByteArray?, transparentReceiver: String? @@ -360,7 +360,7 @@ class RustBackend private constructor( return withContext(SdkDispatchers.DATABASE_IO) { proposeShielding( dataDbFile.absolutePath, - accountIndex, + accountUuid, shieldingThreshold, memo, transparentReceiver, @@ -510,7 +510,7 @@ class RustBackend private constructor( @JvmStatic private external fun getCurrentAddress( dbDataPath: String, - accountIndex: Int, + accountUuid: ByteArray, networkId: Int ): String @@ -523,7 +523,7 @@ class RustBackend private constructor( @JvmStatic private external fun listTransparentReceivers( dbDataPath: String, - accountIndex: Int, + accountUuid: ByteArray, networkId: Int ): Array @@ -671,7 +671,7 @@ class RustBackend private constructor( @JvmStatic private external fun proposeTransferFromUri( dbDataPath: String, - accountIndex: Int, + accountUuid: ByteArray, uri: String, networkId: Int, ): ByteArray @@ -680,7 +680,7 @@ class RustBackend private constructor( @Suppress("LongParameterList") private external fun proposeTransfer( dbDataPath: String, - accountIndex: Int, + accountUuid: ByteArray, to: String, value: Long, memo: ByteArray?, @@ -691,7 +691,7 @@ class RustBackend private constructor( @Suppress("LongParameterList") private external fun proposeShielding( dbDataPath: String, - accountIndex: Int, + accountUuid: ByteArray, shieldingThreshold: Long, memo: ByteArray?, transparentReceiver: String?, diff --git a/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/JniAccount.kt b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/JniAccount.kt index a79a9e002..a7fd0d646 100644 --- a/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/JniAccount.kt +++ b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/JniAccount.kt @@ -5,19 +5,19 @@ import androidx.annotation.Keep /** * Serves as cross layer (Kotlin, Rust) communication class. * - * @param accountIndex the ZIP 32 account index. + * @param accountUuid the "one-way stable" identifier for the account. * @param ufvk The account's Unified Full Viewing Key, if any. * @throws IllegalArgumentException if the values are inconsistent. */ @Keep @Suppress("LongParameterList") class JniAccount( - val accountIndex: Int, + val accountUuid: ByteArray, val ufvk: String?, ) { init { - require(accountIndex >= 0) { - "Account index must be non-negative" + require(accountUuid.size == 16) { + "Account UUID must be 16 bytes" } } } diff --git a/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/JniAccountBalance.kt b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/JniAccountBalance.kt index 437e67acc..a7d2580ac 100644 --- a/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/JniAccountBalance.kt +++ b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/JniAccountBalance.kt @@ -25,7 +25,7 @@ import androidx.annotation.Keep @Keep @Suppress("LongParameterList") class JniAccountBalance( - val account: Int, + val account: ByteArray, val saplingVerifiedBalance: Long, val saplingChangePending: Long, val saplingValuePending: Long, diff --git a/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/JniUnifiedSpendingKey.kt b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/JniUnifiedSpendingKey.kt index 5eaa1ffe0..a6c80b4db 100644 --- a/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/JniUnifiedSpendingKey.kt +++ b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/JniUnifiedSpendingKey.kt @@ -14,12 +14,13 @@ import androidx.annotation.Keep @Keep class JniUnifiedSpendingKey( /** - * The [ZIP 32](https://zips.z.cash/zip-0032) account index used to derive this key. + * The "one-way stable" identifier for the account tracked in the wallet to which this + * spending key belongs. */ - val account: Int, + val accountUuid: ByteArray, /** * The binary encoding of the [ZIP 316](https://zips.z.cash/zip-0316) Unified Spending - * Key for [account]. + * Key for [accountUuid]. * * This encoding **MUST NOT** be exposed to users. It is an internal encoding that is * inherently unstable, and only intended to be passed between the SDK and the storage @@ -28,7 +29,7 @@ class JniUnifiedSpendingKey( val bytes: ByteArray ) { // Override to prevent leaking key to logs - override fun toString() = "JniUnifiedSpendingKey(account=$account, bytes=***)" + override fun toString() = "JniUnifiedSpendingKey(account=$accountUuid, bytes=***)" override fun equals(other: Any?): Boolean { if (this === other) return true @@ -36,14 +37,14 @@ class JniUnifiedSpendingKey( other as JniUnifiedSpendingKey - if (account != other.account) return false + if (!accountUuid.contentEquals(other.accountUuid)) return false if (!bytes.contentEquals(other.bytes)) return false return true } override fun hashCode(): Int { - var result = account.hashCode() + var result = accountUuid.hashCode() result = 31 * result + bytes.hashCode() return result } diff --git a/backend-lib/src/main/rust/lib.rs b/backend-lib/src/main/rust/lib.rs index ea1f369cd..6a81e082a 100644 --- a/backend-lib/src/main/rust/lib.rs +++ b/backend-lib/src/main/rust/lib.rs @@ -18,6 +18,7 @@ use tor_rtcompat::BlockOn; use tracing::{debug, error}; use tracing_subscriber::prelude::*; use tracing_subscriber::reload; +use uuid::Uuid; use zcash_address::{ToAddress, ZcashAddress}; use zcash_client_backend::data_api::{TransactionDataRequest, TransactionStatus}; use zcash_client_backend::fees::zip317::MultiOutputChangeStrategy; @@ -44,6 +45,7 @@ use zcash_client_backend::{ ShieldedProtocol, }; use zcash_client_sqlite::error::SqliteClientError; +use zcash_client_sqlite::AccountUuid; use zcash_client_sqlite::{ chain::{init::init_blockmeta_db, BlockMeta}, wallet::init::{init_wallet_db, WalletMigrationError}, @@ -113,43 +115,16 @@ fn zip32_account_index_from_jint(account_index: jint) -> anyhow::Result( + env: &JNIEnv, db_data: &WalletDb, - account_index: jint, + account_uuid: JByteArray, ) -> anyhow::Result { - let requested_account_index = zip32_account_index_from_jint(account_index)?; - - // Find the single account matching the given ZIP 32 account index. - let mut accounts = db_data - .get_account_ids()? - .into_iter() - .filter_map(|account_id| { - db_data - .get_account(account_id) - .map_err(|e| { - anyhow!( - "Database error encountered retrieving account {:?}: {}", - account_id, - e - ) - }) - .and_then(|acct_opt| - acct_opt.ok_or_else(|| - anyhow!( - "Wallet data corrupted: unable to retrieve account data for account {:?}", - account_id - ) - ).map(|account| match account.source() { - AccountSource::Derived { account_index, .. } if account_index == requested_account_index => Some(account), - _ => None - }) - ) - .transpose() - }); + let account_uuid = + AccountUuid::from_uuid(Uuid::from_slice(&env.convert_byte_array(account_uuid)?)?); - match (accounts.next(), accounts.next()) { - (Some(account), None) => Ok(account?.id()), - (None, None) => Err(anyhow!("Account does not exist")), - (_, Some(_)) => Err(anyhow!("Account index matches more than one account")), + match db_data.get_account_for_uuid(account_uuid)? { + Some(account) => Ok(account.id()), + None => Err(anyhow!("Account does not exist")), } } @@ -297,11 +272,6 @@ fn encode_account<'a, P: Parameters>( network: &P, account: zcash_client_sqlite::wallet::Account, ) -> jni::errors::Result> { - let account_index = match account.source() { - AccountSource::Derived { account_index, .. } => account_index, - AccountSource::Imported { .. } => panic!("Should have been filtered out"), - }; - let ufvk = match account.ufvk() { Some(ufvk) => env.new_string(ufvk.encode(network))?.into(), None => JObject::null(), @@ -309,10 +279,9 @@ fn encode_account<'a, P: Parameters>( env.new_object( JNI_ACCOUNT, - "(ILjava/lang/String;)V", + "([BLjava/lang/String;)V", &[ - // TODO: This will be replaced by the multi-seed-compatible account ID. - JValue::Int(u32::from(account_index) as i32), + (&env.byte_array_from_slice(account.uuid().expose_uuid().as_bytes())?).into(), (&ufvk).into(), ], ) @@ -341,13 +310,6 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_getAccoun }) .collect::, _>>()?; - // Filter out imported accounts (for which we don't know a ZIP 32 account index). - // TODO: Remove this when we switch to account identifiers. - let accounts = accounts - .into_iter() - .filter(|account| matches!(account.source(), AccountSource::Derived { .. })) - .collect::>(); - let first_account = accounts.first().cloned(); Ok(utils::rust_vec_to_java( @@ -495,14 +457,14 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_getCurren mut env: JNIEnv<'local>, _: JClass<'local>, db_data: JString<'local>, - account_index: jint, + account_uuid: JByteArray<'local>, network_id: jint, ) -> jstring { let res = catch_unwind(&mut env, |env| { let _span = tracing::info_span!("RustBackend.getCurrentAddress").entered(); let network = parse_network(network_id as u32)?; let db_data = wallet_db(env, network, db_data)?; - let account = account_id_from_jni(&db_data, account_index)?; + let account = account_id_from_jni(&env, &db_data, account_uuid)?; match db_data.get_current_address(account) { Ok(Some(addr)) => { @@ -1184,7 +1146,7 @@ const JNI_ACCOUNT_BALANCE: &str = "cash/z/ecc/android/sdk/internal/model/JniAcco fn encode_account_balance<'a>( env: &mut JNIEnv<'a>, - account: &zip32::AccountId, + account_uuid: &AccountUuid, balance: &AccountBalance, ) -> jni::errors::Result> { let sapling_verified_balance = Amount::from(balance.sapling_balance().spendable_value()); @@ -1203,9 +1165,9 @@ fn encode_account_balance<'a>( env.new_object( JNI_ACCOUNT_BALANCE, - "(IJJJJJJJ)V", + "([BJJJJJJJ)V", &[ - JValue::Int(u32::from(*account) as i32), + (&env.byte_array_from_slice(account_uuid.expose_uuid().as_bytes())?).into(), JValue::Long(sapling_verified_balance.into()), JValue::Long(sapling_change_pending.into()), JValue::Long(sapling_value_pending.into()), @@ -1228,28 +1190,38 @@ fn encode_wallet_summary<'a, P: Parameters>( db_data: &WalletDb, summary: WalletSummary, ) -> anyhow::Result> { + let account_balances = summary + .account_balances() + .iter() + .map(|(account_id, balance)| { + let account_uuid = db_data + .get_account(*account_id)? + .expect("the account exists in the wallet") + .uuid(); + Ok::<_, anyhow::Error>((account_uuid, balance)) + }) + .collect::, _>>()?; + + let first_account_balance = account_balances + .first() + .map(|(account_uuid, balance)| (*account_uuid, **balance)); + let account_balances = utils::rust_vec_to_java( env, - summary - .account_balances() - .iter() - .map(|(account_id, balance)| { - let account_index = match db_data - .get_account(*account_id)? - .expect("the account exists in the wallet") - .source() - { - AccountSource::Derived { account_index, .. } => account_index, - AccountSource::Imported { .. } => { - unreachable!("Imported accounts are unimplemented") - } - }; - Ok::<_, anyhow::Error>((account_index, balance)) - }) - .collect::>()?, + account_balances, JNI_ACCOUNT_BALANCE, - |env, (account_index, balance)| encode_account_balance(env, &account_index, balance), - |env| encode_account_balance(env, &zip32::AccountId::ZERO, &AccountBalance::ZERO), + |env, (account_uuid, balance)| encode_account_balance(env, &account_uuid, balance), + |env| { + // The array contains non-null values in Kotlin. This returns `null` if there + // there are no accounts with balances, which is fine because the array has no + // entries and thus the empty element is never used. + match first_account_balance { + Some((account_uuid, balance)) => { + encode_account_balance(env, &account_uuid, &balance) + } + None => Ok(JObject::null()), + } + }, )?; let (progress_numerator, progress_denominator) = @@ -1639,7 +1611,7 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_proposeTr mut env: JNIEnv<'local>, _: JClass<'local>, db_data: JString<'local>, - account_index: jint, + account_uuid: JByteArray<'local>, payment_uri: JString<'local>, network_id: jint, ) -> jbyteArray { @@ -1647,7 +1619,7 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_proposeTr let _span = tracing::info_span!("RustBackend.proposeTransfer").entered(); let network = parse_network(network_id as u32)?; let mut db_data = wallet_db(env, network, db_data)?; - let account = account_id_from_jni(&db_data, account_index)?; + let account = account_id_from_jni(&env, &db_data, account_uuid)?; let payment_uri = utils::java_string_to_rust(env, &payment_uri); // Always use ZIP 317 fees @@ -1683,7 +1655,7 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_proposeTr mut env: JNIEnv<'local>, _: JClass<'local>, db_data: JString<'local>, - account_index: jint, + account_uuid: JByteArray<'local>, to: JString<'local>, value: jlong, memo: JByteArray<'local>, @@ -1693,7 +1665,7 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_proposeTr let _span = tracing::info_span!("RustBackend.proposeTransfer").entered(); let network = parse_network(network_id as u32)?; let mut db_data = wallet_db(env, network, db_data)?; - let account = account_id_from_jni(&db_data, account_index)?; + let account = account_id_from_jni(&env, &db_data, account_uuid)?; let to = utils::java_string_to_rust(env, &to); let value = NonNegativeAmount::from_nonnegative_i64(value) .map_err(|_| anyhow!("Invalid amount, out of range"))?; @@ -1747,7 +1719,7 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_proposeSh mut env: JNIEnv<'local>, _: JClass<'local>, db_data: JString<'local>, - account_index: jint, + account_uuid: JByteArray<'local>, shielding_threshold: jlong, memo: JByteArray<'local>, transparent_receiver: JString<'local>, @@ -1757,7 +1729,7 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_proposeSh let _span = tracing::info_span!("RustBackend.proposeShielding").entered(); let network = parse_network(network_id as u32)?; let mut db_data = wallet_db(env, network, db_data)?; - let account = account_id_from_jni(&db_data, account_index)?; + let account = account_id_from_jni(&env, &db_data, account_uuid)?; let shielding_threshold = NonNegativeAmount::from_nonnegative_i64(shielding_threshold) .map_err(|_| anyhow!("Invalid shielding threshold, out of range"))?; @@ -2266,7 +2238,7 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_listTrans mut env: JNIEnv<'local>, _: JClass<'local>, db_data: JString<'local>, - account_index: jint, + account_uuid: JByteArray<'local>, network_id: jint, ) -> jobjectArray { let res = catch_unwind(&mut env, |env| { @@ -2274,7 +2246,7 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_listTrans let network = parse_network(network_id as u32)?; let zcash_network = network.network_type(); let db_data = wallet_db(env, network, db_data)?; - let account = account_id_from_jni(&db_data, account_index)?; + let account = account_id_from_jni(&env, &db_data, account_uuid)?; match db_data.get_transparent_receivers(account) { Ok(receivers) => { diff --git a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/ui/screen/home/viewmodel/WalletViewModel.kt b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/ui/screen/home/viewmodel/WalletViewModel.kt index 1053a16b6..aa91f203a 100644 --- a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/ui/screen/home/viewmodel/WalletViewModel.kt +++ b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/ui/screen/home/viewmodel/WalletViewModel.kt @@ -107,7 +107,7 @@ class WalletViewModel(application: Application) : AndroidViewModel(application) DerivationTool.getInstance().deriveUnifiedSpendingKey( seed = bip39Seed, network = it.network, - account = Account.DEFAULT + accountIndex = 0 ) }.stateIn( viewModelScope, diff --git a/sdk-incubator-lib/src/androidTest/kotlin/cash/z/ecc/android/sdk/internal/DerivationToolImplTest.kt b/sdk-incubator-lib/src/androidTest/kotlin/cash/z/ecc/android/sdk/internal/DerivationToolImplTest.kt index 62ea17b34..c16855885 100644 --- a/sdk-incubator-lib/src/androidTest/kotlin/cash/z/ecc/android/sdk/internal/DerivationToolImplTest.kt +++ b/sdk-incubator-lib/src/androidTest/kotlin/cash/z/ecc/android/sdk/internal/DerivationToolImplTest.kt @@ -14,7 +14,7 @@ import kotlin.test.assertEquals class DerivationToolImplTest { private val seedPhrase = WalletFixture.Alice.seedPhrase private val network = ZcashNetwork.Mainnet - private val account = Account.DEFAULT + private val accountIndex = 0 @OptIn(ExperimentalEncodingApi::class) @Test @@ -25,7 +25,7 @@ class DerivationToolImplTest { contextString = CONTEXT.toByteArray(), seed = seedPhrase.toByteArray(), network = network, - account = account, + accountIndex = accountIndex, ) assertEquals("byyNHiMfj8N2tiCHc4Mv/0ts0IuUqDPe99MvW8B03IY=", Base64.encode(key)) } @@ -63,7 +63,7 @@ class DerivationToolImplTest { contextString = "Zcash test vectors".toByteArray(), seed = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f".hexToByteArray(), network = network, - account = account, + accountIndex = accountIndex, ) assertEquals("bf60078362a09234fcbc6bf6c8a87bde9fc73776bf93f37adbcc439a85574a9a", secretKey.toHex()) } diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/DerivationToolExt.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/DerivationToolExt.kt index d5691ea09..26f57ff7f 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/DerivationToolExt.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/DerivationToolExt.kt @@ -9,8 +9,8 @@ import cash.z.ecc.android.sdk.model.ZcashNetwork fun Derivation.deriveUnifiedAddress( seed: ByteArray, network: ZcashNetwork, - account: Account -): String = deriveUnifiedAddress(seed, network.id, account.value) + accountIndex: Int +): String = deriveUnifiedAddress(seed, network.id, accountIndex) fun Derivation.deriveUnifiedAddress( viewingKey: String, @@ -20,8 +20,8 @@ fun Derivation.deriveUnifiedAddress( fun Derivation.deriveUnifiedSpendingKey( seed: ByteArray, network: ZcashNetwork, - account: Account -): UnifiedSpendingKey = UnifiedSpendingKey(deriveUnifiedSpendingKey(seed, network.id, account.value)) + accountIndex: Int +): UnifiedSpendingKey = UnifiedSpendingKey(deriveUnifiedSpendingKey(seed, network.id, accountIndex)) fun Derivation.deriveUnifiedFullViewingKey( usk: UnifiedSpendingKey, @@ -30,7 +30,7 @@ fun Derivation.deriveUnifiedFullViewingKey( UnifiedFullViewingKey( deriveUnifiedFullViewingKey( JniUnifiedSpendingKey( - usk.account.value, + usk.account.accountUuid, usk.copyBytes() ), network.id @@ -57,5 +57,5 @@ fun Derivation.deriveArbitraryAccountKeyTypesafe( contextString: ByteArray, seed: ByteArray, network: ZcashNetwork, - account: Account -): ByteArray = deriveArbitraryAccountKey(contextString, seed, network.id, account.value) + accountIndex: Int +): ByteArray = deriveArbitraryAccountKey(contextString, seed, network.id, accountIndex) diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/TypesafeBackendImpl.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/TypesafeBackendImpl.kt index 15a198745..b8067c017 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/TypesafeBackendImpl.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/TypesafeBackendImpl.kt @@ -27,7 +27,7 @@ internal class TypesafeBackendImpl(private val backend: Backend) : TypesafeBacke override val network: ZcashNetwork get() = ZcashNetwork.from(backend.networkId) - override suspend fun getAccounts(): List = backend.getAccounts().map { Account(it.accountIndex.toInt()) } + override suspend fun getAccounts(): List = backend.getAccounts().map { Account(it.accountUuid) } override suspend fun createAccountAndGetSpendingKey( seed: ByteArray, @@ -49,7 +49,7 @@ internal class TypesafeBackendImpl(private val backend: Backend) : TypesafeBacke ): Proposal = Proposal.fromUnsafe( backend.proposeTransferFromUri( - account.value, + account.accountUuid, uri ) ) @@ -63,7 +63,7 @@ internal class TypesafeBackendImpl(private val backend: Backend) : TypesafeBacke ): Proposal = Proposal.fromUnsafe( backend.proposeTransfer( - account.value, + account.accountUuid, to, value, memo @@ -77,7 +77,7 @@ internal class TypesafeBackendImpl(private val backend: Backend) : TypesafeBacke transparentReceiver: String? ): Proposal? = backend.proposeShielding( - account.value, + account.accountUuid, shieldingThreshold, memo, transparentReceiver @@ -98,14 +98,14 @@ internal class TypesafeBackendImpl(private val backend: Backend) : TypesafeBacke override suspend fun getCurrentAddress(account: Account): String { return runCatching { - backend.getCurrentAddress(account.value) + backend.getCurrentAddress(account.accountUuid) }.onFailure { Twig.warn(it) { "Currently unable to get current address" } }.getOrElse { throw RustLayerException.GetCurrentAddressException(it) } } override suspend fun listTransparentReceivers(account: Account): List { - return backend.listTransparentReceivers(account.value) + return backend.listTransparentReceivers(account.accountUuid) } override fun getBranchIdForHeight(height: BlockHeight): Long { diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/TypesafeDerivationToolImpl.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/TypesafeDerivationToolImpl.kt index 3356c0aae..a4883feb5 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/TypesafeDerivationToolImpl.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/TypesafeDerivationToolImpl.kt @@ -1,6 +1,5 @@ package cash.z.ecc.android.sdk.internal -import cash.z.ecc.android.sdk.model.Account import cash.z.ecc.android.sdk.model.UnifiedFullViewingKey import cash.z.ecc.android.sdk.model.UnifiedSpendingKey import cash.z.ecc.android.sdk.model.ZcashNetwork @@ -21,14 +20,14 @@ internal class TypesafeDerivationToolImpl(private val derivation: Derivation) : override suspend fun deriveUnifiedSpendingKey( seed: ByteArray, network: ZcashNetwork, - account: Account - ): UnifiedSpendingKey = derivation.deriveUnifiedSpendingKey(seed, network, account) + accountIndex: Int + ): UnifiedSpendingKey = derivation.deriveUnifiedSpendingKey(seed, network, accountIndex) override suspend fun deriveUnifiedAddress( seed: ByteArray, network: ZcashNetwork, - account: Account - ): String = derivation.deriveUnifiedAddress(seed, network, account) + accountIndex: Int + ): String = derivation.deriveUnifiedAddress(seed, network, accountIndex) override suspend fun deriveUnifiedAddress( viewingKey: String, @@ -44,6 +43,6 @@ internal class TypesafeDerivationToolImpl(private val derivation: Derivation) : contextString: ByteArray, seed: ByteArray, network: ZcashNetwork, - account: Account - ): ByteArray = derivation.deriveArbitraryAccountKeyTypesafe(contextString, seed, network, account) + accountIndex: Int + ): ByteArray = derivation.deriveArbitraryAccountKeyTypesafe(contextString, seed, network, accountIndex) } diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/Account.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/Account.kt index 0c82fad26..1cf2118cb 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/Account.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/Account.kt @@ -1,16 +1,29 @@ package cash.z.ecc.android.sdk.model /** - * A [ZIP 32](https://zips.z.cash/zip-0032) account index. + * Unique identifier for a specific account tracked by a `Synchronizer`. * - * @param value A 0-based account index. Must be >= 0. + * Account identifiers are "one-way stable": a given identifier always points to a + * specific viewing key within a specific `Synchronizer` instance, but the same viewing + * key may have multiple account identifiers over time. In particular, this SDK upholds + * the following properties: + * + * - When an account starts being tracked within a `Synchronizer` instance, a new + * `Account` is generated. + * - If an `Account` is present within a `Synchronizer`, it always points to the same + * account. + * + * What this means is that account identifiers are not stable across "wallet recreation + * events". Examples of these include: + * - Restoring a wallet from a backed-up seed. + * - Importing the same viewing key into two different wallet instances. + * + * @param accountUuid The account identifier. Must be length 16. */ -data class Account(val value: Int) { +data class Account(val accountUuid: ByteArray) { init { - require(value >= 0) { "Account index must be >= 0 but actually is $value" } - } - - companion object { - val DEFAULT = Account(0) + require(accountUuid.size == 16) { + "Account UUID must be 16 bytes" + } } } diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/UnifiedSpendingKey.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/UnifiedSpendingKey.kt index 648ba1910..6eb0f9372 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/UnifiedSpendingKey.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/UnifiedSpendingKey.kt @@ -28,7 +28,7 @@ class UnifiedSpendingKey private constructor( private val bytes: FirstClassByteArray ) { internal constructor(uskJni: JniUnifiedSpendingKey) : this( - Account(uskJni.account), + Account(uskJni.accountUuid), FirstClassByteArray(uskJni.bytes.copyOf()) ) diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/tool/DerivationTool.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/tool/DerivationTool.kt index a21eeaa42..375d7f10a 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/tool/DerivationTool.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/tool/DerivationTool.kt @@ -4,7 +4,6 @@ import cash.z.ecc.android.sdk.internal.Derivation import cash.z.ecc.android.sdk.internal.SuspendingLazy import cash.z.ecc.android.sdk.internal.TypesafeDerivationToolImpl import cash.z.ecc.android.sdk.internal.jni.RustDerivationTool -import cash.z.ecc.android.sdk.model.Account import cash.z.ecc.android.sdk.model.UnifiedFullViewingKey import cash.z.ecc.android.sdk.model.UnifiedSpendingKey import cash.z.ecc.android.sdk.model.ZcashNetwork @@ -45,28 +44,28 @@ interface DerivationTool { * the returned spending key in a secure fashion. * * @param seed the seed from which to derive spending keys. - * @param account the account to derive. + * @param accountIndex the ZIP 32 account index to derive. * * @return the unified spending key for the account. */ suspend fun deriveUnifiedSpendingKey( seed: ByteArray, network: ZcashNetwork, - account: Account + accountIndex: Int ): UnifiedSpendingKey /** * Given a seed and account index, return the associated Unified Address. * * @param seed the seed from which to derive the address. - * @param account the index of the account to use for deriving the address. + * @param accountIndex the ZIP 32 account index to use for deriving the address. * * @return the address that corresponds to the seed and account index. */ suspend fun deriveUnifiedAddress( seed: ByteArray, network: ZcashNetwork, - account: Account + accountIndex: Int ): String /** @@ -108,13 +107,15 @@ interface DerivationTool { * * @param contextString a globally-unique non-empty sequence of at most 252 bytes that * identifies the desired context. + * @param seed the seed from which to derive the arbitrary key. + * @param accountIndex the ZIP 32 account index for which to derive the arbitrary key. * @return an array of 32 bytes. */ suspend fun deriveArbitraryAccountKey( contextString: ByteArray, seed: ByteArray, network: ZcashNetwork, - account: Account + accountIndex: Int ): ByteArray companion object {