diff --git a/CHANGELOG.md b/CHANGELOG.md index bb2f3d433..054924b40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,18 +7,30 @@ and this library adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added -- `Synchronizer.getAccounts()` -- `Synchronizer.walletBalances: StateFlow?>` that is replacement for the removed +- `Synchronizer.importAccountByUfvk()` has been added +- `Synchronizer.getAccounts()` returning all the created or imported accounts. See the documentation in `Account`. +- `Synchronizer.walletBalances: StateFlow?>` that is replacement for the removed `orchardBalances`, `saplingBalances`, and `transparentBalance` +- `getTransactions(accountUuid: AccountUuid)` to get transactions belonging to the given account +- `Zip32AccountIndex`, `AccountUuid`, `AccountUsk`, `AccountPurpose`, `AccountCreateSetup`, and `AcountImportSetup` + model classes have been added to support the new or the changed APIs -### Changed -- `Synchronizer.orchardBalances`, `Synchronizer.saplingBalances`, and `Synchronizer.transparentBalance` have been - replaced by `Synchronizer.walletBalances` that provides these balances based on `Account` +### Changed +- `Account` data class works with `accountUuid: AccountUuid` instead of the previous ZIP 32 account index +- These functions from `DerivationTool` have been refactored to work with the new `Zip32AccountIndex` instead of the + `Account` data class: `deriveUnifiedSpendingKey`, `deriveUnifiedAddress`, `deriveArbitraryAccountKey` +- `WalletCoordinator` now provides a way to instantiate `Synchronizer` with the new `accountName` and `keySource` + parameters +- `UnifiedSpendingKey` does not hold `Account` information anymore, it has been replaced by `AccountUsk` model class + in a few internal cases +- `Synchronizer.send` extension function receives `Account` on input +- `PendingTransaction` sealed class descendants have been renamed ### Removed - `Synchronizer.sendToAddress` and `Synchronizer.shieldFunds` have been removed, use `Synchronizer.createProposedTransactions` and `Synchronizer.proposeShielding` instead - `Synchronizer.orchardBalances`, `Synchronizer.saplingBalances`, and `Synchronizer.transparentBalance` + (use `Synchronizer.walletBalances` instead). ### Fixed - The `CompactBlockProcessor` now correctly distinguishes between `Response.Failure.Server.Unavailable` and other diff --git a/backend-lib/Cargo.lock b/backend-lib/Cargo.lock index ddd59ba7c..a0e977306 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=c2ebc05be118a972352801a328e6c61f69bb8a16#c2ebc05be118a972352801a328e6c61f69bb8a16" 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=c2ebc05be118a972352801a328e6c61f69bb8a16#c2ebc05be118a972352801a328e6c61f69bb8a16" 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=c2ebc05be118a972352801a328e6c61f69bb8a16#c2ebc05be118a972352801a328e6c61f69bb8a16" 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=c2ebc05be118a972352801a328e6c61f69bb8a16#c2ebc05be118a972352801a328e6c61f69bb8a16" 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=c2ebc05be118a972352801a328e6c61f69bb8a16#c2ebc05be118a972352801a328e6c61f69bb8a16" 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=c2ebc05be118a972352801a328e6c61f69bb8a16#c2ebc05be118a972352801a328e6c61f69bb8a16" 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=c2ebc05be118a972352801a328e6c61f69bb8a16#c2ebc05be118a972352801a328e6c61f69bb8a16" 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=c2ebc05be118a972352801a328e6c61f69bb8a16#c2ebc05be118a972352801a328e6c61f69bb8a16" 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=c2ebc05be118a972352801a328e6c61f69bb8a16#c2ebc05be118a972352801a328e6c61f69bb8a16" 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=c2ebc05be118a972352801a328e6c61f69bb8a16#c2ebc05be118a972352801a328e6c61f69bb8a16" 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=c2ebc05be118a972352801a328e6c61f69bb8a16#c2ebc05be118a972352801a328e6c61f69bb8a16" dependencies = [ "base64", "nom", diff --git a/backend-lib/Cargo.toml b/backend-lib/Cargo.toml index f67c3e396..cb64523fc 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 = "c2ebc05be118a972352801a328e6c61f69bb8a16" } +zcash_client_backend = { git = "https://github.com/zcash/librustzcash", rev = "c2ebc05be118a972352801a328e6c61f69bb8a16" } +zcash_client_sqlite = { git = "https://github.com/zcash/librustzcash", rev = "c2ebc05be118a972352801a328e6c61f69bb8a16" } +zcash_primitives = { git = "https://github.com/zcash/librustzcash", rev = "c2ebc05be118a972352801a328e6c61f69bb8a16" } +zcash_proofs = { git = "https://github.com/zcash/librustzcash", rev = "c2ebc05be118a972352801a328e6c61f69bb8a16" } 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..e30ad36e3 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 @@ -1,13 +1,13 @@ package cash.z.ecc.android.sdk.internal import cash.z.ecc.android.sdk.internal.model.JniAccount +import cash.z.ecc.android.sdk.internal.model.JniAccountUsk import cash.z.ecc.android.sdk.internal.model.JniBlockMeta import cash.z.ecc.android.sdk.internal.model.JniRewindResult import cash.z.ecc.android.sdk.internal.model.JniScanRange import cash.z.ecc.android.sdk.internal.model.JniScanSummary import cash.z.ecc.android.sdk.internal.model.JniSubtreeRoot import cash.z.ecc.android.sdk.internal.model.JniTransactionDataRequest -import cash.z.ecc.android.sdk.internal.model.JniUnifiedSpendingKey import cash.z.ecc.android.sdk.internal.model.JniWalletSummary import cash.z.ecc.android.sdk.internal.model.ProposalUnsafe @@ -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 @@ -85,10 +85,28 @@ interface Backend { */ @Throws(RuntimeException::class) suspend fun createAccount( + accountName: String, + keySource: String?, seed: ByteArray, treeState: ByteArray, - recoverUntil: Long? - ): JniUnifiedSpendingKey + recoverUntil: Long?, + ): JniAccountUsk + + /** + * @throws RuntimeException as a common indicator of the operation failure + */ + @Throws(RuntimeException::class) + @Suppress("LongParameterList") + suspend fun importAccountUfvk( + accountName: String, + keySource: String?, + ufvk: String, + treeState: ByteArray, + recoverUntil: Long?, + purpose: Int, + seedFingerprint: ByteArray?, + zip32AccountIndex: Long?, + ): JniAccount /** * @throws RuntimeException as a common indicator of the operation failure @@ -109,13 +127,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/Derivation.kt b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/Derivation.kt index 229649e1f..fa03d4957 100644 --- a/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/Derivation.kt +++ b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/Derivation.kt @@ -11,14 +11,14 @@ interface Derivation { fun deriveUnifiedAddress( seed: ByteArray, networkId: Int, - accountIndex: Int + accountIndex: Long ): String fun deriveUnifiedSpendingKey( seed: ByteArray, networkId: Int, - accountIndex: Int - ): JniUnifiedSpendingKey + accountIndex: Long + ): ByteArray /** * @return a unified full viewing key. @@ -66,7 +66,7 @@ interface Derivation { contextString: ByteArray, seed: ByteArray, networkId: Int, - accountIndex: Int + accountIndex: Long ): ByteArray companion object { diff --git a/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/jni/JniConstants.kt b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/jni/JniConstants.kt new file mode 100644 index 000000000..ff8cea065 --- /dev/null +++ b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/jni/JniConstants.kt @@ -0,0 +1,14 @@ +package cash.z.ecc.android.sdk.internal.jni + +import cash.z.ecc.android.sdk.internal.model.JniAccount + +/** + * The number of bytes in the account UUID parameter. It's used e.g. in [JniAccount.accountUuid], or + * [JniUnifiedSpendingKey.accountUuid] + */ +const val JNI_ACCOUNT_UUID_BYTES_SIZE = 16 + +/** + * The number of bytes in the seed fingerprint parameter. It's used e.g. in [JniAccount.seedFingerprint] + */ +const val JNI_ACCOUNT_SEED_FP_BYTES_SIZE = 32 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..491a95967 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 @@ -5,13 +5,13 @@ import cash.z.ecc.android.sdk.internal.SdkDispatchers import cash.z.ecc.android.sdk.internal.ext.deleteRecursivelySuspend import cash.z.ecc.android.sdk.internal.ext.deleteSuspend import cash.z.ecc.android.sdk.internal.model.JniAccount +import cash.z.ecc.android.sdk.internal.model.JniAccountUsk import cash.z.ecc.android.sdk.internal.model.JniBlockMeta import cash.z.ecc.android.sdk.internal.model.JniRewindResult import cash.z.ecc.android.sdk.internal.model.JniScanRange import cash.z.ecc.android.sdk.internal.model.JniScanSummary import cash.z.ecc.android.sdk.internal.model.JniSubtreeRoot import cash.z.ecc.android.sdk.internal.model.JniTransactionDataRequest -import cash.z.ecc.android.sdk.internal.model.JniUnifiedSpendingKey import cash.z.ecc.android.sdk.internal.model.JniWalletSummary import cash.z.ecc.android.sdk.internal.model.ProposalUnsafe import cash.z.ecc.android.sdk.internal.model.RustLogging @@ -90,17 +90,47 @@ class RustBackend private constructor( } override suspend fun createAccount( + accountName: String, + keySource: String?, seed: ByteArray, treeState: ByteArray, - recoverUntil: Long? - ): JniUnifiedSpendingKey { + recoverUntil: Long?, + ): JniAccountUsk { return withContext(SdkDispatchers.DATABASE_IO) { createAccount( - dataDbFile.absolutePath, - seed, - treeState, - recoverUntil ?: -1, - networkId = networkId + dbDataPath = dataDbFile.absolutePath, + networkId = networkId, + accountName = accountName, + keySource = keySource, + seed = seed, + treeState = treeState, + recoverUntil = recoverUntil ?: -1, + ) + } + } + + override suspend fun importAccountUfvk( + accountName: String, + keySource: String?, + ufvk: String, + treeState: ByteArray, + recoverUntil: Long?, + purpose: Int, + seedFingerprint: ByteArray?, + zip32AccountIndex: Long?, + ): JniAccount { + return withContext(SdkDispatchers.DATABASE_IO) { + importAccountUfvk( + dbDataPath = dataDbFile.absolutePath, + networkId = networkId, + accountName = accountName, + keySource = keySource, + ufvk = ufvk, + treeState = treeState, + recoverUntil = recoverUntil ?: -1, + purpose = purpose, + seedFingerprint = seedFingerprint, + zip32AccountIndex = zip32AccountIndex, ) } } @@ -114,11 +144,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 +157,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 +348,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 +363,7 @@ class RustBackend private constructor( } override suspend fun proposeTransfer( - accountIndex: Int, + accountUuid: ByteArray, to: String, value: Long, memo: ByteArray? @@ -342,7 +372,7 @@ class RustBackend private constructor( ProposalUnsafe.parse( proposeTransfer( dataDbFile.absolutePath, - accountIndex, + accountUuid, to, value, memo, @@ -352,7 +382,7 @@ class RustBackend private constructor( } override suspend fun proposeShielding( - accountIndex: Int, + accountUuid: ByteArray, shieldingThreshold: Long, memo: ByteArray?, transparentReceiver: String? @@ -360,7 +390,7 @@ class RustBackend private constructor( return withContext(SdkDispatchers.DATABASE_IO) { proposeShielding( dataDbFile.absolutePath, - accountIndex, + accountUuid, shieldingThreshold, memo, transparentReceiver, @@ -492,13 +522,31 @@ class RustBackend private constructor( ): Array @JvmStatic + @Suppress("LongParameterList") private external fun createAccount( dbDataPath: String, + networkId: Int, + accountName: String, + keySource: String?, seed: ByteArray, treeState: ByteArray, recoverUntil: Long, - networkId: Int - ): JniUnifiedSpendingKey + ): JniAccountUsk + + @JvmStatic + @Suppress("LongParameterList") + private external fun importAccountUfvk( + dbDataPath: String, + networkId: Int, + accountName: String, + keySource: String?, + ufvk: String, + treeState: ByteArray, + recoverUntil: Long, + purpose: Int, + seedFingerprint: ByteArray?, + zip32AccountIndex: Long?, + ): JniAccount @JvmStatic private external fun isSeedRelevantToAnyDerivedAccounts( @@ -510,7 +558,7 @@ class RustBackend private constructor( @JvmStatic private external fun getCurrentAddress( dbDataPath: String, - accountIndex: Int, + accountUuid: ByteArray, networkId: Int ): String @@ -523,7 +571,7 @@ class RustBackend private constructor( @JvmStatic private external fun listTransparentReceivers( dbDataPath: String, - accountIndex: Int, + accountUuid: ByteArray, networkId: Int ): Array @@ -671,7 +719,7 @@ class RustBackend private constructor( @JvmStatic private external fun proposeTransferFromUri( dbDataPath: String, - accountIndex: Int, + accountUuid: ByteArray, uri: String, networkId: Int, ): ByteArray @@ -680,7 +728,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 +739,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/jni/RustDerivationTool.kt b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/jni/RustDerivationTool.kt index e276387e4..d81619827 100644 --- a/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/jni/RustDerivationTool.kt +++ b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/jni/RustDerivationTool.kt @@ -18,13 +18,13 @@ class RustDerivationTool private constructor() : Derivation { override fun deriveUnifiedSpendingKey( seed: ByteArray, networkId: Int, - accountIndex: Int - ): JniUnifiedSpendingKey = deriveSpendingKey(seed, accountIndex, networkId = networkId) + accountIndex: Long + ): ByteArray = deriveSpendingKey(seed, accountIndex, networkId = networkId) override fun deriveUnifiedAddress( seed: ByteArray, networkId: Int, - accountIndex: Int + accountIndex: Long ): String = deriveUnifiedAddressFromSeed(seed, accountIndex = accountIndex, networkId = networkId) /** @@ -49,7 +49,7 @@ class RustDerivationTool private constructor() : Derivation { contextString: ByteArray, seed: ByteArray, networkId: Int, - accountIndex: Int + accountIndex: Long ): ByteArray = deriveArbitraryAccountKeyFromSeed( contextString = contextString, @@ -68,9 +68,9 @@ class RustDerivationTool private constructor() : Derivation { @JvmStatic private external fun deriveSpendingKey( seed: ByteArray, - accountIndex: Int, + accountIndex: Long, networkId: Int - ): JniUnifiedSpendingKey + ): ByteArray @JvmStatic private external fun deriveUnifiedFullViewingKeysFromSeed( @@ -88,7 +88,7 @@ class RustDerivationTool private constructor() : Derivation { @JvmStatic private external fun deriveUnifiedAddressFromSeed( seed: ByteArray, - accountIndex: Int, + accountIndex: Long, networkId: Int ): String @@ -108,7 +108,7 @@ class RustDerivationTool private constructor() : Derivation { private external fun deriveArbitraryAccountKeyFromSeed( contextString: ByteArray, seed: ByteArray, - accountIndex: Int, + accountIndex: Long, networkId: Int ): ByteArray } 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 545228ca0..e50bab4a5 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 @@ -1,22 +1,40 @@ package cash.z.ecc.android.sdk.internal.model import androidx.annotation.Keep +import cash.z.ecc.android.sdk.internal.jni.JNI_ACCOUNT_SEED_FP_BYTES_SIZE +import cash.z.ecc.android.sdk.internal.jni.JNI_ACCOUNT_UUID_BYTES_SIZE /** * 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. + * @param accountName A human-readable name for the account + * @param keySource A string identifier or other metadata describing the source of the seed + * @param seedFingerprint The seed fingerprint + * @param hdAccountIndex ZIP 32 account index + * * @throws IllegalArgumentException if the values are inconsistent. */ @Keep class JniAccount( - val accountIndex: Int, + val accountName: String?, + val accountUuid: ByteArray, + // We use -1L to represent NULL across JNI + val hdAccountIndex: Long, + val keySource: String?, + val seedFingerprint: ByteArray?, val ufvk: String?, ) { init { - require(accountIndex >= 0) { - "Account index must be non-negative" + require(accountUuid.size == JNI_ACCOUNT_UUID_BYTES_SIZE) { + "Account UUID must be 16 bytes" + } + + seedFingerprint?.let { + require(seedFingerprint.size == JNI_ACCOUNT_SEED_FP_BYTES_SIZE) { + "Seed fingerprint must be 32 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..9b644e9b1 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 @@ -1,11 +1,12 @@ package cash.z.ecc.android.sdk.internal.model import androidx.annotation.Keep +import cash.z.ecc.android.sdk.internal.jni.JNI_ACCOUNT_UUID_BYTES_SIZE /** * Serves as cross layer (Kotlin, Rust) communication class. * - * @param account the account ID + * @param accountUuid the account UUID in ByteArray * @param saplingVerifiedBalance The verified account balance in the Sapling pool. * @param saplingChangePending The value in the account of Sapling change notes that do * not yet have sufficient confirmations to be spendable. @@ -25,7 +26,7 @@ import androidx.annotation.Keep @Keep @Suppress("LongParameterList") class JniAccountBalance( - val account: Int, + val accountUuid: ByteArray, val saplingVerifiedBalance: Long, val saplingChangePending: Long, val saplingValuePending: Long, @@ -35,6 +36,9 @@ class JniAccountBalance( val unshieldedBalance: Long, ) { init { + require(accountUuid.size == JNI_ACCOUNT_UUID_BYTES_SIZE) { + "Account UUID must be 16 bytes" + } require(saplingVerifiedBalance >= MIN_INCLUSIVE) { "Sapling verified balance $saplingVerifiedBalance must by equal or above $MIN_INCLUSIVE" } diff --git a/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/JniAccountUsk.kt b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/JniAccountUsk.kt new file mode 100644 index 000000000..08ff0d52a --- /dev/null +++ b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/JniAccountUsk.kt @@ -0,0 +1,41 @@ +package cash.z.ecc.android.sdk.internal.model + +import androidx.annotation.Keep +import cash.z.ecc.android.sdk.internal.jni.JNI_ACCOUNT_UUID_BYTES_SIZE + +/** + * Serves as cross layer (Kotlin, Rust) communication class. It contains account identifier together with + * a [ZIP 316](https://zips.z.cash/zip-0316) Unified Spending Key. + * + * This is the spend authority for an account under the wallet's seed. + * + * An instance of this class contains all of the per-pool spending keys that could be + * derived at the time of its creation. As such, it is not suitable for long-term storage, + * export/import, or backup purposes. + */ +@Keep +class JniAccountUsk( + /** + * The "one-way stable" identifier for the account tracked in the wallet to which this + * spending key belongs. + */ + val accountUuid: ByteArray, + /** + * The binary encoding of the [ZIP 316](https://zips.z.cash/zip-0316) Unified Spending + * 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 + * backend. Wallets **MUST NOT** allow this encoding to be exported or imported. + */ + val bytes: ByteArray +) { + init { + require(accountUuid.size == JNI_ACCOUNT_UUID_BYTES_SIZE) { + "Account UUID must be 16 bytes" + } + } + + // Override to prevent leaking key to logs + override fun toString() = "JniAccountUsk(account=$accountUuid, bytes=***)" +} 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..17fa87b6c 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 @@ -13,13 +13,9 @@ import androidx.annotation.Keep */ @Keep class JniUnifiedSpendingKey( - /** - * The [ZIP 32](https://zips.z.cash/zip-0032) account index used to derive this key. - */ - val account: Int, /** * The binary encoding of the [ZIP 316](https://zips.z.cash/zip-0316) Unified Spending - * Key for [account]. + * Key for the selected account. * * 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 @@ -27,24 +23,12 @@ class JniUnifiedSpendingKey( */ val bytes: ByteArray ) { - // Override to prevent leaking key to logs - override fun toString() = "JniUnifiedSpendingKey(account=$account, bytes=***)" - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as JniUnifiedSpendingKey - - if (account != other.account) return false - if (!bytes.contentEquals(other.bytes)) return false - - return true + init { + require(bytes.isNotEmpty()) { + "Unified Spending Key must not be empty" + } } - override fun hashCode(): Int { - var result = account.hashCode() - result = 31 * result + bytes.hashCode() - return result - } + // Override to prevent leaking key to logs + override fun toString() = "JniUnifiedSpendingKey(bytes=***)" } diff --git a/backend-lib/src/main/rust/lib.rs b/backend-lib/src/main/rust/lib.rs index 1c0b77b8c..b015ced61 100644 --- a/backend-lib/src/main/rust/lib.rs +++ b/backend-lib/src/main/rust/lib.rs @@ -18,8 +18,12 @@ use tor_rtcompat::BlockOn; use tracing::{debug, error}; use tracing_subscriber::prelude::*; use tracing_subscriber::reload; +use utils::{java_nullable_string_to_rust, java_string_to_rust}; +use uuid::Uuid; use zcash_address::{ToAddress, ZcashAddress}; -use zcash_client_backend::data_api::{TransactionDataRequest, TransactionStatus}; +use zcash_client_backend::data_api::{ + AccountPurpose, BirthdayError, TransactionDataRequest, TransactionStatus, Zip32Derivation, +}; use zcash_client_backend::fees::zip317::MultiOutputChangeStrategy; use zcash_client_backend::fees::{SplitPolicy, StandardFeeRule}; use zcash_client_backend::{ @@ -31,7 +35,7 @@ use zcash_client_backend::{ create_proposed_transactions, decrypt_and_store_transaction, input_selection::GreedyInputSelector, propose_shielding, propose_transfer, }, - Account, AccountBalance, AccountBirthday, AccountSource, InputSource, SeedRelevance, + Account, AccountBalance, AccountBirthday, InputSource, SeedRelevance, WalletCommitmentTrees, WalletRead, WalletSummary, WalletWrite, }, encoding::AddressCodec, @@ -44,10 +48,11 @@ 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}, - AccountId, FsBlockDb, WalletDb, + FsBlockDb, WalletDb, }; use zcash_primitives::consensus::NetworkConstants; use zcash_primitives::{ @@ -67,6 +72,7 @@ use zcash_primitives::{ zip32::{self, DiversifierIndex}, }; use zcash_proofs::prover::LocalTxProver; +use zip32::fingerprint::SeedFingerprint; use zip32::ChildIndex; use crate::utils::{catch_unwind, exception::unwrap_exc_or}; @@ -112,45 +118,10 @@ fn zip32_account_index_from_jint(account_index: jint) -> anyhow::Result( - db_data: &WalletDb, - account_index: jint, -) -> 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() - }); - - 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")), - } +fn account_id_from_jni(env: &JNIEnv, account_uuid: JByteArray) -> anyhow::Result { + Ok(AccountUuid::from_uuid(Uuid::from_slice( + &env.convert_byte_array(account_uuid)?, + )?)) } /// Initializes global Rust state, such as the logging infrastructure and threadpools. @@ -297,23 +268,42 @@ 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(), }; + let account_name = match account.name() { + Some(name) => env.new_string(name)?.into(), + None => JObject::null(), + }; + + let key_source = match account.source().key_source() { + Some(source) => env.new_string(source)?.into(), + None => JObject::null(), + }; + + let seed_fingerprint = match account.source().key_derivation() { + Some(d) => env.byte_array_from_slice(&d.seed_fingerprint().to_bytes()[..])?.into(), + None => JObject::null(), + }; + + let hd_account_index = match account.source().key_derivation() { + Some(d) => JValue::Long(i64::from(u32::from(d.account_index()))), + // Use -1 to return null across the FFI. + None => JValue::Long(-1), + }; + env.new_object( JNI_ACCOUNT, - "(ILjava/lang/String;)V", + "(Ljava/lang/String;[BJLjava/lang/String;[BLjava/lang/String;)V", &[ - // TODO: This will be replaced by the multi-seed-compatible account ID. - JValue::Int(u32::from(account_index) as i32), - (&ufvk).into(), + (&account_name).into(), + (&env.byte_array_from_slice(account.id().expose_uuid().as_bytes())?).into(), + hd_account_index, + (&key_source).into(), + (&seed_fingerprint).into(), + (&ufvk).into() ], ) } @@ -341,13 +331,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::>(); - Ok( utils::rust_vec_to_java(env, accounts, JNI_ACCOUNT, |env, account| { encode_account(env, &network, account) @@ -360,16 +343,16 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_getAccoun fn encode_usk<'a>( env: &mut JNIEnv<'a>, - account_index: zip32::AccountId, + account_uuid: AccountUuid, usk: UnifiedSpendingKey, ) -> jni::errors::Result> { let encoded = SecretVec::new(usk.to_bytes(Era::Orchard)); let bytes = env.byte_array_from_slice(encoded.expose_secret())?; env.new_object( - "cash/z/ecc/android/sdk/internal/model/JniUnifiedSpendingKey", - "(I[B)V", + "cash/z/ecc/android/sdk/internal/model/JniAccountUsk", + "([B[B)V", &[ - JValue::Int(u32::from(account_index) as i32), + (&env.byte_array_from_slice(account_uuid.expose_uuid().as_bytes())?).into(), (&bytes).into(), ], ) @@ -413,13 +396,13 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_createAcc mut env: JNIEnv<'local>, _: JClass<'local>, db_data: JString<'local>, + network_id: jint, + account_name: JString<'local>, + key_source: JString<'local>, seed: JByteArray<'local>, treestate: JByteArray<'local>, recover_until: jlong, - network_id: jint, ) -> jobject { - use zcash_client_backend::data_api::BirthdayError; - let res = catch_unwind(&mut env, |env| { let network = parse_network(network_id as u32)?; let mut db_data = wallet_db(env, network, db_data)?; @@ -438,17 +421,132 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_createAcc } })?; - let (account_id, usk) = db_data - .create_account(&seed, &birthday) + let account_name = java_string_to_rust(env, &account_name); + let key_source = java_nullable_string_to_rust(env, &key_source); + + let (account_uuid, usk) = db_data + .create_account( + &account_name, + &seed, + &birthday, + key_source.as_ref().map(|s| s.as_ref()), + ) .map_err(|e| anyhow!("Error while initializing accounts: {}", e))?; - let account = db_data.get_account(account_id)?.expect("just created"); - let account_index = match account.source() { - AccountSource::Derived { account_index, .. } => account_index, - AccountSource::Imported { .. } => unreachable!("just created"), - }; + Ok(encode_usk(env, account_uuid, usk)?.into_raw()) + }); + unwrap_exc_or(&mut env, res, ptr::null_mut()) +} + +/// Tells the wallet to track an account using a unified full viewing key. +/// +/// Returns details about the imported account, including the unique account identifier for +/// the newly-created wallet database entry. Unlike the other account creation APIs +/// ([`Self::create_account`] and [`Self::import_account_hd`]), no spending key is returned +/// because the wallet has no information about how the UFVK was derived. +/// +/// Certain optimizations are possible for accounts which will never be used to spend funds. +/// If `purpose` is 1 (ViewOnly), the wallet may choose to optimize for this case, in which +/// case any attempt to spend funds from the account will result in an error. +/// +/// The [`WalletWrite`] trait documentation has more details about account creation and import. +/// +/// # Arguments +/// - `account_name`: A human-readable name for the account. +/// - `key_source`: A string identifier or other metadata describing the source of the seed. +/// This is treated as opaque metadata by the wallet backend; it is provided for use by +/// applications which need to track additional identifying information for an account. +/// - `ufvk_str`: The UFVK used to detect transactions involving the account. +/// - `treestate`: The tree state corresponding to the last block prior to the wallet's +/// birthday height. +/// - `recover_until`: An optional height at which the wallet should exit "recovery mode". In +/// order to avoid confusing shifts in wallet balance and spendability that may temporarily be +/// visible to a user during the process of recovering from seed, wallets may optionally set a +/// "recover until" height. The wallet is considered to be in "recovery mode" until there +/// exist no unscanned ranges between the wallet's birthday height and the provided +/// `recover_until` height, exclusive. +/// - `purpose`: 0 (Spending) if data required for spending should be tracked by the wallet, +/// or 1 (ViewOnly) if the account will never be used to spend funds. +/// - `seed_fingerprint_bytes`: the [seed fingerprint]. Only used if `purpose` is 0 (Spending). +/// - `hd_account_index_raw`: the ZIP 32 account index. Only used if `purpose` is 0 (Spending). +/// +/// [seed fingerprint]: https://zips.z.cash/zip-0032#seed-fingerprints +/// +/// # Panics +/// +/// Panics if the length of the seed is not between 32 and 252 bytes inclusive. +#[unsafe(no_mangle)] +pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_importAccountUfvk<'local>( + mut env: JNIEnv<'local>, + _: JClass<'local>, + db_data: JString<'local>, + network_id: jint, + account_name: JString<'local>, + key_source: JString<'local>, + ufvk_str: JString<'local>, + treestate: JByteArray<'local>, + recover_until: jlong, + purpose: jint, + seed_fingerprint_bytes: JByteArray<'local>, + hd_account_index_raw: u32, +) -> jobject { + let res = catch_unwind(&mut env, |env| { + let network = parse_network(network_id as u32)?; + let mut db_data = wallet_db(env, network, db_data)?; + let ufvk_str = java_string_to_rust(env, &ufvk_str); + let ufvk = UnifiedFullViewingKey::decode(&network, &ufvk_str).map_err(|e| { + anyhow!( + "Value \"{}\" did not decode as a valid UFVK: {}", + ufvk_str, + e + ) + })?; + let treestate = TreeState::decode(&env.convert_byte_array(treestate).unwrap()[..]) + .map_err(|e| anyhow!("Invalid TreeState: {}", e))?; + let recover_until = recover_until.try_into().ok(); + + let birthday = + AccountBirthday::from_treestate(treestate, recover_until).map_err(|e| match e { + BirthdayError::HeightInvalid(e) => { + anyhow!("Invalid TreeState: Invalid height: {}", e) + } + BirthdayError::Decode(e) => { + anyhow!("Invalid TreeState: Invalid frontier encoding: {}", e) + } + })?; + + let account_name = java_string_to_rust(env, &account_name); + let key_source = java_nullable_string_to_rust(env, &key_source); + + let seed_fingerprint = + <[u8; 32]>::try_from(&env.convert_byte_array(seed_fingerprint_bytes)?[..]) + .ok() + .map(SeedFingerprint::from_bytes); + let hd_account_index = zip32::AccountId::try_from(hd_account_index_raw).ok(); + + let derivation = seed_fingerprint + .zip(hd_account_index) + .map(|(seed_fp, idx)| Zip32Derivation::new(seed_fp, idx)); + + let purpose = match purpose { + 0 => Ok(AccountPurpose::Spending { derivation }), + 1 => Ok(AccountPurpose::ViewOnly), + _ => Err(anyhow!( + "Account purpose must be either 0 (Spending) or 1 (ViewOnly)" + )), + }?; + + let account = db_data + .import_account_ufvk( + &account_name, + &ufvk, + &birthday, + purpose, + key_source.as_ref().map(|s| s.as_ref()), + ) + .map_err(|e| anyhow!("Error while initializing accounts: {}", e))?; - Ok(encode_usk(env, account_index, usk)?.into_raw()) + Ok(encode_account(env, &network, account)?.into_raw()) }); unwrap_exc_or(&mut env, res, ptr::null_mut()) } @@ -483,16 +581,16 @@ 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_uuid = account_id_from_jni(&env, account_uuid)?; - match db_data.get_current_address(account) { + match db_data.get_current_address(account_uuid) { Ok(Some(addr)) => { let addr_str = addr.encode(&network); let output = env @@ -500,7 +598,7 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_getCurren .expect("Couldn't create Java string!"); Ok(output.into_raw()) } - Ok(None) => Err(anyhow!("{:?} is not known to the wallet", account)), + Ok(None) => Err(anyhow!("{:?} is not known to the wallet", account_uuid)), Err(e) => Err(anyhow!("Error while fetching address: {}", e)), } }); @@ -1172,7 +1270,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()); @@ -1191,9 +1289,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()), @@ -1211,32 +1309,17 @@ fn encode_account_balance<'a>( /// /// If these conditions are not met, this fails and leaves an `IllegalArgumentException` /// pending. -fn encode_wallet_summary<'a, P: Parameters>( +fn encode_wallet_summary<'a>( env: &mut JNIEnv<'a>, - db_data: &WalletDb, - summary: WalletSummary, + summary: WalletSummary, ) -> anyhow::Result> { + let account_balances = summary.account_balances().iter().collect::>(); + 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, (account_uuid, balance)| encode_account_balance(env, &account_uuid, balance), )?; let (progress_numerator, progress_denominator) = @@ -1284,7 +1367,7 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_getWallet .map_err(|e| anyhow!("Error while fetching scan progress: {}", e))? .filter(|summary| summary.progress().scan().denominator() > &0) { - Some(summary) => Ok(encode_wallet_summary(env, &db_data, summary)?.into_raw()), + Some(summary) => Ok(encode_wallet_summary(env, summary)?.into_raw()), None => Ok(ptr::null_mut()), } }); @@ -1613,7 +1696,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 { @@ -1621,7 +1704,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_uuid = account_id_from_jni(&env, account_uuid)?; let payment_uri = utils::java_string_to_rust(env, &payment_uri); // Always use ZIP 317 fees @@ -1633,7 +1716,7 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_proposeTr let proposal = propose_transfer::<_, _, _, _, Infallible>( &mut db_data, &network, - account, + account_uuid, &input_selector, &change_strategy, request, @@ -1657,7 +1740,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>, @@ -1667,7 +1750,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_uuid = account_id_from_jni(&env, 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"))?; @@ -1697,7 +1780,7 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_proposeTr let proposal = propose_transfer::<_, _, _, _, Infallible>( &mut db_data, &network, - account, + account_uuid, &input_selector, &change_strategy, request, @@ -1721,7 +1804,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>, @@ -1731,7 +1814,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_uuid = account_id_from_jni(&env, account_uuid)?; let shielding_threshold = NonNegativeAmount::from_nonnegative_i64(shielding_threshold) .map_err(|_| anyhow!("Invalid shielding threshold, out of range"))?; @@ -1746,7 +1829,7 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_proposeSh } Address::Transparent(addr) => { if db_data - .get_transparent_receivers(account)? + .get_transparent_receivers(account_uuid)? .contains_key(&addr) { Ok(Some(addr)) @@ -1771,11 +1854,11 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_proposeSh }) .and_then(|anchor| { db_data - .get_transparent_balances(account, anchor) + .get_transparent_balances(account_uuid, anchor) .map_err(|e| { anyhow!( "Error while fetching transparent balances for {:?}: {}", - account, + account_uuid, e ) }) @@ -1815,7 +1898,7 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_proposeSh &change_strategy, shielding_threshold, &from_addrs, - account, + account_uuid, min_confirmations, ) .map_err(|e| anyhow!("Error while shielding transaction: {}", e))?; @@ -1918,7 +2001,7 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustDerivationTool_de seed: JByteArray<'local>, account_index: jint, network_id: jint, -) -> jobject { +) -> jbyteArray { let res = catch_unwind(&mut env, |env| { let _span = tracing::info_span!("RustDerivationTool.deriveSpendingKey").entered(); let network = parse_network(network_id as u32)?; @@ -1928,7 +2011,8 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustDerivationTool_de let usk = UnifiedSpendingKey::from_seed(&network, seed.expose_secret(), account) .map_err(|e| anyhow!("error generating unified spending key from seed: {:?}", e))?; - Ok(encode_usk(env, account, usk)?.into_raw()) + let encoded = SecretVec::new(usk.to_bytes(Era::Orchard)); + Ok(utils::rust_bytes_to_java(&env, encoded.expose_secret())?.into_raw()) }); unwrap_exc_or(&mut env, res, ptr::null_mut()) } @@ -2236,7 +2320,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| { @@ -2244,7 +2328,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, account_uuid)?; match db_data.get_transparent_receivers(account) { Ok(receivers) => { diff --git a/backend-lib/src/test/java/cash/z/ecc/android/sdk/internal/fixture/JniAccountBalanceFixture.kt b/backend-lib/src/test/java/cash/z/ecc/android/sdk/internal/fixture/JniAccountBalanceFixture.kt index 5dd9cde9c..ba1147fae 100644 --- a/backend-lib/src/test/java/cash/z/ecc/android/sdk/internal/fixture/JniAccountBalanceFixture.kt +++ b/backend-lib/src/test/java/cash/z/ecc/android/sdk/internal/fixture/JniAccountBalanceFixture.kt @@ -18,7 +18,7 @@ object JniAccountBalanceFixture { @Suppress("LongParameterList") fun new( - account: ByteArray = ACCOUNT_UUID, + accountUuid: ByteArray = ACCOUNT_UUID, saplingVerifiedBalance: Long = SAPLING_VERIFIED_BALANCE, saplingChangePending: Long = SAPLING_CHANGE_PENDING, saplingValuePending: Long = SAPLING_VALUE_PENDING, @@ -27,7 +27,7 @@ object JniAccountBalanceFixture { orchardValuePending: Long = ORCHARD_VALUE_PENDING, unshieldedBalance: Long = UNSHIELDED_BALANCE, ) = JniAccountBalance( - account = account, + accountUuid = accountUuid, saplingVerifiedBalance = saplingVerifiedBalance, saplingChangePending = saplingChangePending, saplingValuePending = saplingValuePending, diff --git a/darkside-test-lib/build.gradle.kts b/darkside-test-lib/build.gradle.kts index 1b59f3c12..cf498160e 100644 --- a/darkside-test-lib/build.gradle.kts +++ b/darkside-test-lib/build.gradle.kts @@ -25,6 +25,7 @@ dependencies { implementation(libs.androidx.multidex) implementation(libs.bundles.grpc) + androidTestImplementation(projects.sdkLib) androidTestImplementation(projects.sdkIncubatorLib) androidTestImplementation(libs.bundles.androidx.test) androidTestImplementation(libs.zcashwalletplgn) diff --git a/darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/test/DarksideTestCoordinator.kt b/darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/test/DarksideTestCoordinator.kt index 4fedab2a6..6f6bcb0de 100644 --- a/darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/test/DarksideTestCoordinator.kt +++ b/darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/test/DarksideTestCoordinator.kt @@ -189,7 +189,7 @@ class DarksideTestCoordinator(val wallet: TestWallet) { available: Long = -1, total: Long = -1 ) { - val balance = synchronizer.walletBalances.value?.get(wallet.account)?.sapling + val balance = synchronizer.walletBalances.value?.get(wallet.account.accountUuid)?.sapling if (available > 0) { assertTrue( "invalid available balance. Expected a minimum of $available but found ${balance?.available}", diff --git a/darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/test/TestWallet.kt b/darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/test/TestWallet.kt index 98a7355e6..d7ae8bfcf 100644 --- a/darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/test/TestWallet.kt +++ b/darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/test/TestWallet.kt @@ -7,6 +7,7 @@ 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.ext.Darkside +import cash.z.ecc.android.sdk.fixture.AccountCreateSetupFixture import cash.z.ecc.android.sdk.fixture.AccountFixture import cash.z.ecc.android.sdk.internal.Twig import cash.z.ecc.android.sdk.model.BlockHeight @@ -63,23 +64,23 @@ class TestWallet( DerivationTool.getInstance().deriveUnifiedSpendingKey( seed = seed, network = network, - account = AccountFixture.new() + accountIndex = AccountFixture.new().hdAccountIndex!! ) } val synchronizer: SdkSynchronizer = Synchronizer.newBlocking( - context, - network, - alias, - endpoint, - seed, - startHeight, + alias = alias, + birthday = startHeight, + context = context, + lightWalletEndpoint = endpoint, + setup = AccountCreateSetupFixture.new(), // Using existing wallet init mode as simplification for the test - walletInitMode = WalletInitMode.ExistingWallet + walletInitMode = WalletInitMode.ExistingWallet, + zcashNetwork = network, ) as SdkSynchronizer val available - get() = synchronizer.walletBalances.value?.get(account)?.sapling?.available + get() = synchronizer.walletBalances.value?.get(account.accountUuid)?.sapling?.available val unifiedAddress = runBlocking { synchronizer.getUnifiedAddress(account) } val transparentAddress = @@ -114,7 +115,7 @@ class TestWallet( ): TestWallet { synchronizer.createProposedTransactions( synchronizer.proposeTransfer( - shieldedSpendingKey.account, + account, address, amount, memo @@ -136,7 +137,7 @@ class TestWallet( synchronizer.getTransparentBalance(transparentAddress).let { walletBalance -> if (walletBalance.value > 0L) { - synchronizer.proposeShielding(shieldedSpendingKey.account, Zatoshi(100000))?.let { + synchronizer.proposeShielding(account, Zatoshi(100000))?.let { synchronizer.createProposedTransactions( it, shieldedSpendingKey diff --git a/demo-app/src/androidTest/java/cash/z/wallet/sdk/sample/demoapp/SampleCodeTest.kt b/demo-app/src/androidTest/java/cash/z/wallet/sdk/sample/demoapp/SampleCodeTest.kt index c8b433670..215b53a00 100644 --- a/demo-app/src/androidTest/java/cash/z/wallet/sdk/sample/demoapp/SampleCodeTest.kt +++ b/demo-app/src/androidTest/java/cash/z/wallet/sdk/sample/demoapp/SampleCodeTest.kt @@ -8,6 +8,7 @@ import cash.z.ecc.android.sdk.demoapp.ext.defaultForNetwork 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 +import cash.z.ecc.android.sdk.fixture.AccountCreateSetupFixture import cash.z.ecc.android.sdk.fixture.AccountFixture import cash.z.ecc.android.sdk.internal.Twig import cash.z.ecc.android.sdk.model.BlockHeight @@ -172,6 +173,7 @@ class SampleCodeTest { @Test fun submitTransaction(): Unit = runBlocking { + val account = AccountFixture.new() val amount = 0.123.convertZecToZatoshi() val address = "ztestsapling1tklsjr0wyw0d58f3p7wufvrj2cyfv6q6caumyueadq8qvqt8lda6v6tpx474rfru9y6u75u7qnw" val memo = "Test Transaction" @@ -179,11 +181,11 @@ class SampleCodeTest { DerivationTool.getInstance().deriveUnifiedSpendingKey( seed, ZcashNetwork.Mainnet, - AccountFixture.new() + account.hdAccountIndex!! ) synchronizer.createProposedTransactions( synchronizer.proposeTransfer( - spendingKey.account, + account, address, amount, memo @@ -199,19 +201,20 @@ class SampleCodeTest { companion object { private val seed = "Inserting seed for test purposes".toByteArray() private val lightwalletdHost = LightWalletEndpoint.Mainnet + private val setup = AccountCreateSetupFixture.new(seed = seed) private val context = InstrumentationRegistry.getInstrumentation().targetContext private val synchronizer: Synchronizer = run { val network = ZcashNetwork.fromResources(context) Synchronizer.newBlocking( - context, - network, - lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network), - seed = seed, birthday = null, + context = context, + lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network), + setup = setup, // Using existing wallet init mode as simplification for the test - walletInitMode = WalletInitMode.ExistingWallet + walletInitMode = WalletInitMode.ExistingWallet, + zcashNetwork = network, ) } diff --git a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/Constants.kt b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/Constants.kt index 61983fac5..295cb15fc 100644 --- a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/Constants.kt +++ b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/Constants.kt @@ -1,5 +1,6 @@ package cash.z.ecc.android.sdk.demoapp +import cash.z.ecc.android.sdk.model.AccountCreateSetup import kotlin.time.Duration.Companion.seconds // Recommended timeout for Android configuration changes to keep Kotlin Flow from restarting @@ -10,6 +11,14 @@ val ANDROID_STATE_FLOW_TIMEOUT = 5.seconds */ const val MINIMAL_WEIGHT = 0.0001f -// TODO [#1644]: Refactor Account ZIP32 index across SDK -// TODO [#1644]: https://github.com/Electric-Coin-Company/zcash-android-wallet-sdk/issues/1644 -const val CURRENT_ZIP_32_ACCOUNT_INDEX = 0 +/** + * Until we support full multi-account feature in Demo app we use this constant as a single source of truth for + * account selection + */ +const val CURRENT_ZIP_32_ACCOUNT_INDEX = 0L + +/** + * This value is used across Demo app as a [AccountCreateSetup.keySource] for the default account with index + * [CURRENT_ZIP_32_ACCOUNT_INDEX] + */ +const val DEMO_APP_ZCASH_ACCOUNT_KEY_SOURCE = "zcash" diff --git a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/Navigation.kt b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/Navigation.kt index 7127ac0ca..2840dc677 100644 --- a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/Navigation.kt +++ b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/Navigation.kt @@ -110,7 +110,10 @@ internal fun ComposeActivity.Navigation() { if (null == synchronizer || null == walletSnapshot) { // Display loading indicator } else { - val balance = walletSnapshot.balanceByAccount(walletViewModel.getCurrentAccount()) + val balance = + walletSnapshot.balanceByAccountUuid( + accountUuid = walletViewModel.getCurrentAccount().accountUuid + ) val scope = rememberCoroutineScope() Balance( exchangeRateUsd = walletSnapshot.exchangeRateUsd, @@ -171,7 +174,10 @@ internal fun ComposeActivity.Navigation() { } else { val currentAccount = walletViewModel.getCurrentAccount() Send( - accountBalance = walletSnapshot.balanceByAccount(currentAccount), + accountBalance = + walletSnapshot.balanceByAccountUuid( + accountUuid = currentAccount.accountUuid + ), sendState = walletViewModel.sendState.collectAsStateWithLifecycle().value, onSend = { walletViewModel.send(it) diff --git a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/SharedViewModel.kt b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/SharedViewModel.kt index c85971cd6..e09e2b20a 100644 --- a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/SharedViewModel.kt +++ b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/SharedViewModel.kt @@ -11,7 +11,9 @@ import cash.z.ecc.android.sdk.demoapp.ext.defaultForNetwork 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 +import cash.z.ecc.android.sdk.model.AccountCreateSetup import cash.z.ecc.android.sdk.model.BlockHeight +import cash.z.ecc.android.sdk.model.FirstClassByteArray import cash.z.ecc.android.sdk.model.ZcashNetwork import co.electriccoin.lightwallet.client.ext.BenchmarkingExt import co.electriccoin.lightwallet.client.fixture.BenchmarkingBlockRangeFixture @@ -76,19 +78,24 @@ class SharedViewModel(application: Application) : AndroidViewModel(application) val network = ZcashNetwork.fromResources(application) val synchronizer = Synchronizer.new( - application, - network, - lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network), - seed = seedBytes, + alias = OLD_UI_SYNCHRONIZER_ALIAS, birthday = if (BenchmarkingExt.isBenchmarking()) { BlockHeight.new(BenchmarkingBlockRangeFixture.new().start) } else { birthdayHeight.value }, + context = application, + lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network), + setup = + AccountCreateSetup( + accountName = "Zcash Account 1", + keySource = DEMO_APP_ZCASH_ACCOUNT_KEY_SOURCE, + seed = FirstClassByteArray(seedBytes) + ), // We use restore mode as this is always initialization with an older seed walletInitMode = WalletInitMode.RestoreWallet, - alias = OLD_UI_SYNCHRONIZER_ALIAS + zcashNetwork = network, ) send(InternalSynchronizerStatus.Available(synchronizer)) diff --git a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/WalletCoordinatorFactory.kt b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/WalletCoordinatorFactory.kt index 3b172e239..3ff897738 100644 --- a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/WalletCoordinatorFactory.kt +++ b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/WalletCoordinatorFactory.kt @@ -24,7 +24,9 @@ private val lazy = WalletCoordinator( context = it, - persistableWallet = persistableWalletFlow + persistableWallet = persistableWalletFlow, + accountName = "Zcash Account 1", + keySource = "ZCASH", ) } diff --git a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getaddress/GetAddressFragment.kt b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getaddress/GetAddressFragment.kt index 47c3e9ef1..e7a9bc786 100644 --- a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getaddress/GetAddressFragment.kt +++ b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getaddress/GetAddressFragment.kt @@ -30,7 +30,7 @@ class GetAddressFragment : BaseDemoFragment() { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { launch { sharedViewModel.synchronizerFlow.filterNotNull().collect { synchronizer -> - val account = synchronizer.getAccounts()[CURRENT_ZIP_32_ACCOUNT_INDEX] + val account = synchronizer.getAccounts()[CURRENT_ZIP_32_ACCOUNT_INDEX.toInt()] binding.unifiedAddress.apply { reportTraceEvent(ProvideAddressBenchmarkTrace.Event.UNIFIED_ADDRESS_START) 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 9f76c2eb2..17925b1fe 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 @@ -19,11 +19,11 @@ import cash.z.ecc.android.sdk.ext.convertZatoshiToZec import cash.z.ecc.android.sdk.ext.convertZatoshiToZecString import cash.z.ecc.android.sdk.ext.toUsdString import cash.z.ecc.android.sdk.internal.Twig -import cash.z.ecc.android.sdk.model.Account import cash.z.ecc.android.sdk.model.PercentDecimal 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.Zip32AccountIndex import cash.z.ecc.android.sdk.tool.DerivationTool import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.combine @@ -79,10 +79,11 @@ class GetBalanceFragment : BaseDemoFragment() { DerivationTool.getInstance().deriveUnifiedSpendingKey( seed, network, - Account(CURRENT_ZIP_32_ACCOUNT_INDEX) + Zip32AccountIndex.new(CURRENT_ZIP_32_ACCOUNT_INDEX) ) sharedViewModel.synchronizerFlow.value?.let { synchronizer -> - synchronizer.proposeShielding(usk.account, Zatoshi(100000))?.let { it1 -> + val account = synchronizer.getAccounts()[CURRENT_ZIP_32_ACCOUNT_INDEX.toInt()] + synchronizer.proposeShielding(account, Zatoshi(100000))?.let { it1 -> synchronizer.createProposedTransactions( it1, usk @@ -117,10 +118,10 @@ class GetBalanceFragment : BaseDemoFragment() { sharedViewModel.synchronizerFlow .filterNotNull() .flatMapLatest { - val account = it.getAccounts()[CURRENT_ZIP_32_ACCOUNT_INDEX] + val account = it.getAccounts()[CURRENT_ZIP_32_ACCOUNT_INDEX.toInt()] it.walletBalances.combine(it.exchangeRateUsd) { balances, rate -> balances?.let { - val walletBalance = balances[account]!!.sapling + val walletBalance = balances[account.accountUuid]!!.sapling walletBalance to rate.currencyConversion ?.priceOfZec @@ -134,10 +135,10 @@ class GetBalanceFragment : BaseDemoFragment() { sharedViewModel.synchronizerFlow .filterNotNull() .flatMapLatest { - val account = it.getAccounts()[CURRENT_ZIP_32_ACCOUNT_INDEX] + val account = it.getAccounts()[CURRENT_ZIP_32_ACCOUNT_INDEX.toInt()] it.walletBalances.combine(it.exchangeRateUsd) { balances, rate -> balances?.let { - val walletBalance = balances[account]!!.orchard + val walletBalance = balances[account.accountUuid]!!.orchard walletBalance to rate.currencyConversion ?.priceOfZec @@ -150,10 +151,10 @@ class GetBalanceFragment : BaseDemoFragment() { sharedViewModel.synchronizerFlow .filterNotNull() .flatMapLatest { - val account = it.getAccounts()[CURRENT_ZIP_32_ACCOUNT_INDEX] + val account = it.getAccounts()[CURRENT_ZIP_32_ACCOUNT_INDEX.toInt()] it.walletBalances.combine(it.exchangeRateUsd) { balances, rate -> balances?.let { - val walletBalance = balances[account]!!.unshielded + val walletBalance = balances[account.accountUuid]!!.unshielded walletBalance to rate.currencyConversion ?.priceOfZec @@ -217,10 +218,22 @@ class GetBalanceFragment : BaseDemoFragment() { sharedViewModel.synchronizerFlow.value?.let { synchronizer -> val rate = synchronizer.exchangeRateUsd.value.currencyConversion?.priceOfZec?.toBigDecimal() viewLifecycleOwner.lifecycleScope.launch { - val account = synchronizer.getAccounts()[CURRENT_ZIP_32_ACCOUNT_INDEX] - onOrchardBalance(synchronizer.walletBalances.value?.let { Pair(it[account]!!.orchard, rate) }) - onSaplingBalance(synchronizer.walletBalances.value?.let { Pair(it[account]!!.sapling, rate) }) - onTransparentBalance(synchronizer.walletBalances.value?.let { Pair(it[account]!!.unshielded, rate) }) + val account = synchronizer.getAccounts()[CURRENT_ZIP_32_ACCOUNT_INDEX.toInt()] + onOrchardBalance( + synchronizer.walletBalances.value?.let { + Pair(it[account.accountUuid]!!.orchard, rate) + } + ) + onSaplingBalance( + synchronizer.walletBalances.value?.let { + Pair(it[account.accountUuid]!!.sapling, rate) + } + ) + onTransparentBalance( + synchronizer.walletBalances.value?.let { + Pair(it[account.accountUuid]!!.unshielded, rate) + } + ) } } } diff --git a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getprivatekey/GetPrivateKeyFragment.kt b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getprivatekey/GetPrivateKeyFragment.kt index 20ba6d7e3..f4f9e0619 100644 --- a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getprivatekey/GetPrivateKeyFragment.kt +++ b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getprivatekey/GetPrivateKeyFragment.kt @@ -13,8 +13,8 @@ import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment import cash.z.ecc.android.sdk.demoapp.databinding.FragmentGetPrivateKeyBinding import cash.z.ecc.android.sdk.demoapp.ext.requireApplicationContext import cash.z.ecc.android.sdk.demoapp.util.fromResources -import cash.z.ecc.android.sdk.model.Account import cash.z.ecc.android.sdk.model.ZcashNetwork +import cash.z.ecc.android.sdk.model.Zip32AccountIndex import cash.z.ecc.android.sdk.tool.DerivationTool import kotlinx.coroutines.launch @@ -50,7 +50,7 @@ class GetPrivateKeyFragment : BaseDemoFragment() { DerivationTool.getInstance().deriveUnifiedSpendingKey( seed, ZcashNetwork.fromResources(requireApplicationContext()), - Account(5) + Zip32AccountIndex.new(5L) ) // derive the key that allows you to view but not spend transactions diff --git a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listutxos/ListUtxosFragment.kt b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listutxos/ListUtxosFragment.kt index b5dd07e75..baf65a07b 100644 --- a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listutxos/ListUtxosFragment.kt +++ b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listutxos/ListUtxosFragment.kt @@ -188,7 +188,7 @@ class ListUtxosFragment : BaseDemoFragment() { .filterNotNull() .collect { binding.inputAddress.setText( - it.getTransparentAddress(it.getAccounts()[CURRENT_ZIP_32_ACCOUNT_INDEX]) + it.getTransparentAddress(it.getAccounts()[CURRENT_ZIP_32_ACCOUNT_INDEX.toInt()]) ) } } 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 6924f4554..89ba83d5c 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 @@ -99,10 +99,10 @@ class SendFragment : BaseDemoFragment() { sharedViewModel.synchronizerFlow .filterNotNull() .flatMapLatest { - val account = it.getAccounts()[CURRENT_ZIP_32_ACCOUNT_INDEX] + val account = it.getAccounts()[CURRENT_ZIP_32_ACCOUNT_INDEX.toInt()] it.walletBalances.mapLatest { balances -> balances?.let { - val walletBalance = balances[account]!!.sapling + val walletBalance = balances[account.accountUuid]!!.sapling walletBalance } } @@ -156,9 +156,10 @@ class SendFragment : BaseDemoFragment() { val toAddress = addressInput.text.toString().trim() lifecycleScope.launch { sharedViewModel.synchronizerFlow.value?.let { synchronizer -> + val account = synchronizer.getAccounts()[CURRENT_ZIP_32_ACCOUNT_INDEX.toInt()] synchronizer.createProposedTransactions( synchronizer.proposeTransfer( - spendingKey.account, + account, toAddress, amount, "Funds from Demo App" diff --git a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/fixture/WalletSnapshotFixture.kt b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/fixture/WalletSnapshotFixture.kt index 1afcbb573..344ca3b35 100644 --- a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/fixture/WalletSnapshotFixture.kt +++ b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/fixture/WalletSnapshotFixture.kt @@ -6,8 +6,8 @@ import cash.z.ecc.android.sdk.demoapp.ui.screen.home.viewmodel.SynchronizerError import cash.z.ecc.android.sdk.demoapp.ui.screen.home.viewmodel.WalletSnapshot import cash.z.ecc.android.sdk.fixture.AccountBalanceFixture import cash.z.ecc.android.sdk.fixture.AccountFixture -import cash.z.ecc.android.sdk.model.Account import cash.z.ecc.android.sdk.model.AccountBalance +import cash.z.ecc.android.sdk.model.AccountUuid import cash.z.ecc.android.sdk.model.PercentDecimal import java.math.BigDecimal @@ -17,9 +17,9 @@ object WalletSnapshotFixture { val PROGRESS = PercentDecimal.ZERO_PERCENT val EXCHANGE_RATE_USD: BigDecimal = BigDecimal(37.4850) val ACCOUNT = AccountFixture.new() - val WALLET_BALANCES: Map = + val WALLET_BALANCES: Map = mapOf( - ACCOUNT to AccountBalanceFixture.new() + ACCOUNT.accountUuid to AccountBalanceFixture.new() ) // Should fill in with non-empty values for better example values in tests and UI previews @@ -32,16 +32,16 @@ object WalletSnapshotFixture { null, null ), - walletBalances: Map = WALLET_BALANCES, + walletBalances: Map = WALLET_BALANCES, exchangeRateUsd: BigDecimal? = EXCHANGE_RATE_USD, progress: PercentDecimal = PROGRESS, synchronizerError: SynchronizerError? = null ) = WalletSnapshot( - status, - processorInfo, - walletBalances, - exchangeRateUsd, - progress, - synchronizerError + status = status, + processorInfo = processorInfo, + walletBalances = walletBalances, + exchangeRateUsd = exchangeRateUsd, + progress = progress, + synchronizerError = synchronizerError ) } diff --git a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/ui/screen/home/viewmodel/WalletSnapshot.kt b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/ui/screen/home/viewmodel/WalletSnapshot.kt index 854397dae..aa4445678 100644 --- a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/ui/screen/home/viewmodel/WalletSnapshot.kt +++ b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/ui/screen/home/viewmodel/WalletSnapshot.kt @@ -2,20 +2,20 @@ package cash.z.ecc.android.sdk.demoapp.ui.screen.home.viewmodel import cash.z.ecc.android.sdk.Synchronizer import cash.z.ecc.android.sdk.block.processor.CompactBlockProcessor -import cash.z.ecc.android.sdk.model.Account import cash.z.ecc.android.sdk.model.AccountBalance +import cash.z.ecc.android.sdk.model.AccountUuid import cash.z.ecc.android.sdk.model.PercentDecimal import java.math.BigDecimal data class WalletSnapshot( val status: Synchronizer.Status, val processorInfo: CompactBlockProcessor.ProcessorInfo, - val walletBalances: Map, + val walletBalances: Map, val exchangeRateUsd: BigDecimal?, val progress: PercentDecimal, val synchronizerError: SynchronizerError? ) { - fun balanceByAccount(account: Account): AccountBalance { - return walletBalances[account] ?: error("Balance of $account could not be found.") + fun balanceByAccountUuid(accountUuid: AccountUuid): AccountBalance { + return walletBalances[accountUuid] ?: error("Balance of account? $accountUuid could not be found.") } } 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 0652a7a33..214620484 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 @@ -21,6 +21,7 @@ import cash.z.ecc.android.sdk.demoapp.util.fromResources import cash.z.ecc.android.sdk.internal.Twig import cash.z.ecc.android.sdk.model.Account import cash.z.ecc.android.sdk.model.AccountBalance +import cash.z.ecc.android.sdk.model.AccountUuid import cash.z.ecc.android.sdk.model.BlockHeight import cash.z.ecc.android.sdk.model.ObserveFiatCurrencyResult import cash.z.ecc.android.sdk.model.PercentDecimal @@ -100,16 +101,18 @@ class WalletViewModel(application: Application) : AndroidViewModel(application) secretState .filterIsInstance() .map { it.persistableWallet } - .map { + .map { secretState -> val bip39Seed = withContext(Dispatchers.IO) { - Mnemonics.MnemonicCode(it.seedPhrase.joinToString()).toSeed() + Mnemonics.MnemonicCode(secretState.seedPhrase.joinToString()).toSeed() } - DerivationTool.getInstance().deriveUnifiedSpendingKey( - seed = bip39Seed, - network = it.network, - account = getCurrentAccount() - ) + getCurrentAccount().hdAccountIndex?.let { accountIndex -> + DerivationTool.getInstance().deriveUnifiedSpendingKey( + seed = bip39Seed, + network = secretState.network, + accountIndex = accountIndex + ) + } }.stateIn( viewModelScope, SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT), @@ -210,10 +213,12 @@ class WalletViewModel(application: Application) : AndroidViewModel(application) mutableSendState.value = SendState.Sending val synchronizer = synchronizer.value + if (null != synchronizer) { + val account = getCurrentAccount() viewModelScope.launch { val spendingKey = spendingKey.filterNotNull().first() - runCatching { synchronizer.send(spendingKey, zecSend) } + runCatching { synchronizer.send(spendingKey, account, zecSend) } .onSuccess { mutableSendState.value = SendState.Sent(it.toList()) } .onFailure { mutableSendState.value = SendState.Error(it) } } @@ -233,11 +238,11 @@ class WalletViewModel(application: Application) : AndroidViewModel(application) val synchronizer = synchronizer.value return if (null != synchronizer) { + val account = getCurrentAccount() // Calling the proposal API within a blocking coroutine should be fine for the showcase purpose runBlocking { - val spendingKey = spendingKey.filterNotNull().first() kotlin.runCatching { - synchronizer.proposeSend(spendingKey.account, zecSend) + synchronizer.proposeSend(account, zecSend) }.onFailure { Twig.error(it) { "Failed to get transaction proposal" } }.getOrNull() @@ -258,11 +263,11 @@ class WalletViewModel(application: Application) : AndroidViewModel(application) val synchronizer = synchronizer.value return if (null != synchronizer) { + val account = getCurrentAccount() // Calling the proposal API within a blocking coroutine should be fine for the showcase purpose runBlocking { - val spendingKey = spendingKey.filterNotNull().first() kotlin.runCatching { - synchronizer.proposeFulfillingPaymentUri(spendingKey.account, uri) + synchronizer.proposeFulfillingPaymentUri(account, uri) }.onFailure { Twig.error(it) { "Failed to get transaction proposal from uri" } }.getOrNull() @@ -286,11 +291,12 @@ class WalletViewModel(application: Application) : AndroidViewModel(application) val synchronizer = synchronizer.value if (null != synchronizer) { + val account = getCurrentAccount() viewModelScope.launch { val spendingKey = spendingKey.filterNotNull().first() kotlin.runCatching { @Suppress("MagicNumber") - synchronizer.proposeShielding(spendingKey.account, Zatoshi(100000))?.let { + synchronizer.proposeShielding(account, Zatoshi(100000))?.let { synchronizer.createProposedTransactions( it, spendingKey @@ -383,7 +389,7 @@ class WalletViewModel(application: Application) : AndroidViewModel(application) emptyList() ) - fun getCurrentAccount(): Account = getAccounts()[CURRENT_ZIP_32_ACCOUNT_INDEX] + fun getCurrentAccount(): Account = getAccounts()[CURRENT_ZIP_32_ACCOUNT_INDEX.toInt()] companion object { private const val QUICK_REWIND_BLOCKS = 100 @@ -508,7 +514,7 @@ private fun Synchronizer.toWalletSnapshot() = WalletSnapshot( flows[0] as Synchronizer.Status, flows[1] as CompactBlockProcessor.ProcessorInfo, - flows[2] as Map, + flows[2] as Map, exchangeRateUsd.currencyConversion?.priceOfZec?.toBigDecimal(), progressPercentDecimal, flows[5] as SynchronizerError? 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 9fc2efeab..ca51c496f 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 @@ -3,6 +3,7 @@ package cash.z.ecc.android.sdk.internal import cash.z.ecc.android.sdk.ext.toHex import cash.z.ecc.android.sdk.fixture.WalletFixture import cash.z.ecc.android.sdk.model.ZcashNetwork +import cash.z.ecc.android.sdk.model.Zip32AccountIndex import cash.z.ecc.android.sdk.tool.DerivationTool import kotlinx.coroutines.test.runTest import org.junit.Test @@ -13,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 = Zip32AccountIndex.new(0L) @OptIn(ExperimentalEncodingApi::class) @Test @@ -24,7 +25,7 @@ class DerivationToolImplTest { contextString = CONTEXT.toByteArray(), seed = seedPhrase.toByteArray(), network = network, - account = account, + accountIndex = accountIndex, ) assertEquals("byyNHiMfj8N2tiCHc4Mv/0ts0IuUqDPe99MvW8B03IY=", Base64.encode(key)) } @@ -62,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-incubator-lib/src/main/java/cash/z/ecc/android/sdk/WalletCoordinator.kt b/sdk-incubator-lib/src/main/java/cash/z/ecc/android/sdk/WalletCoordinator.kt index fe8ca0193..7bbd7935f 100644 --- a/sdk-incubator-lib/src/main/java/cash/z/ecc/android/sdk/WalletCoordinator.kt +++ b/sdk-incubator-lib/src/main/java/cash/z/ecc/android/sdk/WalletCoordinator.kt @@ -3,6 +3,8 @@ package cash.z.ecc.android.sdk import android.content.Context import cash.z.ecc.android.sdk.ext.onFirst import cash.z.ecc.android.sdk.internal.Twig +import cash.z.ecc.android.sdk.model.AccountCreateSetup +import cash.z.ecc.android.sdk.model.FirstClassByteArray import cash.z.ecc.android.sdk.model.PersistableWallet import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi @@ -31,6 +33,9 @@ import java.util.UUID /** * @param persistableWallet flow of the user's stored wallet. Null indicates that no wallet has been stored. + * @param accountName A human-readable name for the account, that will be used while instantiating [Synchronizer.new] + * @param keySource A string identifier or other metadata describing the source of the seed, that will be used while + * instantiating [Synchronizer.new] * * One area where this class needs to change before it can be moved out of the incubator is that we need to be able to * start synchronization without necessarily decrypting the wallet. @@ -40,7 +45,9 @@ import java.util.UUID */ class WalletCoordinator( context: Context, - val persistableWallet: Flow + val persistableWallet: Flow, + val accountName: String, + val keySource: String?, ) { private val applicationContext = context.applicationContext @@ -79,7 +86,12 @@ class WalletCoordinator( zcashNetwork = persistableWallet.network, lightWalletEndpoint = persistableWallet.endpoint, birthday = persistableWallet.birthday, - seed = persistableWallet.seedPhrase.toByteArray(), + setup = + AccountCreateSetup( + accountName = accountName, + keySource = keySource, + seed = FirstClassByteArray(persistableWallet.seedPhrase.toByteArray()) + ), walletInitMode = persistableWallet.walletInitMode, ) diff --git a/sdk-incubator-lib/src/main/java/cash/z/ecc/android/sdk/fixture/WalletFixture.kt b/sdk-incubator-lib/src/main/java/cash/z/ecc/android/sdk/fixture/WalletFixture.kt index c5f645265..d89169277 100644 --- a/sdk-incubator-lib/src/main/java/cash/z/ecc/android/sdk/fixture/WalletFixture.kt +++ b/sdk-incubator-lib/src/main/java/cash/z/ecc/android/sdk/fixture/WalletFixture.kt @@ -3,6 +3,7 @@ package cash.z.ecc.android.sdk.fixture import cash.z.ecc.android.sdk.model.Account import cash.z.ecc.android.sdk.model.BlockHeight import cash.z.ecc.android.sdk.model.ZcashNetwork +import java.util.UUID /** * Provides two default wallets, making it easy to test sending funds back and forth between them. @@ -19,7 +20,7 @@ sealed class WalletFixture { @Suppress("MaxLineLength") data object Ben : WalletFixture() { override val accounts: List - get() = listOf(AccountFixture.new(0)) + get() = listOf(AccountFixture.new(accountUuid = UUID.fromString("52175368-821a-4664-8a7c-6a75d850f71c"))) override val seedPhrase: String get() = @@ -56,7 +57,7 @@ sealed class WalletFixture { @Suppress("MaxLineLength") data object Alice : WalletFixture() { override val accounts: List - get() = listOf(AccountFixture.new(1)) + get() = listOf(AccountFixture.new(accountUuid = UUID.fromString("8a204240-73a5-4e7a-93c2-a6e05711a000"))) override val seedPhrase: String get() = diff --git a/sdk-incubator-lib/src/main/java/cash/z/ecc/android/sdk/model/ZecSend.kt b/sdk-incubator-lib/src/main/java/cash/z/ecc/android/sdk/model/ZecSend.kt index 73464d6e2..7bb5920db 100644 --- a/sdk-incubator-lib/src/main/java/cash/z/ecc/android/sdk/model/ZecSend.kt +++ b/sdk-incubator-lib/src/main/java/cash/z/ecc/android/sdk/model/ZecSend.kt @@ -13,15 +13,17 @@ data class ZecSend( suspend fun Synchronizer.send( spendingKey: UnifiedSpendingKey, + account: Account, send: ZecSend ) = createProposedTransactions( - proposeTransfer( - spendingKey.account, - send.destination.address, - send.amount, - send.memo.value - ), - spendingKey + proposal = + proposeTransfer( + account = account, + recipient = send.destination.address, + amount = send.amount, + memo = send.memo.value + ), + usk = spendingKey ) /** diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/fixture/WalletFixture.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/fixture/WalletFixture.kt index 6bfb39772..286a47d6a 100644 --- a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/fixture/WalletFixture.kt +++ b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/fixture/WalletFixture.kt @@ -1,19 +1,17 @@ package cash.z.ecc.android.sdk.fixture import cash.z.ecc.android.bip39.Mnemonics -import cash.z.ecc.android.sdk.internal.deriveUnifiedSpendingKey import cash.z.ecc.android.sdk.internal.jni.RustDerivationTool +import cash.z.ecc.android.sdk.model.UnifiedSpendingKey import cash.z.ecc.android.sdk.model.ZcashNetwork object WalletFixture { - // TODO [#1644]: Refactor Account ZIP32 index across SDK - // TODO [#1644]: https://github.com/Electric-Coin-Company/zcash-android-wallet-sdk/issues/1644 - const val ACCOUNT_INDEX = 0 + const val ACCOUNT_INDEX = 0L val NETWORK = ZcashNetwork.Mainnet // This is the "Ben" wallet phrase from sdk-incubator-lib. - const val SEED_PHRASE = + const val BEN_SEED_PHRASE = "kitchen renew wide common vague fold vacuum tilt amazing pear square gossip jewel month" + " tree shock scan alpha just spot fluid toilet view dinner" @@ -23,12 +21,14 @@ object WalletFixture { " cannon grab despair throw review deal slush frame" suspend fun getUnifiedSpendingKey( - seed: String = SEED_PHRASE, + seed: String = BEN_SEED_PHRASE, network: ZcashNetwork = NETWORK, - accountIndex: Int = ACCOUNT_INDEX - ) = RustDerivationTool.new().deriveUnifiedSpendingKey( - Mnemonics.MnemonicCode(seed).toEntropy(), - network.id, - accountIndex + accountIndex: Long = ACCOUNT_INDEX + ) = UnifiedSpendingKey.new( + RustDerivationTool.new().deriveUnifiedSpendingKey( + Mnemonics.MnemonicCode(seed).toEntropy(), + network.id, + accountIndex + ) ) } diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/integration/TestnetIntegrationTest.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/integration/TestnetIntegrationTest.kt index b2c66f1bc..5a7d91cd0 100644 --- a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/integration/TestnetIntegrationTest.kt +++ b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/integration/TestnetIntegrationTest.kt @@ -6,6 +6,7 @@ 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.onFirst +import cash.z.ecc.android.sdk.fixture.AccountCreateSetupFixture import cash.z.ecc.android.sdk.fixture.AccountFixture import cash.z.ecc.android.sdk.internal.Twig import cash.z.ecc.android.sdk.model.BlockHeight @@ -80,7 +81,7 @@ class TestnetIntegrationTest : ScopedTest() { runBlocking { var availableBalance: Zatoshi? = null synchronizer.walletBalances.onFirst { - availableBalance = it?.get(account)?.sapling?.available + availableBalance = it?.get(account.accountUuid)?.sapling?.available } synchronizer.status.filter { it == SYNCED }.onFirst { @@ -105,16 +106,17 @@ class TestnetIntegrationTest : ScopedTest() { } private suspend fun sendFunds(): Boolean { + val account = AccountFixture.new() val spendingKey = DerivationTool.getInstance().deriveUnifiedSpendingKey( seed, synchronizer.network, - AccountFixture.ZIP_32_ACCOUNT_INDEX + account.hdAccountIndex!! ) log("sending to address") synchronizer.createProposedTransactions( synchronizer.proposeTransfer( - spendingKey.account, + account, toAddress, Zatoshi(10_000L), "first mainnet tx from the SDK" @@ -148,14 +150,14 @@ class TestnetIntegrationTest : ScopedTest() { fun startUp() { synchronizer = Synchronizer.newBlocking( - context, - ZcashNetwork.Testnet, - lightWalletEndpoint = - lightWalletEndpoint, - seed = seed, + alias = "TEST", + context = context, birthday = BlockHeight.new(BIRTHDAY_HEIGHT), + lightWalletEndpoint = lightWalletEndpoint, + setup = AccountCreateSetupFixture.new(), // Using existing wallet init mode as simplification for the test - walletInitMode = WalletInitMode.ExistingWallet + walletInitMode = WalletInitMode.ExistingWallet, + zcashNetwork = ZcashNetwork.Testnet, ) } } diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/internal/SdkSynchronizerTest.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/internal/SdkSynchronizerTest.kt index db3461143..104a21c1b 100644 --- a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/internal/SdkSynchronizerTest.kt +++ b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/internal/SdkSynchronizerTest.kt @@ -6,7 +6,7 @@ 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.exception.InitializeException -import cash.z.ecc.android.sdk.fixture.AccountFixture +import cash.z.ecc.android.sdk.fixture.AccountCreateSetupFixture import cash.z.ecc.android.sdk.fixture.LightWalletEndpointFixture import cash.z.ecc.android.sdk.fixture.WalletFixture import cash.z.ecc.android.sdk.model.ZcashNetwork @@ -27,25 +27,31 @@ class SdkSynchronizerTest { // In the future, inject fake networking component so that it doesn't require hitting the network Synchronizer.new( - InstrumentationRegistry.getInstrumentation().context, - ZcashNetwork.Mainnet, - alias, - LightWalletEndpointFixture.newEndpointForNetwork(ZcashNetwork.Mainnet), - Mnemonics.MnemonicCode(WalletFixture.SEED_PHRASE).toEntropy(), + alias = alias, birthday = null, + context = InstrumentationRegistry.getInstrumentation().context, + lightWalletEndpoint = LightWalletEndpointFixture.newEndpointForNetwork(ZcashNetwork.Mainnet), + setup = + AccountCreateSetupFixture.new( + seed = Mnemonics.MnemonicCode(WalletFixture.BEN_SEED_PHRASE).toEntropy() + ), // Using existing wallet init mode as simplification for the test - walletInitMode = WalletInitMode.ExistingWallet + walletInitMode = WalletInitMode.ExistingWallet, + zcashNetwork = ZcashNetwork.Mainnet, ).use { assertFailsWith { Synchronizer.new( - InstrumentationRegistry.getInstrumentation().context, - ZcashNetwork.Mainnet, - alias, - LightWalletEndpointFixture.newEndpointForNetwork(ZcashNetwork.Mainnet), - Mnemonics.MnemonicCode(WalletFixture.SEED_PHRASE).toEntropy(), + alias = alias, birthday = null, + context = InstrumentationRegistry.getInstrumentation().context, + lightWalletEndpoint = LightWalletEndpointFixture.newEndpointForNetwork(ZcashNetwork.Mainnet), + setup = + AccountCreateSetupFixture.new( + seed = Mnemonics.MnemonicCode(WalletFixture.BEN_SEED_PHRASE).toEntropy() + ), // Using existing wallet init mode as simplification for the test - walletInitMode = WalletInitMode.ExistingWallet + walletInitMode = WalletInitMode.ExistingWallet, + zcashNetwork = ZcashNetwork.Mainnet, ) } } @@ -63,26 +69,32 @@ class SdkSynchronizerTest { // 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, - ZcashNetwork.Mainnet, - alias, - LightWalletEndpointFixture.newEndpointForNetwork(ZcashNetwork.Mainnet), - Mnemonics.MnemonicCode(WalletFixture.SEED_PHRASE).toEntropy(), + alias = alias, birthday = null, + context = InstrumentationRegistry.getInstrumentation().context, + lightWalletEndpoint = LightWalletEndpointFixture.newEndpointForNetwork(ZcashNetwork.Mainnet), + setup = + AccountCreateSetupFixture.new( + seed = Mnemonics.MnemonicCode(WalletFixture.BEN_SEED_PHRASE).toEntropy() + ), // Using existing wallet init mode as simplification for the test - walletInitMode = WalletInitMode.ExistingWallet + walletInitMode = WalletInitMode.ExistingWallet, + zcashNetwork = ZcashNetwork.Mainnet, ).use {} // Second instance should succeed because first one was closed Synchronizer.new( - InstrumentationRegistry.getInstrumentation().context, - ZcashNetwork.Mainnet, - alias, - LightWalletEndpointFixture.newEndpointForNetwork(ZcashNetwork.Mainnet), - Mnemonics.MnemonicCode(WalletFixture.SEED_PHRASE).toEntropy(), + alias = alias, birthday = null, + context = InstrumentationRegistry.getInstrumentation().context, + lightWalletEndpoint = LightWalletEndpointFixture.newEndpointForNetwork(ZcashNetwork.Mainnet), + setup = + AccountCreateSetupFixture.new( + seed = Mnemonics.MnemonicCode(WalletFixture.BEN_SEED_PHRASE).toEntropy() + ), // Using existing wallet init mode as simplification for the test - walletInitMode = WalletInitMode.ExistingWallet + walletInitMode = WalletInitMode.ExistingWallet, + zcashNetwork = ZcashNetwork.Mainnet, ).use {} } @@ -98,30 +110,36 @@ class SdkSynchronizerTest { // 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, - ZcashNetwork.Mainnet, - alias, - LightWalletEndpointFixture.newEndpointForNetwork(ZcashNetwork.Mainnet), - Mnemonics.MnemonicCode(WalletFixture.SEED_PHRASE).toEntropy(), + alias = alias, birthday = null, + context = InstrumentationRegistry.getInstrumentation().context, + lightWalletEndpoint = LightWalletEndpointFixture.newEndpointForNetwork(ZcashNetwork.Mainnet), + setup = + AccountCreateSetupFixture.new( + seed = Mnemonics.MnemonicCode(WalletFixture.ALICE_SEED_PHRASE).toEntropy() + ), // Using existing wallet init mode as simplification for the test - walletInitMode = WalletInitMode.ExistingWallet + walletInitMode = WalletInitMode.ExistingWallet, + zcashNetwork = ZcashNetwork.Mainnet, ).use { - it.getSaplingAddress(AccountFixture.new()) + it.getSaplingAddress(it.getAccounts()[0]) } // Second instance should fail because the seed is not relevant to the wallet. val error = assertFailsWith { Synchronizer.new( - InstrumentationRegistry.getInstrumentation().context, - ZcashNetwork.Mainnet, - alias, - LightWalletEndpointFixture.newEndpointForNetwork(ZcashNetwork.Mainnet), - Mnemonics.MnemonicCode(WalletFixture.ALICE_SEED_PHRASE).toEntropy(), + alias = alias, birthday = null, + context = InstrumentationRegistry.getInstrumentation().context, + lightWalletEndpoint = LightWalletEndpointFixture.newEndpointForNetwork(ZcashNetwork.Mainnet), + setup = + AccountCreateSetupFixture.new( + seed = Mnemonics.MnemonicCode(WalletFixture.BEN_SEED_PHRASE).toEntropy() + ), // Using existing wallet init mode as simplification for the test - walletInitMode = WalletInitMode.ExistingWallet + walletInitMode = WalletInitMode.ExistingWallet, + zcashNetwork = ZcashNetwork.Mainnet, ).use {} } assertEquals(InitializeException.SeedNotRelevant, error) diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/model/UnifiedSpendingKeyTest.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/model/UnifiedSpendingKeyTest.kt index a1b6c5895..d6e7a7d53 100644 --- a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/model/UnifiedSpendingKeyTest.kt +++ b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/model/UnifiedSpendingKeyTest.kt @@ -16,10 +16,10 @@ class UnifiedSpendingKeyTest { val expected = spendingKey.copyBytes().copyOf() val bytes = spendingKey.copyBytes() - val newSpendingKey = UnifiedSpendingKey.new(spendingKey.account, bytes) + val newSpendingKey = UnifiedSpendingKey.new(bytes) bytes.clear() - assertContentEquals(expected, newSpendingKey.getOrThrow().copyBytes()) + assertContentEquals(expected, newSpendingKey.copyBytes()) } @Test @@ -29,11 +29,11 @@ class UnifiedSpendingKeyTest { val spendingKey = WalletFixture.getUnifiedSpendingKey() val expected = spendingKey.copyBytes() - val newSpendingKey = UnifiedSpendingKey.new(spendingKey.account, expected) + val newSpendingKey = UnifiedSpendingKey.new(expected) - newSpendingKey.getOrThrow().copyBytes().clear() + newSpendingKey.copyBytes().clear() - assertContentEquals(expected, newSpendingKey.getOrThrow().copyBytes()) + assertContentEquals(expected, newSpendingKey.copyBytes()) } @Test @@ -41,7 +41,7 @@ class UnifiedSpendingKeyTest { fun toString_does_not_leak() = runTest { assertEquals( - "UnifiedSpendingKey(account=Account(value=0))", + "UnifiedSpendingKey(bytes=***)", WalletFixture.getUnifiedSpendingKey().toString() ) } diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/sample/ShieldFundsSample.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/sample/ShieldFundsSample.kt index e3608e9e3..56715dcd9 100644 --- a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/sample/ShieldFundsSample.kt +++ b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/sample/ShieldFundsSample.kt @@ -37,7 +37,7 @@ class ShieldFundsSample { Assert.assertEquals( Zatoshi(5), - wallet.synchronizer.walletBalances.value?.get(AccountFixture.new())?.sapling?.available + wallet.synchronizer.walletBalances.value?.get(AccountFixture.new().accountUuid)?.sapling?.available ) } } diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/sample/TransparentRestoreSample.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/sample/TransparentRestoreSample.kt index ed60bc93c..8c1ea912b 100644 --- a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/sample/TransparentRestoreSample.kt +++ b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/sample/TransparentRestoreSample.kt @@ -3,6 +3,7 @@ package cash.z.ecc.android.sdk.sample import androidx.test.filters.LargeTest +import cash.z.ecc.android.sdk.fixture.AccountFixture import cash.z.ecc.android.sdk.model.BlockHeight import cash.z.ecc.android.sdk.model.Zatoshi import cash.z.ecc.android.sdk.model.ZcashNetwork @@ -76,8 +77,9 @@ class TransparentRestoreSample { val wallet = TestWallet(TestWallet.Backups.SAMPLE_WALLET, alias = "WalletC") // wallet.sync().rewindToHeight(1339178).join(10000) wallet.sync().rewindToHeight(BlockHeight.new(1339178L)).send( - "ztestsapling17zazsl8rryl8kjaqxnr2r29rw9d9a2mud37ugapm0s8gmyv0ue43h9lqwmhdsp3nu9dazeqfs6l", - "is send broken?" + account = AccountFixture.new(), + address = "ztestsapling17zazsl8rryl8kjaqxnr2r29rw9d9a2mud37ugapm0s8gmyv0ue43h9lqwmhdsp3nu9dazeqfs6l", + memo = "is send broken?" ).join(5) } diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/AddressGeneratorUtil.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/AddressGeneratorUtil.kt index 0d3219b34..dfdb11fa3 100644 --- a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/AddressGeneratorUtil.kt +++ b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/AddressGeneratorUtil.kt @@ -35,7 +35,7 @@ class AddressGeneratorUtil { RustDerivationTool.new().deriveUnifiedAddress( seed = seed, network = ZcashNetwork.Mainnet, - accountIndex = AccountFixture.ZIP_32_ACCOUNT_INDEX + accountIndex = AccountFixture.new().hdAccountIndex!! ) }.collect { address -> println("xrxrx2\t$address") diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/BalancePrinterUtil.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/BalancePrinterUtil.kt index 92ba7c8b8..a4ea5d6a8 100644 --- a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/BalancePrinterUtil.kt +++ b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/BalancePrinterUtil.kt @@ -4,6 +4,7 @@ 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.fixture.AccountCreateSetupFixture import cash.z.ecc.android.sdk.fixture.LightWalletEndpointFixture import cash.z.ecc.android.sdk.internal.Twig import cash.z.ecc.android.sdk.internal.ext.deleteSuspend @@ -93,13 +94,13 @@ class BalancePrinterUtil { synchronizer?.close() synchronizer = Synchronizer.new( - context, - network, - lightWalletEndpoint = LightWalletEndpointFixture.newEndpointForNetwork(network), - seed = seed, birthday = birthdayHeight, + lightWalletEndpoint = LightWalletEndpointFixture.newEndpointForNetwork(network), + context = context, + setup = AccountCreateSetupFixture.new(seed = seed), // Using existing wallet init mode as simplification for the test - walletInitMode = WalletInitMode.ExistingWallet + walletInitMode = WalletInitMode.ExistingWallet, + zcashNetwork = network, ) // deleteDb(dataDbPath) diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/DataDbScannerUtil.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/DataDbScannerUtil.kt index a6f88b479..2371affef 100644 --- a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/DataDbScannerUtil.kt +++ b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/DataDbScannerUtil.kt @@ -5,6 +5,7 @@ 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.fixture.AccountCreateSetupFixture import cash.z.ecc.android.sdk.fixture.LightWalletEndpointFixture import cash.z.ecc.android.sdk.model.BlockHeight import cash.z.ecc.android.sdk.model.ZcashNetwork @@ -66,11 +67,11 @@ class DataDbScannerUtil { fun scanExistingDb() { synchronizer = Synchronizer.newBlocking( - context, - ZcashNetwork.Mainnet, - lightWalletEndpoint = LightWalletEndpointFixture.newEndpointForNetwork(ZcashNetwork.Mainnet), - seed = byteArrayOf(), birthday = BlockHeight.new(birthdayHeight), + context = context, + lightWalletEndpoint = LightWalletEndpointFixture.newEndpointForNetwork(ZcashNetwork.Mainnet), + zcashNetwork = ZcashNetwork.Mainnet, + setup = AccountCreateSetupFixture.new(seed = byteArrayOf()), // Using existing wallet init mode as simplification for the test walletInitMode = WalletInitMode.ExistingWallet ) diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/TestWallet.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/TestWallet.kt index 00d5e7f12..2c1b379fc 100644 --- a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/TestWallet.kt +++ b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/TestWallet.kt @@ -6,11 +6,13 @@ 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.fixture.AccountCreateSetupFixture import cash.z.ecc.android.sdk.fixture.AccountFixture import cash.z.ecc.android.sdk.fixture.LightWalletEndpointFixture 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 +import cash.z.ecc.android.sdk.model.Account import cash.z.ecc.android.sdk.model.BlockHeight import cash.z.ecc.android.sdk.model.Zatoshi import cash.z.ecc.android.sdk.model.ZcashNetwork @@ -64,22 +66,22 @@ class TestWallet( RustDerivationTool.new().deriveUnifiedSpendingKey( seed = seed, network = network, - accountIndex = AccountFixture.ZIP_32_ACCOUNT_INDEX + accountIndex = AccountFixture.new().hdAccountIndex!! ) } val synchronizer: SdkSynchronizer = Synchronizer.newBlocking( - context, - network, - alias, + alias = alias, + birthday = startHeight, + context = context, lightWalletEndpoint = endpoint, - seed = seed, - startHeight, + setup = AccountCreateSetupFixture.new(), // Using existing wallet init mode as simplification for the test - walletInitMode = WalletInitMode.ExistingWallet + walletInitMode = WalletInitMode.ExistingWallet, + zcashNetwork = network, ) as SdkSynchronizer - val available get() = synchronizer.walletBalances.value?.get(account)?.sapling?.available + val available get() = synchronizer.walletBalances.value?.get(account.accountUuid)?.sapling?.available val unifiedAddress = runBlocking { synchronizer.getUnifiedAddress(account) } val transparentAddress = @@ -108,13 +110,14 @@ class TestWallet( } suspend fun send( + account: Account, address: String = transparentAddress, memo: String = "", amount: Zatoshi = Zatoshi(500L) ): TestWallet { synchronizer.createProposedTransactions( synchronizer.proposeTransfer( - spendingKey.account, + account, address, amount, memo @@ -129,7 +132,7 @@ class TestWallet( return this } - suspend fun shieldFunds(): TestWallet { + suspend fun shieldFunds(account: Account): TestWallet { synchronizer.refreshUtxos(account, BlockHeight.new(935000L)).let { count -> Twig.debug { "FOUND $count new UTXOs" } } @@ -138,7 +141,7 @@ class TestWallet( Twig.debug { "FOUND utxo balance of total: $walletBalance" } if (walletBalance.value > 0L) { - synchronizer.proposeShielding(spendingKey.account, Zatoshi(100000))?.let { + synchronizer.proposeShielding(account, Zatoshi(100000))?.let { synchronizer.createProposedTransactions( it, spendingKey diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/fixture/FakeRustBackend.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/fixture/FakeRustBackend.kt index 31394ab01..116bc0c5c 100644 --- a/sdk-lib/src/androidTest/java/cash/z/ecc/fixture/FakeRustBackend.kt +++ b/sdk-lib/src/androidTest/java/cash/z/ecc/fixture/FakeRustBackend.kt @@ -2,12 +2,12 @@ package cash.z.ecc.fixture import cash.z.ecc.android.sdk.internal.Backend import cash.z.ecc.android.sdk.internal.model.JniAccount +import cash.z.ecc.android.sdk.internal.model.JniAccountUsk import cash.z.ecc.android.sdk.internal.model.JniBlockMeta import cash.z.ecc.android.sdk.internal.model.JniRewindResult import cash.z.ecc.android.sdk.internal.model.JniScanRange import cash.z.ecc.android.sdk.internal.model.JniSubtreeRoot import cash.z.ecc.android.sdk.internal.model.JniTransactionDataRequest -import cash.z.ecc.android.sdk.internal.model.JniUnifiedSpendingKey import cash.z.ecc.android.sdk.internal.model.JniWalletSummary import cash.z.ecc.android.sdk.internal.model.ProposalUnsafe @@ -89,14 +89,14 @@ internal class FakeRustBackend( } override suspend fun proposeTransferFromUri( - accountIndex: Int, + accountUuid: ByteArray, uri: String ): ProposalUnsafe { error("Intentionally not implemented yet.") } override suspend fun proposeTransfer( - accountIndex: Int, + accountUuid: ByteArray, to: String, value: Long, memo: ByteArray? @@ -105,7 +105,7 @@ internal class FakeRustBackend( } override suspend fun proposeShielding( - accountIndex: Int, + accountUuid: ByteArray, shieldingThreshold: Long, memo: ByteArray?, transparentReceiver: String? @@ -136,10 +136,25 @@ internal class FakeRustBackend( } override suspend fun createAccount( + accountName: String, + keySource: String?, seed: ByteArray, treeState: ByteArray, - recoverUntil: Long? - ): JniUnifiedSpendingKey { + recoverUntil: Long?, + ): JniAccountUsk { + error("Intentionally not implemented yet.") + } + + override suspend fun importAccountUfvk( + accountName: String, + keySource: String?, + ufvk: String, + treeState: ByteArray, + recoverUntil: Long?, + purpose: Int, + seedFingerprint: ByteArray?, + zip32AccountIndex: Long?, + ): JniAccount { error("Intentionally not implemented yet.") } @@ -159,7 +174,7 @@ internal class FakeRustBackend( error("Intentionally not implemented in mocked FakeRustBackend implementation.") } - override suspend fun getCurrentAddress(accountIndex: Int): String { + override suspend fun getCurrentAddress(accountUuid: ByteArray): String { error("Intentionally not implemented yet.") } @@ -171,7 +186,7 @@ internal class FakeRustBackend( error("Intentionally not implemented yet.") } - override suspend fun listTransparentReceivers(accountIndex: Int): List { + override suspend fun listTransparentReceivers(accountUuid: ByteArray): List { error("Intentionally not implemented yet.") } 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 e4cde8496..6b7739e3b 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 @@ -42,9 +42,14 @@ import cash.z.ecc.android.sdk.internal.transaction.OutboundTransactionManagerImp import cash.z.ecc.android.sdk.internal.transaction.TransactionEncoder import cash.z.ecc.android.sdk.internal.transaction.TransactionEncoderImpl import cash.z.ecc.android.sdk.model.Account +import cash.z.ecc.android.sdk.model.AccountCreateSetup +import cash.z.ecc.android.sdk.model.AccountImportSetup +import cash.z.ecc.android.sdk.model.AccountUsk +import cash.z.ecc.android.sdk.model.AccountUuid import cash.z.ecc.android.sdk.model.BlockHeight import cash.z.ecc.android.sdk.model.FastestServersResult import cash.z.ecc.android.sdk.model.FetchFiatCurrencyResult +import cash.z.ecc.android.sdk.model.FirstClassByteArray import cash.z.ecc.android.sdk.model.ObserveFiatCurrencyResult import cash.z.ecc.android.sdk.model.PercentDecimal import cash.z.ecc.android.sdk.model.Proposal @@ -56,6 +61,7 @@ import cash.z.ecc.android.sdk.model.TransactionSubmitResult import cash.z.ecc.android.sdk.model.UnifiedSpendingKey import cash.z.ecc.android.sdk.model.Zatoshi import cash.z.ecc.android.sdk.model.ZcashNetwork +import cash.z.ecc.android.sdk.tool.CheckpointTool import cash.z.ecc.android.sdk.type.AddressType import cash.z.ecc.android.sdk.type.AddressType.Shielded import cash.z.ecc.android.sdk.type.AddressType.Tex @@ -118,6 +124,7 @@ import kotlin.time.Duration.Companion.seconds */ @Suppress("TooManyFunctions", "LongParameterList") class SdkSynchronizer private constructor( + private val context: Context, private val synchronizerKey: SynchronizerKey, private val storage: DerivedDataRepository, private val txManager: OutboundTransactionManager, @@ -149,6 +156,7 @@ class SdkSynchronizer private constructor( */ @Suppress("LongParameterList") internal suspend fun new( + context: Context, zcashNetwork: ZcashNetwork, alias: String, repository: DerivedDataRepository, @@ -165,6 +173,7 @@ class SdkSynchronizer private constructor( checkForExistingSynchronizers(synchronizerKey) SdkSynchronizer( + context, synchronizerKey, repository, txManager, @@ -459,6 +468,22 @@ class SdkSynchronizer private constructor( } } + override suspend fun getTransactions(accountUuid: AccountUuid): Flow> { + return combine( + processor.networkHeight, + storage.getTransactions(accountUuid) + ) { networkHeight, allAccountTransactions -> + val latestBlockHeight = + networkHeight ?: runCatching { + backend.getMaxScannedHeight() + }.onFailure { + Twig.error(it) { "Failed to get max scanned height" } + }.getOrNull() + + allAccountTransactions.map { TransactionOverview.new(it, latestBlockHeight) } + } + } + // // Storage APIs // @@ -644,12 +669,16 @@ class SdkSynchronizer private constructor( // Not ready to be a public API; internal for testing only internal suspend fun createAccount( - seed: ByteArray, + accountName: String, + keySource: String?, + recoverUntil: BlockHeight?, + seed: FirstClassByteArray, treeState: TreeState, - recoverUntil: BlockHeight? - ): UnifiedSpendingKey { + ): AccountUsk { return runCatching { backend.createAccountAndGetSpendingKey( + accountName = accountName, + keySource = keySource, seed = seed, treeState = treeState, recoverUntil = recoverUntil @@ -663,6 +692,44 @@ class SdkSynchronizer private constructor( } } + override suspend fun importAccountByUfvk(setup: AccountImportSetup,): Account { + val chainTip: BlockHeight? = + when (val response = processor.downloader.getLatestBlockHeight()) { + is Response.Success -> { + Twig.info { "Chain tip for recovery until param fetched: ${response.result.value}" } + runCatching { response.result.toBlockHeight() }.getOrNull() + } + is Response.Failure -> { + Twig.error { + "Chain tip fetch for recovery until failed with: ${response.toThrowable()}" + } + null + } + } + + val loadedCheckpoint = + CheckpointTool.loadNearest( + context = context, + network = network, + chainTip ?: network.saplingActivationHeight + ) + val treeState: TreeState = loadedCheckpoint.treeState() + + return runCatching { + backend.importAccountUfvk( + recoverUntil = chainTip, + setup = setup, + treeState = treeState, + ).also { + refreshAccountsBus.emit(Unit) + } + }.onFailure { + Twig.error(it) { "Import account failed." } + }.getOrElse { + throw InitializeException.ImportAccountException(it) + } + } + override suspend fun getAccounts(): List { return runCatching { backend.getAccounts() @@ -991,22 +1058,20 @@ internal object DefaultSynchronizerFactory { @Suppress("LongParameterList") internal suspend fun defaultDerivedDataRepository( context: Context, - rustBackend: TypesafeBackend, databaseFile: File, checkpoint: Checkpoint, - seed: ByteArray?, - numberOfAccounts: Int, - recoverUntil: BlockHeight? + recoverUntil: BlockHeight?, + rustBackend: TypesafeBackend, + setup: AccountCreateSetup?, ): DerivedDataRepository = DbDerivedDataRepository( DerivedDataDb.new( - context, - rustBackend, - databaseFile, - checkpoint, - seed, - numberOfAccounts, - recoverUntil + context = context, + backend = rustBackend, + databaseFile = databaseFile, + checkpoint = checkpoint, + recoverUntil = recoverUntil, + setup = setup, ) ) 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 163e0742d..8f073fab9 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 @@ -7,7 +7,6 @@ import cash.z.ecc.android.sdk.WalletInitMode.RestoreWallet import cash.z.ecc.android.sdk.block.processor.CompactBlockProcessor import cash.z.ecc.android.sdk.exception.InitializeException import cash.z.ecc.android.sdk.ext.ZcashSdk -import cash.z.ecc.android.sdk.internal.Derivation import cash.z.ecc.android.sdk.internal.FastestServerFetcher import cash.z.ecc.android.sdk.internal.Files import cash.z.ecc.android.sdk.internal.SaplingParamTool @@ -17,6 +16,9 @@ import cash.z.ecc.android.sdk.internal.exchange.UsdExchangeRateFetcher 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.AccountBalance +import cash.z.ecc.android.sdk.model.AccountCreateSetup +import cash.z.ecc.android.sdk.model.AccountImportSetup +import cash.z.ecc.android.sdk.model.AccountUuid import cash.z.ecc.android.sdk.model.BlockHeight import cash.z.ecc.android.sdk.model.FastestServersResult import cash.z.ecc.android.sdk.model.ObserveFiatCurrencyResult @@ -80,7 +82,7 @@ interface Synchronizer { /** * A stream of wallet balances */ - val walletBalances: StateFlow?> + val walletBalances: StateFlow?> /** * The latest known USD/ZEC exchange rate, paired with the time it was queried. @@ -132,14 +134,22 @@ interface Synchronizer { val accountsFlow: Flow?> /** - * Measure connection quality and speed of given [servers]. + * Tells the wallet to track an account using a unified full viewing key. * - * @return a [Flow] of fastest servers which updates it's state during measurement stages + * Returns details about the imported account, including the unique account identifier for + * the newly-created wallet database entry. Unlike the other account creation APIs, no spending + * key is returned because the wallet has no information about the mnemonic phrase from which + * the UFVK was derived. + * + * @param purpose Metadata describing whether or not data required for spending should be tracked by the wallet + * @param setup The account's setup information. See [AccountImportSetup] for more. + * + * @return Account containing details about the imported account, including the unique account identifier for the + * newly-created wallet database entry + * + * @throws [InitializeException.ImportAccountException] in case of the operation failure */ - suspend fun getFastestServers( - context: Context, - servers: List - ): Flow + suspend fun importAccountByUfvk(setup: AccountImportSetup,): Account /** * Adds the next available account-level spend authority, given the current set of @@ -160,9 +170,9 @@ interface Synchronizer { * have been received by the currently-available account (in order to enable * automated account recovery). * - * @param seed the wallet's seed phrase. - * @param treeState - * @param recoverUntil + * @param recoverUntil An optional height at which the wallet should exit "recovery mode" + * @param setup The wallet's setup information. See [AccountCreateSetup] for more. + * @param treeState The tree state corresponding to the last block prior to the wallet's birthday height * * @return the newly created ZIP 316 account identifier, along with the binary * encoding of the `UnifiedSpendingKey` for the newly created account. @@ -172,12 +182,22 @@ interface Synchronizer { @Suppress("standard:no-consecutive-comments") /* Not ready to be a public API; internal for testing only suspend fun createAccount( - seed: ByteArray, + setup: AccountCreateSetup, treeState: TreeState, recoverUntil: BlockHeight? ): UnifiedSpendingKey */ + /** + * Measure connection quality and speed of given [servers]. + * + * @return a [Flow] of fastest servers which updates it's state during measurement stages + */ + suspend fun getFastestServers( + context: Context, + servers: List + ): Flow + /** * Gets the current unified address for the given account. * @@ -449,6 +469,14 @@ interface Synchronizer { */ suspend fun getTransactionOutputs(transactionOverview: TransactionOverview): List + /** + * Returns all transactions belonging to the given account UUID + * + * @param accountUuid The given account UUID + * @return Flow of transactions by the given account UUID + */ + suspend fun getTransactions(accountUuid: AccountUuid): Flow> + // // Error Handling // @@ -559,14 +587,14 @@ interface Synchronizer { * client wishes to change the server endpoint, the active synchronizer will need to be stopped and a new * instance created with a new value. * - * @param seed the wallet's seed phrase. This is required the first time a new wallet is set up. For - * subsequent calls, seed is only needed if [InitializerException.SeedRequired] is thrown. - * * @param birthday Block height representing the "birthday" of the wallet. When creating a new wallet, see * [BlockHeight.ofLatestCheckpoint]. When restoring an existing wallet, use block height that was first used * 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 setup An optional Account setup data that holds seed and other account related information. + * See [AccountCreateSetup] for more. + * * @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 @@ -585,13 +613,13 @@ interface Synchronizer { */ @Suppress("LongParameterList", "LongMethod") suspend fun new( - context: Context, - zcashNetwork: ZcashNetwork, alias: String = ZcashSdk.DEFAULT_ALIAS, - lightWalletEndpoint: LightWalletEndpoint, - seed: ByteArray?, birthday: BlockHeight?, - walletInitMode: WalletInitMode + context: Context, + lightWalletEndpoint: LightWalletEndpoint, + setup: AccountCreateSetup?, + walletInitMode: WalletInitMode, + zcashNetwork: ZcashNetwork, ): CloseableSynchronizer { val applicationContext = context.applicationContext @@ -652,9 +680,8 @@ interface Synchronizer { rustBackend = backend, databaseFile = coordinator.dataDbFile(zcashNetwork, alias), checkpoint = loadedCheckpoint, - seed = seed, - numberOfAccounts = Derivation.DEFAULT_NUMBER_OF_ACCOUNTS, recoverUntil = chainTip, + setup = setup, ) val encoder = DefaultSynchronizerFactory.defaultEncoder(backend, saplingParamTool, repository) @@ -673,6 +700,7 @@ interface Synchronizer { ) return SdkSynchronizer.new( + context = context.applicationContext, zcashNetwork = zcashNetwork, alias = alias, repository = repository, @@ -696,16 +724,24 @@ interface Synchronizer { @JvmStatic @Suppress("LongParameterList") fun newBlocking( - context: Context, - zcashNetwork: ZcashNetwork, alias: String = ZcashSdk.DEFAULT_ALIAS, - lightWalletEndpoint: LightWalletEndpoint, - seed: ByteArray?, birthday: BlockHeight?, - walletInitMode: WalletInitMode + context: Context, + lightWalletEndpoint: LightWalletEndpoint, + setup: AccountCreateSetup?, + walletInitMode: WalletInitMode, + zcashNetwork: ZcashNetwork, ): CloseableSynchronizer = runBlocking { - new(context, zcashNetwork, alias, lightWalletEndpoint, seed, birthday, walletInitMode) + new( + alias = alias, + birthday = birthday, + context = context, + lightWalletEndpoint = lightWalletEndpoint, + setup = setup, + walletInitMode = walletInitMode, + zcashNetwork = zcashNetwork, + ) } /** diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/block/processor/CompactBlockProcessor.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/block/processor/CompactBlockProcessor.kt index dbc8f1a67..a4e7fcd31 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/block/processor/CompactBlockProcessor.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/block/processor/CompactBlockProcessor.kt @@ -56,6 +56,7 @@ import cash.z.ecc.android.sdk.internal.repository.DerivedDataRepository import cash.z.ecc.android.sdk.internal.transaction.OutboundTransactionManager import cash.z.ecc.android.sdk.model.Account import cash.z.ecc.android.sdk.model.AccountBalance +import cash.z.ecc.android.sdk.model.AccountUuid import cash.z.ecc.android.sdk.model.BlockHeight import cash.z.ecc.android.sdk.model.PercentDecimal import cash.z.ecc.android.sdk.model.RawTransaction @@ -165,7 +166,7 @@ class CompactBlockProcessor internal constructor( private val _processorInfo = MutableStateFlow(ProcessorInfo(null, null, null)) private val _networkHeight = MutableStateFlow(null) private val _fullyScannedHeight = MutableStateFlow(null) - internal val walletBalances = MutableStateFlow?>(null) + internal val walletBalances = MutableStateFlow?>(null) private val processingMutex = Mutex() 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 34b485be8..fa90e0880 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 @@ -214,6 +214,11 @@ sealed class InitializeException(message: String, cause: Throwable? = null) : Sd cause ) + class ImportAccountException(cause: Throwable?) : InitializeException( + "Failed to import new account based on UFVK due to: ${cause?.message}", + cause + ) + class AlreadyInitializedException(cause: Throwable, dbPath: String) : InitializeException( "Failed to initialize the blocks table" + " because it already exists in $dbPath", diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/fixture/AccountCreateSetupFixture.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/fixture/AccountCreateSetupFixture.kt new file mode 100644 index 000000000..9abf69142 --- /dev/null +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/fixture/AccountCreateSetupFixture.kt @@ -0,0 +1,24 @@ +package cash.z.ecc.android.sdk.fixture + +import cash.z.ecc.android.sdk.model.AccountCreateSetup +import cash.z.ecc.android.sdk.model.FirstClassByteArray + +object AccountCreateSetupFixture { + const val ACCOUNT_NAME = AccountFixture.ACCOUNT_NAME + const val KEY_SOURCE = AccountFixture.KEY_SOURCE + + // This is the "Alice" wallet phrase from sdk-incubator-lib. + const val ALICE_SEED_PHRASE = + "wish puppy smile loan doll curve hole maze file ginger hair nose key relax knife witness" + + " cannon grab despair throw review deal slush frame" + + fun new( + accountName: String = ACCOUNT_NAME, + keySource: String? = KEY_SOURCE, + seed: ByteArray = ALICE_SEED_PHRASE.toByteArray(), + ) = AccountCreateSetup( + accountName, + keySource, + FirstClassByteArray(seed) + ) +} diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/fixture/AccountFixture.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/fixture/AccountFixture.kt index 6f3799ba0..3e679806b 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/fixture/AccountFixture.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/fixture/AccountFixture.kt @@ -1,6 +1,8 @@ package cash.z.ecc.android.sdk.fixture import cash.z.ecc.android.sdk.model.Account +import cash.z.ecc.android.sdk.model.AccountUuid +import cash.z.ecc.android.sdk.model.Zip32AccountIndex import java.nio.ByteBuffer import java.util.UUID @@ -11,20 +13,73 @@ import java.util.UUID * Note that these values are used in the automated tests only and are not passed across the JNI. */ object AccountFixture { - @Suppress("UnusedPrivateMember") + const val ZIP_32_ACCOUNT_INDEX = 0L + val ACCOUNT_UUID = UUID.fromString("01234567-89ab-cdef-0123-456789abcdef") - const val ZIP_32_ACCOUNT_INDEX = 0 + const val ACCOUNT_NAME = "Test Account" + const val UFVK = "ufvk1d68jqrx0q98rl0w8f5085y898x0p9z5k0sksqre87949w9494949" + const val KEY_SOURCE = "zcash" - fun new(accountId: Int = ZIP_32_ACCOUNT_INDEX) = - Account( - value = accountId + // This seed fingerprint belongs to Alice's seed phrase + @Suppress("MagicNumber") + val SEED_FINGER_PRINT = + byteArrayOf( + -105, + 67, + 68, + 101, + -93, + 45, + -17, + -27, + -69, + -46, + -39, + 11, + 84, + 69, + 85, + 123, + 49, + 78, + 82, + 78, + 127, + -96, + 70, + -112, + -118, + 40, + -113, + 120, + -93, + 101, + 56, + 33 ) + val HD_ACCOUNT_INDEX = Zip32AccountIndex.new(ZIP_32_ACCOUNT_INDEX) + + @Suppress("LongParameterList") + fun new( + accountName: String = ACCOUNT_NAME, + accountUuid: UUID = ACCOUNT_UUID, + hdAccountIndex: Zip32AccountIndex = HD_ACCOUNT_INDEX, + keySource: String = KEY_SOURCE, + seedFingerprint: ByteArray = SEED_FINGER_PRINT, + ufvk: String = UFVK + ) = Account( + accountName = accountName, + accountUuid = AccountUuid.new(accountUuid.toByteArray()), + hdAccountIndex = hdAccountIndex, + keySource = keySource, + seedFingerprint = seedFingerprint, + ufvk = ufvk + ) } private const val UUID_V4_BYTE_SIZE = 16 // This provides us with a way to convert [UUID] to [ByteArray] -@Suppress("UnusedPrivateMember") private fun UUID.toByteArray(): ByteArray = ByteBuffer .allocate(UUID_V4_BYTE_SIZE) 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..21d0f7450 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 @@ -1,16 +1,16 @@ package cash.z.ecc.android.sdk.internal import cash.z.ecc.android.sdk.internal.model.JniUnifiedSpendingKey -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 +import cash.z.ecc.android.sdk.model.Zip32AccountIndex fun Derivation.deriveUnifiedAddress( seed: ByteArray, network: ZcashNetwork, - account: Account -): String = deriveUnifiedAddress(seed, network.id, account.value) + accountIndex: Zip32AccountIndex +): String = deriveUnifiedAddress(seed, network.id, accountIndex.index) fun Derivation.deriveUnifiedAddress( viewingKey: String, @@ -20,8 +20,13 @@ fun Derivation.deriveUnifiedAddress( fun Derivation.deriveUnifiedSpendingKey( seed: ByteArray, network: ZcashNetwork, - account: Account -): UnifiedSpendingKey = UnifiedSpendingKey(deriveUnifiedSpendingKey(seed, network.id, account.value)) + accountIndex: Zip32AccountIndex +): UnifiedSpendingKey = + UnifiedSpendingKey( + JniUnifiedSpendingKey( + bytes = deriveUnifiedSpendingKey(seed, network.id, accountIndex.index) + ) + ) fun Derivation.deriveUnifiedFullViewingKey( usk: UnifiedSpendingKey, @@ -30,8 +35,7 @@ fun Derivation.deriveUnifiedFullViewingKey( UnifiedFullViewingKey( deriveUnifiedFullViewingKey( JniUnifiedSpendingKey( - usk.account.value, - usk.copyBytes() + bytes = usk.copyBytes() ), network.id ) @@ -57,5 +61,5 @@ fun Derivation.deriveArbitraryAccountKeyTypesafe( contextString: ByteArray, seed: ByteArray, network: ZcashNetwork, - account: Account -): ByteArray = deriveArbitraryAccountKey(contextString, seed, network.id, account.value) + accountIndex: Zip32AccountIndex +): ByteArray = deriveArbitraryAccountKey(contextString, seed, network.id, accountIndex.index) diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/TypesafeBackend.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/TypesafeBackend.kt index 810f98b1d..0b00839ce 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/TypesafeBackend.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/TypesafeBackend.kt @@ -13,6 +13,8 @@ import cash.z.ecc.android.sdk.internal.model.TreeState import cash.z.ecc.android.sdk.internal.model.WalletSummary import cash.z.ecc.android.sdk.internal.model.ZcashProtocol import cash.z.ecc.android.sdk.model.Account +import cash.z.ecc.android.sdk.model.AccountImportSetup +import cash.z.ecc.android.sdk.model.AccountUsk import cash.z.ecc.android.sdk.model.BlockHeight import cash.z.ecc.android.sdk.model.FirstClassByteArray import cash.z.ecc.android.sdk.model.Proposal @@ -27,10 +29,18 @@ internal interface TypesafeBackend { suspend fun getAccounts(): List suspend fun createAccountAndGetSpendingKey( - seed: ByteArray, + accountName: String, + keySource: String?, + seed: FirstClassByteArray, treeState: TreeState, recoverUntil: BlockHeight? - ): UnifiedSpendingKey + ): AccountUsk + + suspend fun importAccountUfvk( + recoverUntil: BlockHeight?, + setup: AccountImportSetup, + treeState: TreeState, + ): Account suspend fun proposeTransferFromUri( account: Account, @@ -90,7 +100,7 @@ internal interface TypesafeBackend { ): String? @Throws(InitializeException::class) - suspend fun initDataDb(seed: ByteArray?) + suspend fun initDataDb(seed: FirstClassByteArray?) /** * @throws RuntimeException as a common indicator of the operation failure 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..6e808edf4 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 @@ -14,6 +14,9 @@ import cash.z.ecc.android.sdk.internal.model.TreeState import cash.z.ecc.android.sdk.internal.model.WalletSummary import cash.z.ecc.android.sdk.internal.model.ZcashProtocol import cash.z.ecc.android.sdk.model.Account +import cash.z.ecc.android.sdk.model.AccountImportSetup +import cash.z.ecc.android.sdk.model.AccountPurpose +import cash.z.ecc.android.sdk.model.AccountUsk import cash.z.ecc.android.sdk.model.BlockHeight import cash.z.ecc.android.sdk.model.FirstClassByteArray import cash.z.ecc.android.sdk.model.Proposal @@ -27,29 +30,67 @@ 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.new(it) } override suspend fun createAccountAndGetSpendingKey( - seed: ByteArray, + accountName: String, + keySource: String?, + seed: FirstClassByteArray, treeState: TreeState, recoverUntil: BlockHeight? - ): UnifiedSpendingKey { - return UnifiedSpendingKey( + ): AccountUsk { + return AccountUsk.new( backend.createAccount( - seed = seed, + accountName = accountName, + keySource = keySource, + seed = seed.byteArray, treeState = treeState.encoded, recoverUntil = recoverUntil?.value ) ) } + override suspend fun importAccountUfvk( + recoverUntil: BlockHeight?, + setup: AccountImportSetup, + treeState: TreeState, + ): Account { + return Account.new( + jniAccount = + when (setup.purpose) { + is AccountPurpose.Spending -> + backend.importAccountUfvk( + accountName = setup.accountName, + keySource = setup.keySource, + purpose = setup.purpose.value, + recoverUntil = recoverUntil?.value, + treeState = treeState.encoded, + ufvk = setup.ufvk.encoding, + seedFingerprint = setup.purpose.seedFingerprint, + zip32AccountIndex = setup.purpose.zip32AccountIndex?.index, + ) + AccountPurpose.ViewOnly -> + backend.importAccountUfvk( + accountName = setup.accountName, + keySource = setup.keySource, + purpose = setup.purpose.value, + recoverUntil = recoverUntil?.value, + treeState = treeState.encoded, + ufvk = setup.ufvk.encoding, + seedFingerprint = null, + zip32AccountIndex = null, + ) + } + ) + } + override suspend fun proposeTransferFromUri( account: Account, uri: String ): Proposal = Proposal.fromUnsafe( backend.proposeTransferFromUri( - account.value, + account.accountUuid.value, uri ) ) @@ -63,7 +104,7 @@ internal class TypesafeBackendImpl(private val backend: Backend) : TypesafeBacke ): Proposal = Proposal.fromUnsafe( backend.proposeTransfer( - account.value, + account.accountUuid.value, to, value, memo @@ -77,7 +118,7 @@ internal class TypesafeBackendImpl(private val backend: Backend) : TypesafeBacke transparentReceiver: String? ): Proposal? = backend.proposeShielding( - account.value, + account.accountUuid.value, shieldingThreshold, memo, transparentReceiver @@ -98,14 +139,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.value) }.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.value) } override fun getBranchIdForHeight(height: BlockHeight): Long { @@ -166,8 +207,8 @@ internal class TypesafeBackendImpl(private val backend: Backend) : TypesafeBacke outputIndex = outputIndex ) - override suspend fun initDataDb(seed: ByteArray?) { - val ret = backend.initDataDb(seed) + override suspend fun initDataDb(seed: FirstClassByteArray?) { + val ret = backend.initDataDb(seed?.byteArray) when (ret) { 2 -> throw InitializeException.SeedNotRelevant 1 -> throw InitializeException.SeedRequired 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..1e7b97726 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,9 +1,9 @@ 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 +import cash.z.ecc.android.sdk.model.Zip32AccountIndex import cash.z.ecc.android.sdk.tool.DerivationTool internal class TypesafeDerivationToolImpl(private val derivation: Derivation) : DerivationTool { @@ -21,14 +21,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: Zip32AccountIndex + ): UnifiedSpendingKey = derivation.deriveUnifiedSpendingKey(seed, network, accountIndex) override suspend fun deriveUnifiedAddress( seed: ByteArray, network: ZcashNetwork, - account: Account - ): String = derivation.deriveUnifiedAddress(seed, network, account) + accountIndex: Zip32AccountIndex + ): String = derivation.deriveUnifiedAddress(seed, network, accountIndex) override suspend fun deriveUnifiedAddress( viewingKey: String, @@ -44,6 +44,6 @@ internal class TypesafeDerivationToolImpl(private val derivation: Derivation) : contextString: ByteArray, seed: ByteArray, network: ZcashNetwork, - account: Account - ): ByteArray = derivation.deriveArbitraryAccountKeyTypesafe(contextString, seed, network, account) + accountIndex: Zip32AccountIndex + ): ByteArray = derivation.deriveArbitraryAccountKeyTypesafe(contextString, seed, network, accountIndex) } diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/AllTransactionView.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/AllTransactionView.kt index 33ca5f46f..586bca81b 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/AllTransactionView.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/AllTransactionView.kt @@ -7,6 +7,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase import cash.z.ecc.android.sdk.internal.db.CursorParser import cash.z.ecc.android.sdk.internal.db.queryAndMap import cash.z.ecc.android.sdk.internal.model.DbTransactionOverview +import cash.z.ecc.android.sdk.model.AccountUuid import cash.z.ecc.android.sdk.model.BlockHeight import cash.z.ecc.android.sdk.model.FirstClassByteArray import cash.z.ecc.android.sdk.model.Zatoshi @@ -75,6 +76,17 @@ internal class AllTransactionView( SENT_TRANSACTION_RECOGNITION_VALUE ) + /** + * Get all transactions belonging to the given account UUID + */ + private val SELECTION_TRX_BY_ACCOUNT_UUID = + String.format( + Locale.ROOT, + // $NON-NLS + "%s = ?", + AllTransactionViewDefinition.COLUMN_BLOB_ACCOUNT_UUID, + ) + private val SELECTION_RAW_IS_NULL = String.format( Locale.ROOT, @@ -159,6 +171,16 @@ internal class AllTransactionView( cursorParser = cursorParser ) + fun getTransactions(accountUuid: AccountUuid) = + sqliteDatabase.queryAndMap( + table = AllTransactionViewDefinition.VIEW_NAME, + columns = COLUMNS, + orderBy = ORDER_BY, + selection = SELECTION_TRX_BY_ACCOUNT_UUID, + selectionArgs = arrayOf(accountUuid.value), + cursorParser = cursorParser + ) + fun getUnminedUnexpiredTransactions(blockHeight: BlockHeight) = sqliteDatabase.queryAndMap( table = AllTransactionViewDefinition.VIEW_NAME, @@ -225,4 +247,6 @@ internal object AllTransactionViewDefinition { const val COLUMN_INTEGER_BLOCK_TIME = "block_time" // $NON-NLS const val COLUMN_BOOLEAN_IS_SHIELDING = "is_shielding" // $NON-NLS + + const val COLUMN_BLOB_ACCOUNT_UUID = "account_uuid" // $NON-NLS } diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/DbDerivedDataRepository.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/DbDerivedDataRepository.kt index 2477fa392..0f9c164e7 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/DbDerivedDataRepository.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/DbDerivedDataRepository.kt @@ -3,6 +3,7 @@ package cash.z.ecc.android.sdk.internal.db.derived import cash.z.ecc.android.sdk.internal.model.DbTransactionOverview import cash.z.ecc.android.sdk.internal.model.EncodedTransaction import cash.z.ecc.android.sdk.internal.repository.DerivedDataRepository +import cash.z.ecc.android.sdk.model.AccountUuid import cash.z.ecc.android.sdk.model.BlockHeight import cash.z.ecc.android.sdk.model.FirstClassByteArray import cash.z.ecc.android.sdk.model.TransactionRecipient @@ -41,6 +42,9 @@ internal class DbDerivedDataRepository( override suspend fun getTransactionCount() = derivedDataDb.transactionTable.count() + override suspend fun getTransactions(accountUuid: AccountUuid) = + invalidatingFlow.map { derivedDataDb.allTransactionView.getTransactions(accountUuid).toList() } + override fun invalidate() { invalidatingFlow.value = UUID.randomUUID() } diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/DerivedDataDb.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/DerivedDataDb.kt index 93fa14ede..563e412b7 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/DerivedDataDb.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/DerivedDataDb.kt @@ -2,12 +2,14 @@ package cash.z.ecc.android.sdk.internal.db.derived import android.content.Context import androidx.sqlite.db.SupportSQLiteDatabase +import cash.z.ecc.android.sdk.exception.InitializeException import cash.z.ecc.android.sdk.internal.NoBackupContextWrapper import cash.z.ecc.android.sdk.internal.SdkDispatchers import cash.z.ecc.android.sdk.internal.Twig import cash.z.ecc.android.sdk.internal.TypesafeBackend import cash.z.ecc.android.sdk.internal.db.ReadOnlySupportSqliteOpenHelper import cash.z.ecc.android.sdk.internal.model.Checkpoint +import cash.z.ecc.android.sdk.model.AccountCreateSetup import cash.z.ecc.android.sdk.model.BlockHeight import kotlinx.coroutines.withContext import java.io.File @@ -42,11 +44,10 @@ internal class DerivedDataDb private constructor( backend: TypesafeBackend, databaseFile: File, checkpoint: Checkpoint, - seed: ByteArray?, - numberOfAccounts: Int, - recoverUntil: BlockHeight? + recoverUntil: BlockHeight?, + setup: AccountCreateSetup?, ): DerivedDataDb { - backend.initDataDb(seed) + backend.initDataDb(setup?.seed) val database = ReadOnlySupportSqliteOpenHelper.openExistingDatabaseAsReadOnly( @@ -60,22 +61,22 @@ internal class DerivedDataDb private constructor( val dataDb = DerivedDataDb(database) - // If a seed is provided, fill in the accounts. - seed?.let { checkedSeed -> - val missingAccounts = numberOfAccounts - backend.getAccounts().count() - require(missingAccounts >= 0) { - "Unexpected number of accounts: $missingAccounts" - } - repeat(missingAccounts) { - runCatching { - backend.createAccountAndGetSpendingKey( - seed = checkedSeed, - treeState = checkpoint.treeState(), - recoverUntil = recoverUntil - ) - }.onFailure { - Twig.error(it) { "Create account failed." } - } + // If a seed is provided, and the wallet database does not contain the primary seed account, we approach + // adding it. Note that this is subject to refactoring once we support a fully multi-account wallet. + if (setup != null && backend.getAccounts().isEmpty()) { + runCatching { + backend.createAccountAndGetSpendingKey( + accountName = setup.accountName, + keySource = setup.keySource, + recoverUntil = recoverUntil, + seed = setup.seed, + treeState = checkpoint.treeState() + ) + }.onFailure { + Twig.error(it) { "Create account failed." } + throw InitializeException.CreateAccountException(it) + }.onSuccess { + Twig.debug { "The creation of account: $it was successful." } } } diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/TxOutputsView.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/TxOutputsView.kt index cdb77a8b4..42d4a1e49 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/TxOutputsView.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/TxOutputsView.kt @@ -3,7 +3,6 @@ package cash.z.ecc.android.sdk.internal.db.derived import androidx.sqlite.db.SupportSQLiteDatabase import cash.z.ecc.android.sdk.internal.db.queryAndMap import cash.z.ecc.android.sdk.internal.model.OutputProperties -import cash.z.ecc.android.sdk.model.Account import cash.z.ecc.android.sdk.model.FirstClassByteArray import cash.z.ecc.android.sdk.model.TransactionRecipient import java.util.Locale @@ -27,7 +26,7 @@ internal class TxOutputsView(private val sqliteDatabase: SupportSQLiteDatabase) private val PROJECTION_RECIPIENT = arrayOf( TxOutputsViewDefinition.COLUMN_STRING_TO_ADDRESS, - TxOutputsViewDefinition.COLUMN_INTEGER_TO_ACCOUNT + TxOutputsViewDefinition.COLUMN_BLOB_TO_ACCOUNT ) private val SELECT_BY_TRANSACTION_ID_AND_NOT_CHANGE = @@ -67,15 +66,13 @@ internal class TxOutputsView(private val sqliteDatabase: SupportSQLiteDatabase) selectionArgs = arrayOf(transactionId.byteArray), orderBy = ORDER_BY, cursorParser = { - val toAccountIndex = it.getColumnIndex(TxOutputsViewDefinition.COLUMN_INTEGER_TO_ACCOUNT) + val toAccountIndex = it.getColumnIndex(TxOutputsViewDefinition.COLUMN_BLOB_TO_ACCOUNT) val toAddressIndex = it.getColumnIndex(TxOutputsViewDefinition.COLUMN_STRING_TO_ADDRESS) if (!it.isNull(toAccountIndex)) { - // TODO [#1644]: Refactor Account ZIP32 index across SDK - // TODO [#1644]: https://github.com/Electric-Coin-Company/zcash-android-wallet-sdk/issues/1644 - TransactionRecipient.Account(0) + TransactionRecipient.RecipientAccount(accountUuid = it.getBlob(toAccountIndex)) } else { - TransactionRecipient.Address(it.getString(toAddressIndex)) + TransactionRecipient.RecipientAddress(addressValue = it.getString(toAddressIndex)) } } ) @@ -94,7 +91,7 @@ internal object TxOutputsViewDefinition { const val COLUMN_STRING_TO_ADDRESS = "to_address" // $NON-NLS - const val COLUMN_INTEGER_TO_ACCOUNT = "to_account_id" // $NON-NLS + const val COLUMN_BLOB_TO_ACCOUNT = "to_account_uuid" // $NON-NLS const val COLUMN_INTEGER_VALUE = "value" // $NON-NLS diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/WalletSummary.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/WalletSummary.kt index 94c12e01d..51dccd373 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/WalletSummary.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/WalletSummary.kt @@ -1,11 +1,11 @@ package cash.z.ecc.android.sdk.internal.model -import cash.z.ecc.android.sdk.model.Account import cash.z.ecc.android.sdk.model.AccountBalance +import cash.z.ecc.android.sdk.model.AccountUuid import cash.z.ecc.android.sdk.model.BlockHeight internal data class WalletSummary( - val accountBalances: Map, + val accountBalances: Map, val chainTipHeight: BlockHeight, val fullyScannedHeight: BlockHeight, val scanProgress: ScanProgress, @@ -16,9 +16,10 @@ internal data class WalletSummary( fun new(jni: JniWalletSummary): WalletSummary { return WalletSummary( accountBalances = - jni.accountBalances.associateBy({ Account(it.account) }, { - AccountBalance.new(it) - }), + jni.accountBalances.associateBy( + { AccountUuid.new(it.accountUuid) }, + { AccountBalance.new(it) } + ), chainTipHeight = BlockHeight(jni.chainTipHeight), fullyScannedHeight = BlockHeight(jni.fullyScannedHeight), scanProgress = ScanProgress.new(jni), diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/repository/DerivedDataRepository.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/repository/DerivedDataRepository.kt index 0569bc002..b17c84fa5 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/repository/DerivedDataRepository.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/repository/DerivedDataRepository.kt @@ -3,6 +3,7 @@ package cash.z.ecc.android.sdk.internal.repository import cash.z.ecc.android.sdk.internal.model.DbTransactionOverview import cash.z.ecc.android.sdk.internal.model.EncodedTransaction import cash.z.ecc.android.sdk.internal.model.OutputProperties +import cash.z.ecc.android.sdk.model.AccountUuid import cash.z.ecc.android.sdk.model.BlockHeight import cash.z.ecc.android.sdk.model.FirstClassByteArray import cash.z.ecc.android.sdk.model.TransactionRecipient @@ -79,6 +80,8 @@ internal interface DerivedDataRepository { val allTransactions: Flow> + suspend fun getTransactions(accountUuid: AccountUuid): Flow> + fun getOutputProperties(transactionId: FirstClassByteArray): Flow fun getRecipients(transactionId: FirstClassByteArray): Flow diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/OutboundTransactionManagerImpl.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/OutboundTransactionManagerImpl.kt index 590b8b9b2..f090d624f 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/OutboundTransactionManagerImpl.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/OutboundTransactionManagerImpl.kt @@ -31,19 +31,21 @@ internal class OutboundTransactionManagerImpl( memo.toByteArray() } return when (recipient) { - is TransactionRecipient.Account -> { + is TransactionRecipient.RecipientAccount -> { encoder.createShieldingTransaction( - usk, - recipient, - memoBytes + usk = usk, + account = account, + recipient = recipient, + memo = memoBytes ) } - is TransactionRecipient.Address -> { + is TransactionRecipient.RecipientAddress -> { encoder.createTransaction( - usk, - amount, - recipient, - memoBytes + usk = usk, + account = account, + amount = amount, + recipient = recipient, + memo = memoBytes ) } } diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/TransactionEncoder.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/TransactionEncoder.kt index 9e3c50057..b839af28c 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/TransactionEncoder.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/TransactionEncoder.kt @@ -25,6 +25,7 @@ internal interface TransactionEncoder { */ suspend fun createTransaction( usk: UnifiedSpendingKey, + account: Account, amount: Zatoshi, recipient: TransactionRecipient, memo: ByteArray? = byteArrayOf() @@ -38,6 +39,7 @@ internal interface TransactionEncoder { */ suspend fun createShieldingTransaction( usk: UnifiedSpendingKey, + account: Account, recipient: TransactionRecipient, memo: ByteArray? = byteArrayOf() ): EncodedTransaction diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/TransactionEncoderImpl.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/TransactionEncoderImpl.kt index fbb8eaaba..d74b5bfc8 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/TransactionEncoderImpl.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/TransactionEncoderImpl.kt @@ -47,25 +47,39 @@ internal class TransactionEncoderImpl( */ override suspend fun createTransaction( usk: UnifiedSpendingKey, + account: Account, amount: Zatoshi, recipient: TransactionRecipient, memo: ByteArray? ): EncodedTransaction { - require(recipient is TransactionRecipient.Address) + require(recipient is TransactionRecipient.RecipientAddress) - val transactionId = createSpend(usk, amount, recipient.addressValue, memo) + val transactionId = + createSpend( + account = account, + amount = amount, + memo = memo, + toAddress = recipient.addressValue, + usk = usk, + ) return repository.findEncodedTransactionByTxId(transactionId) ?: throw TransactionEncoderException.TransactionNotFoundException(transactionId) } override suspend fun createShieldingTransaction( usk: UnifiedSpendingKey, + account: Account, recipient: TransactionRecipient, memo: ByteArray? ): EncodedTransaction { - require(recipient is TransactionRecipient.Account) + require(recipient is TransactionRecipient.RecipientAccount) - val transactionId = createShieldingSpend(usk, memo) + val transactionId = + createShieldingSpend( + account = account, + memo = memo, + usk = usk, + ) return repository.findEncodedTransactionByTxId(transactionId) ?: throw TransactionEncoderException.TransactionNotFoundException(transactionId) } @@ -255,10 +269,11 @@ internal class TransactionEncoderImpl( * failed. */ private suspend fun createSpend( - usk: UnifiedSpendingKey, + account: Account, amount: Zatoshi, + memo: ByteArray? = null, toAddress: String, - memo: ByteArray? = null + usk: UnifiedSpendingKey, ): FirstClassByteArray { Twig.debug { "creating transaction to spend $amount zatoshi to" + @@ -270,7 +285,7 @@ internal class TransactionEncoderImpl( Twig.debug { "params exist! attempting to send..." } val proposal = backend.proposeTransfer( - usk.account, + account, toAddress, amount.value, memo @@ -286,14 +301,15 @@ internal class TransactionEncoderImpl( } private suspend fun createShieldingSpend( + account: Account, + memo: ByteArray? = null, usk: UnifiedSpendingKey, - memo: ByteArray? = null ): FirstClassByteArray { return runCatching { saplingParamTool.ensureParams(saplingParamTool.properties.paramsDirectory) Twig.debug { "params exist! attempting to shield..." } val proposal = - backend.proposeShielding(usk.account, SHIELDING_THRESHOLD, memo) + backend.proposeShielding(account, SHIELDING_THRESHOLD, memo) ?: throw SdkException( "Insufficient balance (have 0, need $SHIELDING_THRESHOLD including fee)", null 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 571aa73bb..6b3466c0a 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,12 +1,117 @@ package cash.z.ecc.android.sdk.model +import cash.z.ecc.android.sdk.internal.jni.JNI_ACCOUNT_SEED_FP_BYTES_SIZE +import cash.z.ecc.android.sdk.internal.model.JniAccount + /** - * A [ZIP 32](https://zips.z.cash/zip-0032) account index. + * Unique identifier for a specific account tracked by a `Synchronizer`. + * + * 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 value A 0-based account index. Must be >= 0. + * @param accountUuid The account identifier. + * @param ufvk The account's Unified Full Viewing Key, if any. + * @param accountName A human-readable name for the account. This is visible to the wallet user, and + * the wallet app may have obtained it from them. + * @param keySource A string identifier or other metadata describing the location of the spending + * key corresponding to the account's UFVK. This is set internally by the wallet app based on + * its private enumeration of spending methods it supports. + * @param seedFingerprint The seed fingerprint. Must be length 16. + * @param hdAccountIndex ZIP 32 account index */ -data class Account(val value: Int) { +data class Account internal constructor( + val accountUuid: AccountUuid, + val ufvk: String?, + val accountName: String?, + val keySource: String?, + val seedFingerprint: ByteArray?, + val hdAccountIndex: Zip32AccountIndex?, +) { init { - require(value >= 0) { "Account index must be >= 0 but actually is $value" } + seedFingerprint?.let { + require(seedFingerprint.size == JNI_ACCOUNT_SEED_FP_BYTES_SIZE) { + "Seed fingerprint must be 32 bytes" + } + } + } + + companion object { + fun new(jniAccount: JniAccount): Account = + Account( + accountUuid = AccountUuid.new(jniAccount.accountUuid), + ufvk = jniAccount.ufvk, + accountName = jniAccount.accountName, + keySource = jniAccount.keySource, + seedFingerprint = jniAccount.seedFingerprint, + // We use -1L to represent NULL across JNI + hdAccountIndex = + if (jniAccount.hdAccountIndex == -1L) { + null + } else { + Zip32AccountIndex.new(jniAccount.hdAccountIndex) + } + ) + + fun new(accountUuid: AccountUuid): Account = + Account( + accountUuid = accountUuid, + ufvk = null, + accountName = null, + keySource = null, + seedFingerprint = null, + hdAccountIndex = null + ) + } + + override fun toString(): String { + return "Account(accountUuid=$accountUuid," + + " ufvk length=${ufvk?.length}," + + " accountName=$accountName," + + " keySource=$keySource," + + " seedFingerprint size=${seedFingerprint?.size}," + + " hdAccountIndex=$hdAccountIndex)" + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Account + + if (accountUuid != other.accountUuid) return false + if (ufvk != other.ufvk) return false + if (accountName != other.accountName) return false + if (keySource != other.keySource) return false + if (seedFingerprint != null) { + if (other.seedFingerprint == null) return false + if (!seedFingerprint.contentEquals(other.seedFingerprint)) return false + } else if (other.seedFingerprint != null) { + return false + } + if (hdAccountIndex != other.hdAccountIndex) return false + + return true + } + + override fun hashCode(): Int { + var result = accountUuid.hashCode() + result = 31 * result + (ufvk?.hashCode() ?: 0) + result = 31 * result + (accountName?.hashCode() ?: 0) + result = 31 * result + (keySource?.hashCode() ?: 0) + result = 31 * result + (seedFingerprint?.contentHashCode() ?: 0) + result = 31 * result + (hdAccountIndex?.hashCode() ?: 0) + return result } } diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/AccountCreateSetup.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/AccountCreateSetup.kt new file mode 100644 index 000000000..5f6ec35ac --- /dev/null +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/AccountCreateSetup.kt @@ -0,0 +1,18 @@ +package cash.z.ecc.android.sdk.model + +/** + * Wrapper for the create account API based on seed phrase + * + * @param accountName A human-readable name for the account. This will be visible to the wallet + * user, and the wallet app may obtain it from them. + * @param keySource A string identifier or other metadata describing the source of the seed. This + * should be set internally by the wallet app based on its private enumeration of spending + * methods it supports. + * @param seed the wallet's seed phrase. This is required the first time a new wallet is set up. For + * subsequent calls, seed is only needed if [InitializerException.SeedRequired] is thrown. + */ +data class AccountCreateSetup( + val accountName: String, + val keySource: String?, + val seed: FirstClassByteArray, +) diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/AccountImportSetup.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/AccountImportSetup.kt new file mode 100644 index 000000000..5709152d3 --- /dev/null +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/AccountImportSetup.kt @@ -0,0 +1,21 @@ +package cash.z.ecc.android.sdk.model + +/** + * Wrapper for the import account API based on viewing key. Set both [seedFingerprint] and [zip32AccountIndex] null + * when using [AccountPurpose.ViewOnly]. + * + * @param accountName A human-readable name for the account. This will be visible to the wallet + * user, and the wallet app may obtain it from them. + * @param keySource A string identifier or other metadata describing the location of the spending + * key corresponding to the provided UFVK. This should be set internally by the wallet app + * based on its private enumeration of spending methods it supports. + * @param purpose Metadata describing whether or not data required for spending should be tracked by the wallet + * @param ufvk The UFVK used to detect transactions involving the account + * account's UFVK. + */ +data class AccountImportSetup( + val accountName: String, + val keySource: String?, + val purpose: AccountPurpose, + val ufvk: UnifiedFullViewingKey, +) diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/AccountPurpose.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/AccountPurpose.kt new file mode 100644 index 000000000..e0611679d --- /dev/null +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/AccountPurpose.kt @@ -0,0 +1,31 @@ +package cash.z.ecc.android.sdk.model + +/** + * An enumeration used to control what information is tracked by the wallet for notes received by a given account + */ +sealed class AccountPurpose { + /** + * Constant value that uniquely identifies this enum across FFI + */ + abstract val value: Int + + /** + * For spending accounts, the wallet will track information needed to spend received notes + * + * @param seedFingerprint The [ZIP 32 seed fingerprint](https://zips.z.cash/zip-0032#seed-fingerprints) + * @param zip32AccountIndex The ZIP 32 account-level component of the HD derivation path at which to derive the + */ + data class Spending( + val seedFingerprint: ByteArray?, + val zip32AccountIndex: Zip32AccountIndex?, + ) : AccountPurpose() { + override val value = 0 + } + + /** + * For view-only accounts, the wallet will not track spend information + */ + data object ViewOnly : AccountPurpose() { + override val value = 1 + } +} diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/AccountUsk.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/AccountUsk.kt new file mode 100644 index 000000000..c00d03c95 --- /dev/null +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/AccountUsk.kt @@ -0,0 +1,74 @@ +package cash.z.ecc.android.sdk.model + +import cash.z.ecc.android.sdk.internal.jni.RustBackend +import cash.z.ecc.android.sdk.internal.model.JniAccountUsk + +/** + * Account related model class providing a [ZIP 316](https://zips.z.cash/zip-0316) Unified Spending Key. + * + * This is the spend authority for an account under the wallet's seed. + * + * An instance of this class contains all of the per-pool spending keys that could be + * derived at the time of its creation. As such, it is not suitable for long-term storage, + * export/import, or backup purposes. + */ +class AccountUsk private constructor( + /** + * The account UUID used to derive this key. + */ + val accountUuid: AccountUuid, + /** + * The binary encoding of the [ZIP 316](https://zips.z.cash/zip-0316) Unified Spending + * 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 + * backend. Wallets **MUST NOT** allow this encoding to be exported or imported. + */ + private val bytes: FirstClassByteArray +) { + internal constructor(uskJni: JniAccountUsk) : this( + AccountUuid.new(uskJni.accountUuid), + FirstClassByteArray(uskJni.bytes.copyOf()) + ) + + /** + * The binary encoding of the [ZIP 316](https://zips.z.cash/zip-0316) Unified Spending + * 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 + * backend. Wallets **MUST NOT** allow this encoding to be exported or imported. + */ + fun copyBytes() = bytes.byteArray.copyOf() + + // Override to prevent leaking key to logs + override fun toString() = "AccountUsk(account=$accountUuid, bytes=***)" + + companion object { + /** + * This method may fail if the [bytes] no longer represent a valid key. A key could become invalid due to + * network upgrades or other internal changes. If a non-successful result is returned, clients are expected + * to use [DerivationTool.deriveUnifiedSpendingKey] to regenerate the key from the seed. + * + * @return A validated AccountUsk. + */ + suspend fun new( + accountUuid: AccountUuid, + bytes: ByteArray + ): Result { + val bytesCopy = bytes.copyOf() + RustBackend.loadLibrary() + return runCatching { + require(RustBackend.validateUnifiedSpendingKey(bytesCopy)) + AccountUsk(accountUuid, FirstClassByteArray(bytesCopy)) + } + } + + fun new(jniAccountUsk: JniAccountUsk): AccountUsk = + AccountUsk( + accountUuid = AccountUuid.new(jniAccountUsk.accountUuid), + bytes = FirstClassByteArray(jniAccountUsk.bytes) + ) + } +} diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/AccountUuid.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/AccountUuid.kt new file mode 100644 index 000000000..a5ed1db73 --- /dev/null +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/AccountUuid.kt @@ -0,0 +1,35 @@ +package cash.z.ecc.android.sdk.model + +import cash.z.ecc.android.sdk.internal.jni.JNI_ACCOUNT_UUID_BYTES_SIZE + +/** + * Typesafe wrapper class for the account UUID identifier. + * + * @param value The account identifier. Must be length 16. + */ +data class AccountUuid internal constructor( + val value: ByteArray, +) { + init { + require(value.size == JNI_ACCOUNT_UUID_BYTES_SIZE) { + "Account UUID must be 16 bytes" + } + } + + companion object { + fun new(uuid: ByteArray) = AccountUuid(uuid) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as AccountUuid + + return value.contentEquals(other.value) + } + + override fun hashCode(): Int { + return value.contentHashCode() + } +} diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/PendingTransaction.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/PendingTransaction.kt index b054fe464..19ccf391a 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/PendingTransaction.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/PendingTransaction.kt @@ -27,12 +27,12 @@ data class PendingTransaction internal constructor( } sealed class TransactionRecipient { - data class Address(val addressValue: String) : TransactionRecipient() { - override fun toString() = "TransactionRecipient.Address" + data class RecipientAddress(val addressValue: String) : TransactionRecipient() { + override fun toString() = "TransactionRecipient.RecipientAddress" } - data class Account(val accountId: Int) : TransactionRecipient() { - override fun toString() = "TransactionRecipient.Account" + data class RecipientAccount(val accountUuid: ByteArray) : TransactionRecipient() { + override fun toString() = "TransactionRecipient.RecipientAccount" } companion object 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..b068403ca 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 @@ -13,13 +13,9 @@ import cash.z.ecc.android.sdk.internal.model.JniUnifiedSpendingKey * export/import, or backup purposes. */ class UnifiedSpendingKey private constructor( - /** - * The [ZIP 32](https://zips.z.cash/zip-0032) account index used to derive this key. - */ - val account: Account, /** * The binary encoding of the [ZIP 316](https://zips.z.cash/zip-0316) Unified Spending - * Key for [account]. + * Key for the selected account. * * 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 +24,6 @@ class UnifiedSpendingKey private constructor( private val bytes: FirstClassByteArray ) { internal constructor(uskJni: JniUnifiedSpendingKey) : this( - Account(uskJni.account), FirstClassByteArray(uskJni.bytes.copyOf()) ) @@ -43,25 +38,7 @@ class UnifiedSpendingKey private constructor( fun copyBytes() = bytes.byteArray.copyOf() // Override to prevent leaking key to logs - override fun toString() = "UnifiedSpendingKey(account=$account)" - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as UnifiedSpendingKey - - if (account != other.account) return false - if (bytes != other.bytes) return false - - return true - } - - override fun hashCode(): Int { - var result = account.hashCode() - result = 31 * result + bytes.hashCode() - return result - } + override fun toString() = "UnifiedSpendingKey(bytes=***)" companion object { /** @@ -71,15 +48,12 @@ class UnifiedSpendingKey private constructor( * * @return A validated UnifiedSpendingKey. */ - suspend fun new( - account: Account, - bytes: ByteArray - ): Result { + suspend fun new(bytes: ByteArray): UnifiedSpendingKey { val bytesCopy = bytes.copyOf() RustBackend.loadLibrary() - return runCatching { + return run { require(RustBackend.validateUnifiedSpendingKey(bytesCopy)) - UnifiedSpendingKey(account, FirstClassByteArray(bytesCopy)) + UnifiedSpendingKey(FirstClassByteArray(bytesCopy)) } } } diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/Zip32AccountIndex.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/Zip32AccountIndex.kt new file mode 100644 index 000000000..1b647ecba --- /dev/null +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/Zip32AccountIndex.kt @@ -0,0 +1,22 @@ +package cash.z.ecc.android.sdk.model + +import cash.z.ecc.android.sdk.internal.ext.isInUIntRange + +/** + * A typesafe wrapper class for ZIP 32 account index + * + * @param index The account ZIP 32 account identifier + */ +data class Zip32AccountIndex internal constructor( + val index: Long +) { + init { + require(index.isInUIntRange()) { + "Account index $index is outside of allowed UInt range" + } + } + + companion object { + fun new(index: Long): Zip32AccountIndex = Zip32AccountIndex(index) + } +} 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 0129a66fa..eb164a3db 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,10 +4,10 @@ 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 +import cash.z.ecc.android.sdk.model.Zip32AccountIndex interface DerivationTool { /** @@ -44,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: Zip32AccountIndex ): 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: Zip32AccountIndex ): String /** @@ -107,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: Zip32AccountIndex ): ByteArray companion object { diff --git a/sdk-lib/src/test/java/cash/z/ecc/android/sdk/model/AccountTest.kt b/sdk-lib/src/test/java/cash/z/ecc/android/sdk/model/AccountTest.kt index c2b481fc5..ff171796e 100644 --- a/sdk-lib/src/test/java/cash/z/ecc/android/sdk/model/AccountTest.kt +++ b/sdk-lib/src/test/java/cash/z/ecc/android/sdk/model/AccountTest.kt @@ -1,13 +1,15 @@ package cash.z.ecc.android.sdk.model +import cash.z.ecc.android.sdk.fixture.AccountFixture import org.junit.Test +import java.util.UUID import kotlin.test.assertFailsWith class AccountTest { @Test fun out_of_bounds() { assertFailsWith(IllegalArgumentException::class) { - Account(-1) + AccountFixture.new(accountUuid = UUID.fromString("random")) } } }