From d80876f9546270ec2b2ea02214ca211e66f52d39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Honza=20Rychnovsk=C3=BD?= Date: Tue, 17 Dec 2024 16:26:29 +0100 Subject: [PATCH] Feature branch SDK 2.2.7 (#1661) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [#1632] Refactor `Account.DEFAULT` (#1645) * [#1632] Remove `Account.DEFAULT` * Update Account related APIs * Refactor balances APIs * Adopt `getAccounts` in fragment-based Demo app We have deprecated the old Fragment-based Demo app. See #973. So, the purpose of these changes is purely the buildability of the new Compose-based Demo app. * Add init to `JniUnifiedSpendingKey.kt` * Update deprecated Fragment-based Demo app This part of the Demo app will be removed as part of #973 * Remove deprecated functions from Synchronizer * Update WalletSnapshot and WalletVM APIs in Demo + necessary fixtures changes * Update newer Compose-based Demo app * Hide `Synchronizer.createAccount` form public API As it was, as making it public could bring more requirements on our multi-account support in version 1 * Add all accounts flow API * Refactor AccountFixture - We deduplicated the fixture across the related modules and their tests - Documentation added * Fix `fetchUtxos` error (code 14) - Closes #1648 - This also impacts SDK’s ability to recover from loss of internet connection - Changelog updated * Switch from ZIP 32 account indices to UUID account identifiers (#1640) * Switch from ZIP 32 account indices to UUID account identifiers * Rename TransactionRecipient.Account To emphasize the distinction between Account and TransactionRecipient.Account * Fix `Backend.createAccount` API parameters * Add `importAccountUfvk` to the Rust backend. * Propagate accountName and keySource across SDK * Propagate new importAccount across SDK * Wrap createAccount setup information * Wrap importAccount setup information * Add `Zip32AccountIndex` wrapper * Update key source parameter * Remove account from `UnifiedSpendingKey` * Fix `importAccountByUfvk` API * Refactor Account.accountUuid to wrapper class So we can easily keep it typesafe and compare it to each other * Add `JniAccountUsk` * Add `seedFingerprint` and `zip32AccountIndex` To public API `importAccountUfvk` * Transactions by account UUID * Refactor default account creation * Migrate to Rust crate revision with bugfixes to account UUID migration * Refactor inputs of `importAccountUfvk` * Use `FirsClassByteArray` for `seed` parameter --------- Co-authored-by: Honza Co-authored-by: Kris Nuttycombe Co-authored-by: Honza Rychnovský Co-authored-by: Daira-Emma Hopwood * Remove unused `TransactionEncoder` functions (#1660) * Resolve [#1640] review comments * `Account` documentation fix * Make `AccountPurpose.Spending` args not null As they now live under `AccountPurpose.Spending` where we need them not null + `AccountImportSetup` doc fix * Replace `bytes` with `usk` in `AccountUsk` * Change test name * Feature/proposal to pczt (#1647) * Migrate to latest revision of Zcash crates * Integrate PCZT APIs into the Android SDK * Fix `Backend.createPcztFromProposal` parameter * Add Synchronizer side of PCZT logic * Add `getAccountForUfvk` method to Rust backend & fix key derivation null issue. (#1656) * Add `getAccountForUfvk` method to the FFI. * Only decode key derivation metadata when creating a count with `Spending` purpose. Also, ensure that a spending account can still be created without providing key derivation metadata. * Update to `librustzcash` revision `205d4c930319b7b6d24aeb4efde69e9b4d1b6f7b` * Add the new `getAccountForUfvk` to `TypesafeBackend` * Update `FakeRustBackend` --------- Co-authored-by: Honza * Remove unused `TransactionEncoder` functions (#1660) * Resolve [#1640] review comments * `Account` documentation fix * Make `AccountPurpose.Spending` args not null As they now live under `AccountPurpose.Spending` where we need them not null + `AccountImportSetup` doc fix * Replace `bytes` with `usk` in `AccountUsk` * Change test name * Testnet Synchronizer test fix * Detekt warning * Changelog update --------- Co-authored-by: Jack Grigg Co-authored-by: Honza Co-authored-by: Honza Rychnovský * Peformance optimizations * Revert "Peformance optimizations" This reverts commit 68de6aed874c7fa321620419dd42d29a83dab92c. * PCZT support follow-ups (#1663) * Rename exception * Add sapling param fetching to new KS-related fun * Add dedicated clone PZCT function * Add `Synchronizer.Status.INITIALIZING` For wallets to know that accounts and other APIs are not ready yet * [#1665] Checkpoints update Closes #1665 * Bump lib version to 2.2.7 * Migrate to the published Zcash crate releases including PCZT v1 (#1664) Co-authored-by: Honza --------- Co-authored-by: Jack Grigg Co-authored-by: Kris Nuttycombe Co-authored-by: Daira-Emma Hopwood Co-authored-by: Kris Nuttycombe Co-authored-by: Milan Cerovsky --- CHANGELOG.md | 35 ++ backend-lib/Cargo.lock | 283 ++++++++-- backend-lib/Cargo.toml | 13 +- .../z/ecc/android/sdk/internal/Backend.kt | 77 ++- .../z/ecc/android/sdk/internal/Derivation.kt | 8 +- .../android/sdk/internal/jni/JniConstants.kt | 14 + .../android/sdk/internal/jni/RustBackend.kt | 176 +++++- .../sdk/internal/jni/RustDerivationTool.kt | 16 +- .../android/sdk/internal/model/JniAccount.kt | 27 +- .../sdk/internal/model/JniAccountBalance.kt | 8 +- .../sdk/internal/model/JniAccountUsk.kt | 41 ++ .../internal/model/JniUnifiedSpendingKey.kt | 30 +- backend-lib/src/main/rust/lib.rs | 511 +++++++++++++----- .../fixture/JniAccountBalanceFixture.kt | 10 +- darkside-test-lib/build.gradle.kts | 3 +- .../darkside/test/DarksideTestCoordinator.kt | 2 +- .../android/sdk/darkside/test/TestWallet.kt | 36 +- .../sdk/sample/demoapp/SampleCodeTest.kt | 25 +- .../z/ecc/android/sdk/demoapp/Constants.kt | 24 + .../z/ecc/android/sdk/demoapp/Navigation.kt | 23 +- .../android/sdk/demoapp/SharedViewModel.kt | 17 +- .../sdk/demoapp/WalletCoordinatorFactory.kt | 4 +- .../demos/getaddress/GetAddressFragment.kt | 10 +- .../demos/getbalance/GetBalanceFragment.kt | 64 ++- .../getprivatekey/GetPrivateKeyFragment.kt | 4 +- .../demos/listutxos/ListUtxosFragment.kt | 4 +- .../sdk/demoapp/demos/send/SendFragment.kt | 15 +- .../demoapp/fixture/WalletSnapshotFixture.kt | 33 +- .../sdk/demoapp/ui/common/Constants.kt | 11 - .../ui/screen/addresses/view/AddressesView.kt | 4 +- .../ui/screen/balance/view/BalanceView.kt | 45 +- .../demoapp/ui/screen/home/view/HomeView.kt | 31 +- .../screen/home/viewmodel/WalletSnapshot.kt | 25 +- .../screen/home/viewmodel/WalletViewModel.kt | 97 ++-- .../demoapp/ui/screen/send/view/SendView.kt | 16 +- .../transactions/view/TransactionsView.kt | 6 +- .../main/res/menu/activity_main_drawer.xml | 2 +- .../main/res/navigation/mobile_navigation.xml | 2 +- demo-app/src/main/res/values/strings.xml | 12 +- gradle.properties | 2 +- .../sdk/internal/DerivationToolImplTest.kt | 8 +- .../z/ecc/android/sdk/WalletCoordinator.kt | 16 +- .../sdk/fixture/AccountBalanceFixture.kt | 22 + .../ecc/android/sdk/fixture/WalletFixture.kt | 21 +- .../ecc/android/sdk/model/WalletAddresses.kt | 11 +- .../cash/z/ecc/android/sdk/model/ZecSend.kt | 16 +- .../ecc/android/sdk/fixture/WalletFixture.kt | 19 +- .../sdk/integration/TestnetIntegrationTest.kt | 39 +- .../sdk/internal/SdkSynchronizerTest.kt | 94 ++-- .../sdk/model/UnifiedSpendingKeyTest.kt | 16 +- .../android/sdk/sample/ShieldFundsSample.kt | 6 +- .../sdk/sample/TransparentRestoreSample.kt | 6 +- .../android/sdk/util/AddressGeneratorUtil.kt | 8 +- .../android/sdk/util/BalancePrinterUtil.kt | 11 +- .../ecc/android/sdk/util/DataDbScannerUtil.kt | 9 +- .../cash/z/ecc/android/sdk/util/TestWallet.kt | 35 +- .../cash/z/ecc/fixture/FakeRustBackend.kt | 53 +- .../checkpoint/mainnet/2720000.json | 8 + .../checkpoint/mainnet/2722500.json | 8 + .../checkpoint/mainnet/2725000.json | 8 + .../checkpoint/mainnet/2727500.json | 8 + .../checkpoint/mainnet/2730000.json | 8 + .../checkpoint/mainnet/2732500.json | 8 + .../checkpoint/mainnet/2735000.json | 8 + .../checkpoint/mainnet/2737500.json | 8 + .../checkpoint/mainnet/2740000.json | 8 + .../checkpoint/mainnet/2742500.json | 8 + .../checkpoint/mainnet/2745000.json | 8 + .../checkpoint/mainnet/2747500.json | 8 + .../checkpoint/mainnet/2750000.json | 8 + .../checkpoint/mainnet/2752500.json | 8 + .../checkpoint/testnet/3080000.json | 8 + .../checkpoint/testnet/3090000.json | 8 + .../checkpoint/testnet/3100000.json | 8 + .../checkpoint/testnet/3110000.json | 8 + .../checkpoint/testnet/3120000.json | 8 + .../checkpoint/testnet/3130000.json | 8 + .../cash/z/ecc/android/sdk/SdkSynchronizer.kt | 227 +++++--- .../cash/z/ecc/android/sdk/Synchronizer.kt | 240 +++++--- .../block/processor/CompactBlockProcessor.kt | 55 +- .../z/ecc/android/sdk/exception/Exceptions.kt | 49 +- .../sdk/fixture/AccountCreateSetupFixture.kt | 24 + .../ecc/android/sdk/fixture/AccountFixture.kt | 88 +++ .../sdk/fixture/WalletBalanceFixture.kt | 1 - .../android/sdk/internal/DerivationToolExt.kt | 22 +- .../android/sdk/internal/TypesafeBackend.kt | 59 +- .../sdk/internal/TypesafeBackendImpl.kt | 97 +++- .../internal/TypesafeDerivationToolImpl.kt | 14 +- .../internal/db/derived/AllTransactionView.kt | 24 + .../db/derived/DbDerivedDataRepository.kt | 4 + .../sdk/internal/db/derived/DerivedDataDb.kt | 41 +- .../sdk/internal/db/derived/TxOutputsView.kt | 11 +- .../sdk/internal/model/WalletSummary.kt | 12 +- .../repository/DerivedDataRepository.kt | 3 + .../transaction/OutboundTransactionManager.kt | 35 +- .../OutboundTransactionManagerImpl.kt | 47 +- .../transaction/TransactionEncoder.kt | 55 +- .../transaction/TransactionEncoderImpl.kt | 183 +++---- .../cash/z/ecc/android/sdk/model/Account.kt | 111 +++- .../{internal => }/model/AccountBalance.kt | 7 +- .../android/sdk/model/AccountCreateSetup.kt | 18 + .../android/sdk/model/AccountImportSetup.kt | 20 + .../z/ecc/android/sdk/model/AccountPurpose.kt | 31 ++ .../z/ecc/android/sdk/model/AccountUsk.kt | 33 ++ .../z/ecc/android/sdk/model/AccountUuid.kt | 35 ++ .../java/cash/z/ecc/android/sdk/model/Pczt.kt | 20 + .../android/sdk/model/PendingTransaction.kt | 8 +- .../android/sdk/model/UnifiedSpendingKey.kt | 36 +- .../android/sdk/model/Zip32AccountIndex.kt | 22 + .../z/ecc/android/sdk/tool/DerivationTool.kt | 17 +- .../z/ecc/android/sdk/model/AccountTest.kt | 6 +- 111 files changed, 2871 insertions(+), 1100 deletions(-) create mode 100644 backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/jni/JniConstants.kt create mode 100644 backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/JniAccountUsk.kt create mode 100644 demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/Constants.kt delete mode 100644 demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/ui/common/Constants.kt create mode 100644 sdk-incubator-lib/src/main/java/cash/z/ecc/android/sdk/fixture/AccountBalanceFixture.kt create mode 100644 sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/2720000.json create mode 100644 sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/2722500.json create mode 100644 sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/2725000.json create mode 100644 sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/2727500.json create mode 100644 sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/2730000.json create mode 100644 sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/2732500.json create mode 100644 sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/2735000.json create mode 100644 sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/2737500.json create mode 100644 sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/2740000.json create mode 100644 sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/2742500.json create mode 100644 sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/2745000.json create mode 100644 sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/2747500.json create mode 100644 sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/2750000.json create mode 100644 sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/2752500.json create mode 100644 sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/testnet/3080000.json create mode 100644 sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/testnet/3090000.json create mode 100644 sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/testnet/3100000.json create mode 100644 sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/testnet/3110000.json create mode 100644 sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/testnet/3120000.json create mode 100644 sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/testnet/3130000.json create mode 100644 sdk-lib/src/main/java/cash/z/ecc/android/sdk/fixture/AccountCreateSetupFixture.kt create mode 100644 sdk-lib/src/main/java/cash/z/ecc/android/sdk/fixture/AccountFixture.kt rename sdk-lib/src/main/java/cash/z/ecc/android/sdk/{internal => }/model/AccountBalance.kt (83%) create mode 100644 sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/AccountCreateSetup.kt create mode 100644 sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/AccountImportSetup.kt create mode 100644 sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/AccountPurpose.kt create mode 100644 sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/AccountUsk.kt create mode 100644 sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/AccountUuid.kt create mode 100644 sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/Pczt.kt create mode 100644 sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/Zip32AccountIndex.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 247845dfb..ddeb69373 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,41 @@ and this library adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- `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 +- `Synchronizer.createPcztFromProposal` +- `Synchronizer.addProofsToPczt` +- `Synchronizer.createTransactionFromPczt` +- `Zip32AccountIndex`, `AccountUuid`, `AccountUsk`, `AccountPurpose`, `AccountCreateSetup`, `AcountImportSetup`, and + `Pczt` model classes have been added to support the new or the changed APIs + +### 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 +- `RustLayerException.GetCurrentAddressException` has been renamed to `RustLayerException.GetAddressException` +- Checkpoints update + +### 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 + errors in its `refreshUtxos` API. It then sets its state to `State.Disconnected` in such a case. + ## [2.2.6] - 2024-11-16 ### Added diff --git a/backend-lib/Cargo.lock b/backend-lib/Cargo.lock index ddd59ba7c..35f255396 100644 --- a/backend-lib/Cargo.lock +++ b/backend-lib/Cargo.lock @@ -257,6 +257,15 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + [[package]] name = "autocfg" version = "1.4.0" @@ -298,9 +307,9 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "bech32" -version = "0.9.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" +checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d" [[package]] name = "bellman" @@ -561,6 +570,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "cobs" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15" + [[package]] name = "combine" version = "4.6.7" @@ -607,6 +622,15 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core2" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "239fa3ae9b63c2dc74bd3fa852d4792b8b305ae64eeede946265b6af62f1fff3" +dependencies = [ + "memchr", +] + [[package]] name = "cpufeatures" version = "0.2.15" @@ -625,6 +649,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + [[package]] name = "crossbeam-channel" version = "0.5.13" @@ -1114,6 +1144,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + [[package]] name = "enum-ordinalize" version = "3.1.15" @@ -1166,9 +1208,9 @@ dependencies = [ [[package]] name = "f4jumble" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a83e8d7fd0c526af4aad893b7c9fe41e2699ed8a776a6c74aecdeafe05afc75" +checksum = "0d42773cb15447644d170be20231a3268600e0c4cea8987d013b93ac973d3cf7" dependencies = [ "blake2b_simd", ] @@ -1431,6 +1473,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "getset" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f636605b743120a8d32ed92fc27b6cde1a769f8f936c065151eb66f88ded513c" +dependencies = [ + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "gimli" version = "0.31.1" @@ -1479,6 +1533,18 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47716fe1ae67969c5e0b2ef826f32db8c3be72be325e1aa3c1951d06b5575ec5" +[[package]] +name = "halo2_poseidon" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa3da60b81f02f9b33ebc6252d766f843291fb4d2247a07ae73d20b791fc56f" +dependencies = [ + "bitvec", + "ff", + "group", + "pasta_curves", +] + [[package]] name = "halo2_proofs" version = "0.3.0" @@ -1495,6 +1561,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1525,6 +1600,20 @@ dependencies = [ "hashbrown 0.14.5", ] +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32", + "rustc_version", + "serde", + "spin 0.9.8", + "stable_deref_trait", +] + [[package]] name = "heck" version = "0.5.0" @@ -1703,9 +1792,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "incrementalmerkletree" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d45063fbc4b0a37837f6bfe0445f269d13d730ad0aa3b5a7f74aa7bf27a0f4df" +checksum = "216c71634ac6f6ed13c2102d64354c0a04dcbdc30e31692c5972d3974d8b6d97" dependencies = [ "either", ] @@ -2001,12 +2090,9 @@ dependencies = [ [[package]] name = "memuse" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2145869435ace5ea6ea3d35f59be559317ec9a0d04e1812d5f185a87b6d36f1a" -dependencies = [ - "nonempty", -] +checksum = "3d97bbf43eb4f088f8ca469930cde17fa036207c9a5e02ccc5107c4e8b17c964" [[package]] name = "merlin" @@ -2249,17 +2335,20 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "orchard" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f18e997fa121de5c73e95cdc7e8512ae43b7de38904aeea5e5713cc48f3c0ba" +checksum = "02f7152474406422f572de163e0bc63b2126cdbfe17bc849efbbde36fcfe647e" dependencies = [ "aes", "bitvec", "blake2b_simd", + "core2", "ff", "fpe", + "getset", "group", "halo2_gadgets", + "halo2_poseidon", "halo2_proofs", "hex", "incrementalmerkletree", @@ -2270,6 +2359,7 @@ dependencies = [ "rand", "reddsa", "serde", + "sinsemilla", "subtle", "tracing", "visibility", @@ -2404,6 +2494,33 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pczt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "545f95dc3d5295b9ed0360aa3b63e610bd4a73f2f382c616b2fda66c3288e989" +dependencies = [ + "blake2b_simd", + "bls12_381", + "ff", + "getset", + "jubjub", + "nonempty", + "orchard", + "pasta_curves", + "postcard", + "rand_core", + "redjubjub", + "sapling-crypto", + "secp256k1", + "serde", + "serde_with", + "zcash_note_encryption", + "zcash_primitives", + "zcash_protocol", + "zcash_transparent", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -2556,6 +2673,19 @@ dependencies = [ "thiserror", ] +[[package]] +name = "postcard" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170a2601f67cc9dba8edd8c4870b15f71a6a2dc196daec8c83f72b59dff628a8" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "heapless", + "serde", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -2610,6 +2740,28 @@ dependencies = [ "toml_edit", ] +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "proc-macro2" version = "1.0.89" @@ -2933,6 +3085,7 @@ dependencies = [ "libsqlite3-sys", "smallvec", "time", + "uuid", ] [[package]] @@ -3074,9 +3227,9 @@ dependencies = [ [[package]] name = "sapling-crypto" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfff8cfce16aeb38da50b8e2ed33c9018f30552beff2210c266662a021b17f38" +checksum = "85c2acdbbab83d554fc2dceea5f7d6d3da71e57adb18a6c80b8901bd0eee54b0" dependencies = [ "aes", "bellman", @@ -3088,6 +3241,7 @@ dependencies = [ "document-features", "ff", "fpe", + "getset", "group", "hex", "incrementalmerkletree", @@ -3380,6 +3534,17 @@ dependencies = [ "time", ] +[[package]] +name = "sinsemilla" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d268ae0ea06faafe1662e9967cd4f9022014f5eeb798e0c302c876df8b7af9c" +dependencies = [ + "group", + "pasta_curves", + "subtle", +] + [[package]] name = "siphasher" version = "0.3.11" @@ -3431,6 +3596,9 @@ name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] [[package]] name = "spki" @@ -3483,6 +3651,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "static_assertions" version = "1.1.0" @@ -4728,6 +4902,10 @@ name = "uuid" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" +dependencies = [ + "getrandom", + "serde", +] [[package]] name = "valuable" @@ -5213,6 +5391,7 @@ dependencies = [ "log-panics", "orchard", "paranoid-android", + "pczt", "prost", "rayon", "rusqlite", @@ -5222,10 +5401,12 @@ dependencies = [ "tor-rtcompat", "tracing", "tracing-subscriber", + "uuid", "xz2", "zcash_address", "zcash_client_backend", "zcash_client_sqlite", + "zcash_note_encryption", "zcash_primitives", "zcash_proofs", "zip32", @@ -5233,12 +5414,13 @@ dependencies = [ [[package]] name = "zcash_address" -version = "0.6.0" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ff95eac82f71286a79c750e674550d64fb2b7aadaef7b89286b2917f645457d" +checksum = "9b955fe87f2d9052e3729bdbeb0e94975355f4fe39f7d26aea9457bec6a0bb55" dependencies = [ "bech32", "bs58", + "core2", "f4jumble", "zcash_encoding", "zcash_protocol", @@ -5246,9 +5428,9 @@ dependencies = [ [[package]] name = "zcash_client_backend" -version = "0.15.0" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e29a9975a741754e9d68c1326df809049712d3d3e831df9983eb631b8c2c2257" +checksum = "c2a187ad05cdfe13707c07e6aedca8026b34921d081decfd0b43aac1efd438a7" dependencies = [ "arti-client", "base64", @@ -5272,7 +5454,9 @@ dependencies = [ "nonempty", "orchard", "pasta_curves", + "pczt", "percent-encoding", + "postcard", "prost", "rand", "rand_core", @@ -5306,9 +5490,9 @@ dependencies = [ [[package]] name = "zcash_client_sqlite" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7ae0c0db930b76831161205be4d93443417a600c7212a27e50df9288c407581" +checksum = "9b8046e94c3d746cc00e0ceb4ec4263c4fb93271a8681b425365ad600148ab15" dependencies = [ "bip32", "bs58", @@ -5327,6 +5511,7 @@ dependencies = [ "schemerz", "schemerz-rusqlite", "secrecy", + "serde", "shardtree", "static_assertions", "subtle", @@ -5344,19 +5529,19 @@ dependencies = [ [[package]] name = "zcash_encoding" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "052d8230202f0a018cd9b5d1b56b94cd25e18eccc2d8665073bcea8261ab87fc" +checksum = "3654116ae23ab67dd1f849b01f8821a8a156f884807ff665eac109bf28306c4d" dependencies = [ - "byteorder", + "core2", "nonempty", ] [[package]] name = "zcash_keys" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf15baad7e4a87cca57af718766a0d5bb03e0cbf118e3ef6462697a4664b88c" +checksum = "6ad3cf576c6e6094cd03f446fcb83ad241ec315a088593cd50940f135cb03ce1" dependencies = [ "bech32", "bip32", @@ -5364,6 +5549,7 @@ dependencies = [ "bls12_381", "bs58", "byteorder", + "core2", "document-features", "group", "memuse", @@ -5376,16 +5562,16 @@ dependencies = [ "tracing", "zcash_address", "zcash_encoding", - "zcash_primitives", "zcash_protocol", + "zcash_transparent", "zip32", ] [[package]] name = "zcash_note_encryption" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b4580cd6cee12e44421dac43169be8d23791650816bdb34e6ddfa70ac89c1c5" +checksum = "77efec759c3798b6e4d829fcc762070d9b229b0f13338c40bf993b7b609c2272" dependencies = [ "chacha20", "chacha20poly1305", @@ -5396,9 +5582,9 @@ dependencies = [ [[package]] name = "zcash_primitives" -version = "0.20.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c1573280a8d545009381af602c714ab53c43088d089e0d17e89740184f1712b" +checksum = "9b45f3ca3a9df34fcdbf036c2c814417bb417bde742812abc09d744bb3d7ed72" dependencies = [ "aes", "bip32", @@ -5409,6 +5595,7 @@ dependencies = [ "equihash", "ff", "fpe", + "getset", "group", "hex", "incrementalmerkletree", @@ -5430,14 +5617,15 @@ dependencies = [ "zcash_note_encryption", "zcash_protocol", "zcash_spec", + "zcash_transparent", "zip32", ] [[package]] name = "zcash_proofs" -version = "0.20.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d83453ba15e77d8f1806ed8558b7ab2b27f0a57dab037b1c8d0c8fb57820cf" +checksum = "d5826910c516675eca1f34b3557e159f4e35a4a1711b39fa4f01fb0adb9a9c24" dependencies = [ "bellman", "blake2b_simd", @@ -5458,11 +5646,13 @@ dependencies = [ [[package]] name = "zcash_protocol" -version = "0.4.1" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4bbb28b59321f47454e69c2d95c11c227bb1a21bfa3381bd43c4ac98f5caee1" +checksum = "82cb36b15b5a1be70b30c32ce40372dead6561df8a467e297f96b892873a63a2" dependencies = [ + "core2", "document-features", + "hex", "memuse", ] @@ -5475,6 +5665,29 @@ dependencies = [ "blake2b_simd", ] +[[package]] +name = "zcash_transparent" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed0512e8e02af804e852fbbc4bd5db35a9037bc253d2ce396506293a6e7dd745" +dependencies = [ + "bip32", + "blake2b_simd", + "bs58", + "core2", + "getset", + "hex", + "ripemd", + "secp256k1", + "sha2", + "subtle", + "zcash_address", + "zcash_encoding", + "zcash_protocol", + "zcash_spec", + "zip32", +] + [[package]] name = "zerocopy" version = "0.7.35" diff --git a/backend-lib/Cargo.toml b/backend-lib/Cargo.toml index f67c3e396..355bfd02c 100644 --- a/backend-lib/Cargo.toml +++ b/backend-lib/Cargo.toml @@ -13,12 +13,14 @@ rust-version = "1.82" [dependencies] # Zcash dependencies orchard = "0.10" -sapling = { package = "sapling-crypto", version = "0.3", default-features = false } +pczt = "0.1" +sapling = { package = "sapling-crypto", version = "0.4", default-features = false } zcash_address = "0.6" -zcash_client_backend = { version = "0.15", features = ["orchard", "tor", "transparent-inputs", "unstable"] } -zcash_client_sqlite = { version = "0.13", features = ["orchard", "transparent-inputs", "unstable"] } -zcash_primitives = "0.20" -zcash_proofs = "0.20" +zcash_client_backend = { version = "0.16", features = ["orchard", "tor", "transparent-inputs", "unstable", "pczt"] } +zcash_client_sqlite = { version = "0.14", features = ["orchard", "transparent-inputs", "unstable", "serde"] } +zcash_note_encryption = "0.4.1" +zcash_primitives = "0.21" +zcash_proofs = "0.21" zip32 = "0.1.2" # Infrastructure @@ -32,6 +34,7 @@ rayon = "1.7" # JNI anyhow = "1" jni = { version = "0.21", default-features = false } +uuid = "1" # Logging log-panics = "2.0.0" 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..876c08149 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 @@ -51,6 +51,43 @@ interface Backend { unifiedSpendingKey: ByteArray ): List + /** + * Creates a partially-created (unsigned without proofs) transaction from the given proposal. + * + * Do not call this multiple times in parallel, or you will generate PCZT instances that, if + * finalized, would double-spend the same notes. + * + * @return the partially created transaction in its serialized format. + * + * @throws RuntimeException as a common indicator of the operation failure + */ + suspend fun createPcztFromProposal( + accountUuid: ByteArray, + proposal: ProposalUnsafe + ): ByteArray + + /** + * Adds proofs to the given PCZT. + * + * @return the updated PCZT in its serialized format. + * + * @throws RuntimeException as a common indicator of the operation failure + */ + suspend fun addProofsToPczt(pczt: ByteArray): ByteArray + + /** + * Takes a PCZT that has been separately proven and signed, finalizes it, and stores + * it in the wallet. + * + * @return the txid of the completed transaction. + * + * @throws RuntimeException as a common indicator of the operation failure + */ + suspend fun extractAndStoreTxFromPczt( + pcztWithProofs: ByteArray, + pcztWithSignatures: ByteArray, + ): ByteArray + /** * @throws RuntimeException as a common indicator of the operation failure */ @@ -80,15 +117,39 @@ interface Backend { @Throws(RuntimeException::class) suspend fun getAccounts(): List + /** + * @throws RuntimeException as a common indicator of the operation failure + */ + @Throws(RuntimeException::class) + suspend fun getAccountForUfvk(ufvk: String): JniAccount? + /** * @throws RuntimeException as a common indicator of the operation failure */ @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 +170,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..981d4b596 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 @@ -89,18 +89,58 @@ class RustBackend private constructor( } } + override suspend fun getAccountForUfvk(ufvk: String): JniAccount? { + return withContext(SdkDispatchers.DATABASE_IO) { + getAccountForUfvk( + dbDataPath = dataDbFile.absolutePath, + networkId = networkId, + ufvk = ufvk, + ) + } + } + 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 +154,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 +167,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 +358,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 +373,7 @@ class RustBackend private constructor( } override suspend fun proposeTransfer( - accountIndex: Int, + accountUuid: ByteArray, to: String, value: Long, memo: ByteArray? @@ -342,7 +382,7 @@ class RustBackend private constructor( ProposalUnsafe.parse( proposeTransfer( dataDbFile.absolutePath, - accountIndex, + accountUuid, to, value, memo, @@ -352,7 +392,7 @@ class RustBackend private constructor( } override suspend fun proposeShielding( - accountIndex: Int, + accountUuid: ByteArray, shieldingThreshold: Long, memo: ByteArray?, transparentReceiver: String? @@ -360,7 +400,7 @@ class RustBackend private constructor( return withContext(SdkDispatchers.DATABASE_IO) { proposeShielding( dataDbFile.absolutePath, - accountIndex, + accountUuid, shieldingThreshold, memo, transparentReceiver, @@ -388,6 +428,41 @@ class RustBackend private constructor( ).asList() } + override suspend fun createPcztFromProposal( + accountUuid: ByteArray, + proposal: ProposalUnsafe + ): ByteArray = + withContext(SdkDispatchers.DATABASE_IO) { + createPcztFromProposal( + dataDbFile.absolutePath, + accountUuid, + proposal.toByteArray(), + networkId = networkId + ) + } + + override suspend fun addProofsToPczt(pczt: ByteArray): ByteArray = + addProofsToPczt( + pczt, + spendParamsPath = saplingSpendFile.absolutePath, + outputParamsPath = saplingOutputFile.absolutePath + ) + + override suspend fun extractAndStoreTxFromPczt( + pcztWithProofs: ByteArray, + pcztWithSignatures: ByteArray + ): ByteArray = + withContext(SdkDispatchers.DATABASE_IO) { + extractAndStoreTxFromPczt( + dataDbFile.absolutePath, + pcztWithProofs, + pcztWithSignatures, + spendParamsPath = saplingSpendFile.absolutePath, + outputParamsPath = saplingOutputFile.absolutePath, + networkId = networkId + ) + } + override suspend fun putUtxo( txId: ByteArray, index: Int, @@ -492,13 +567,38 @@ 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 + private external fun getAccountForUfvk( + dbDataPath: String, + networkId: Int, + ufvk: String, + ): JniAccount? + + @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 +610,7 @@ class RustBackend private constructor( @JvmStatic private external fun getCurrentAddress( dbDataPath: String, - accountIndex: Int, + accountUuid: ByteArray, networkId: Int ): String @@ -523,7 +623,7 @@ class RustBackend private constructor( @JvmStatic private external fun listTransparentReceivers( dbDataPath: String, - accountIndex: Int, + accountUuid: ByteArray, networkId: Int ): Array @@ -671,7 +771,7 @@ class RustBackend private constructor( @JvmStatic private external fun proposeTransferFromUri( dbDataPath: String, - accountIndex: Int, + accountUuid: ByteArray, uri: String, networkId: Int, ): ByteArray @@ -680,7 +780,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 +791,7 @@ class RustBackend private constructor( @Suppress("LongParameterList") private external fun proposeShielding( dbDataPath: String, - accountIndex: Int, + accountUuid: ByteArray, shieldingThreshold: Long, memo: ByteArray?, transparentReceiver: String?, @@ -709,6 +809,32 @@ class RustBackend private constructor( networkId: Int ): Array + @JvmStatic + private external fun createPcztFromProposal( + dbDataPath: String, + accountUuid: ByteArray, + proposal: ByteArray, + networkId: Int, + ): ByteArray + + @JvmStatic + private external fun addProofsToPczt( + pczt: ByteArray, + spendParamsPath: String, + outputParamsPath: String, + ): ByteArray + + @JvmStatic + @Suppress("LongParameterList") + private external fun extractAndStoreTxFromPczt( + dbDataPath: String, + pcztWithProofs: ByteArray, + pcztWithSignatures: ByteArray, + spendParamsPath: String, + outputParamsPath: String, + networkId: Int, + ): ByteArray + @JvmStatic private external fun branchIdForHeight( height: Long, 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 a79a9e002..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,23 +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 -@Suppress("LongParameterList") 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..705c54296 100644 --- a/backend-lib/src/main/rust/lib.rs +++ b/backend-lib/src/main/rust/lib.rs @@ -12,14 +12,22 @@ use jni::{ sys::{jboolean, jbyteArray, jint, jlong, jobject, jobjectArray, jstring, JNI_FALSE, JNI_TRUE}, JNIEnv, }; +use pczt::{ + roles::{combiner::Combiner, prover::Prover}, + Pczt, +}; use prost::Message; use secrecy::{ExposeSecret, SecretVec}; 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::{ @@ -28,15 +36,16 @@ use zcash_client_backend::{ chain::{scan_cached_blocks, CommitmentTreeRoot, ScanSummary}, scanning::{ScanPriority, ScanRange}, wallet::{ - create_proposed_transactions, decrypt_and_store_transaction, - input_selection::GreedyInputSelector, propose_shielding, propose_transfer, + create_pczt_from_proposal, create_proposed_transactions, decrypt_and_store_transaction, + extract_and_store_transaction_from_pczt, input_selection::GreedyInputSelector, + propose_shielding, propose_transfer, }, - Account, AccountBalance, AccountBirthday, AccountSource, InputSource, SeedRelevance, + Account, AccountBalance, AccountBirthday, InputSource, SeedRelevance, WalletCommitmentTrees, WalletRead, WalletSummary, WalletWrite, }, encoding::AddressCodec, fees::DustOutputPolicy, - keys::{DecodingError, Era, UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey}, + keys::{DecodingError, Era, UnifiedFullViewingKey, UnifiedSpendingKey}, proto::{proposal::Proposal, service::TreeState}, tor::http::cryptex, wallet::{NoteId, OvkPolicy, WalletTransparentOutput}, @@ -44,10 +53,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 +77,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}; @@ -77,10 +88,6 @@ mod utils; const ANCHOR_OFFSET_U32: u32 = 10; const ANCHOR_OFFSET: NonZeroU32 = unsafe { NonZeroU32::new_unchecked(ANCHOR_OFFSET_U32) }; -// Do not generate Orchard receivers until we support receiving Orchard funds. -const DEFAULT_ADDRESS_REQUEST: UnifiedAddressRequest = - UnifiedAddressRequest::unsafe_new(true, true, true); - #[cfg(debug_assertions)] fn print_debug_state() { debug!("WARNING! Debugging enabled! This will likely slow things down 10X!"); @@ -112,45 +119,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,22 +269,43 @@ 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), + (&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 +334,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) @@ -358,18 +344,52 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_getAccoun unwrap_exc_or(&mut env, res, ptr::null_mut()) } +#[unsafe(no_mangle)] +pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_getAccountForUfvk<'local>( + mut env: JNIEnv<'local>, + _: JClass<'local>, + db_data: JString<'local>, + network_id: jint, + ufvk_string: JString<'local>, +) -> jobject { + let res = catch_unwind(&mut env, |env| { + let network = parse_network(network_id as u32)?; + let db_data = wallet_db(env, network, db_data)?; + + let ufvk_string = utils::java_string_to_rust(env, &ufvk_string); + let ufvk = match UnifiedFullViewingKey::decode(&network, &ufvk_string) { + Ok(ufvk) => ufvk, + Err(e) => { + return Err(anyhow!( + "Error while deriving viewing key from string input: {}", + e, + )); + } + }; + + let account = db_data.get_account_for_ufvk(&ufvk)?; + + if let Some(account) = account { + Ok(encode_account(env, &network, account)?.into_raw()) + } else { + Ok(ptr::null_mut()) + } + }); + unwrap_exc_or(&mut env, res, ptr::null_mut()) +} + 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 +433,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 +458,137 @@ 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 purpose = match purpose { + 0 => { + let seed_fingerprint = if !seed_fingerprint_bytes.is_null() { + <[u8; 32]>::try_from(&env.convert_byte_array(seed_fingerprint_bytes)?[..]) + .ok() + .map(SeedFingerprint::from_bytes) + } else { + None + }; + 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)); - Ok(encode_usk(env, account_index, usk)?.into_raw()) + 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_account(env, &network, account)?.into_raw()) }); unwrap_exc_or(&mut env, res, ptr::null_mut()) } @@ -483,16 +623,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 +640,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 +1312,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()); @@ -1187,13 +1327,13 @@ fn encode_account_balance<'a>( let orchard_value_pending = Amount::from(balance.orchard_balance().value_pending_spendability()); - let unshielded = Amount::from(balance.unshielded()); + let unshielded = Amount::from(balance.unshielded_balance().total()); 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 +1351,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 +1409,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 +1738,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 +1746,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 +1758,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 +1782,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 +1792,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 +1822,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 +1846,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 +1856,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 +1871,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 +1896,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 +1940,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))?; @@ -1879,6 +2004,137 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_createPro unwrap_exc_or(&mut env, res, ptr::null_mut()) } +/// Creates a partially-created (unsigned without proofs) transaction from the given proposal. +/// +/// Returns the partially created transaction in its serialized format. +/// +/// Do not call this multiple times in parallel, or you will generate PCZT instances that, if +/// finalized, would double-spend the same notes. +#[unsafe(no_mangle)] +pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_createPcztFromProposal< + 'local, +>( + mut env: JNIEnv<'local>, + _: JClass<'local>, + db_data: JString<'local>, + account_uuid: JByteArray<'local>, + proposal: JByteArray<'local>, + network_id: jint, +) -> jbyteArray { + let res = catch_unwind(&mut env, |env| { + let _span = tracing::info_span!("RustBackend.createPcztFromProposal").entered(); + let network = parse_network(network_id as u32)?; + let mut db_data = wallet_db(env, network, db_data)?; + let account_id = account_id_from_jni(&env, account_uuid)?; + + let proposal = Proposal::decode(&env.convert_byte_array(proposal)?[..]) + .map_err(|e| anyhow!("Invalid proposal: {}", e))? + .try_into_standard_proposal(&db_data)?; + + if proposal.steps().len() == 1 { + let pczt = create_pczt_from_proposal::<_, _, Infallible, _, Infallible, _>( + &mut db_data, + &network, + account_id, + OvkPolicy::Sender, + &proposal, + ) + .map_err(|e| anyhow!("Error creating PCZT from single-step proposal: {}", e))?; + + Ok(utils::rust_bytes_to_java(&env, &pczt.serialize())?.into_raw()) + } else { + Err(anyhow!( + "Multi-step proposals are not yet supported for PCZT generation." + )) + } + }); + unwrap_exc_or(&mut env, res, ptr::null_mut()) +} + +/// Adds proofs to the given PCZT. +/// +/// Returns the updated PCZT in its serialized format. +#[unsafe(no_mangle)] +pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_addProofsToPczt<'local>( + mut env: JNIEnv<'local>, + _: JClass<'local>, + pczt: JByteArray<'local>, + spend_params: JString<'local>, + output_params: JString<'local>, +) -> jbyteArray { + let res = catch_unwind(&mut env, |env| { + let _span = tracing::info_span!("RustBackend.addProofsToPczt").entered(); + + let pczt = Pczt::parse(&env.convert_byte_array(pczt)?[..]) + .map_err(|e| anyhow!("Invalid PCZT: {:?}", e))?; + + let spend_params = utils::java_string_to_rust(env, &spend_params); + let output_params = utils::java_string_to_rust(env, &output_params); + let prover = LocalTxProver::new(Path::new(&spend_params), Path::new(&output_params)); + + let pczt_with_proofs = Prover::new(pczt) + .create_orchard_proof(&orchard::circuit::ProvingKey::build()) + .map_err(|e| anyhow!("Failed to create Orchard proof for PCZT: {:?}", e))? + .create_sapling_proofs(&prover, &prover) + .map_err(|e| anyhow!("Failed to create Sapling proofs for PCZT: {:?}", e))? + .finish(); + + Ok(utils::rust_bytes_to_java(&env, &pczt_with_proofs.serialize())?.into_raw()) + }); + unwrap_exc_or(&mut env, res, ptr::null_mut()) +} + +/// Takes a PCZT that has been separately proven and signed, finalizes it, and stores it +/// in the wallet. +/// +/// Returns the txid of the completed transaction. +#[unsafe(no_mangle)] +pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_extractAndStoreTxFromPczt< + 'local, +>( + mut env: JNIEnv<'local>, + _: JClass<'local>, + db_data: JString<'local>, + pczt_with_proofs: JByteArray<'local>, + pczt_with_signatures: JByteArray<'local>, + spend_params: JString<'local>, + output_params: JString<'local>, + network_id: jint, +) -> jbyteArray { + let res = catch_unwind(&mut env, |env| { + let _span = tracing::info_span!("RustBackend.extractAndStoreTxFromPczt").entered(); + let network = parse_network(network_id as u32)?; + let mut db_data = wallet_db(env, network, db_data)?; + + let pczt_with_proofs = Pczt::parse(&env.convert_byte_array(pczt_with_proofs)?[..]) + .map_err(|e| anyhow!("Invalid PCZT-with-proofs: {:?}", e))?; + + let pczt_with_signatures = Pczt::parse(&env.convert_byte_array(pczt_with_signatures)?[..]) + .map_err(|e| anyhow!("Invalid PCZT-with-proofs: {:?}", e))?; + + let spend_params = utils::java_string_to_rust(env, &spend_params); + let output_params = utils::java_string_to_rust(env, &output_params); + let prover = LocalTxProver::new(Path::new(&spend_params), Path::new(&output_params)); + let (spend_vk, output_vk) = prover.verifying_keys(); + + let pczt = Combiner::new(vec![pczt_with_proofs, pczt_with_signatures]) + .combine() + .map_err(|e| anyhow!("Failed to combine PCZTs: {:?}", e))?; + + let txid = extract_and_store_transaction_from_pczt::<_, ()>( + &mut db_data, + pczt, + &spend_vk, + &output_vk, + &orchard::circuit::VerifyingKey::build(), + ) + .map_err(|e| anyhow!("Failed to extract transaction from PCZT: {:?}", e))?; + + Ok(utils::rust_bytes_to_java(env, txid.as_ref())?.into_raw()) + }); + unwrap_exc_or(&mut env, res, ptr::null_mut()) +} + #[unsafe(no_mangle)] pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_branchIdForHeight<'local>( mut env: JNIEnv<'local>, @@ -1918,7 +2174,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 +2184,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()) } @@ -1998,7 +2255,7 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustDerivationTool_de .map(|usk| usk.to_unified_full_viewing_key())?; let (ua, _) = ufvk - .find_address(DiversifierIndex::new(), DEFAULT_ADDRESS_REQUEST) + .find_address(DiversifierIndex::new(), None) .expect("At least one Unified Address should be derivable"); let address_str = ua.encode(&network); let output = env @@ -2035,7 +2292,7 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustDerivationTool_de // Derive the default Unified Address (containing the default Sapling payment // address that older SDKs used). - let (ua, _) = ufvk.default_address(DEFAULT_ADDRESS_REQUEST)?; + let (ua, _) = ufvk.default_address(None)?; let address_str = ua.encode(&network); let output = env .new_string(address_str) @@ -2236,7 +2493,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 +2501,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 937d146ed..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 @@ -2,8 +2,12 @@ package cash.z.ecc.android.sdk.internal.fixture import cash.z.ecc.android.sdk.internal.model.JniAccountBalance +/** + * This is a test fixture for [JniAccountBalance] class. It holds mocked values that are only used within + * [JniWalletSummaryTest]. + */ object JniAccountBalanceFixture { - const val ACCOUNT_ID: Int = 0 + val ACCOUNT_UUID: ByteArray = "random_uuid_16_b".toByteArray() const val SAPLING_VERIFIED_BALANCE: Long = 0L const val SAPLING_CHANGE_PENDING: Long = 0L const val SAPLING_VALUE_PENDING: Long = 0L @@ -14,7 +18,7 @@ object JniAccountBalanceFixture { @Suppress("LongParameterList") fun new( - account: Int = ACCOUNT_ID, + accountUuid: ByteArray = ACCOUNT_UUID, saplingVerifiedBalance: Long = SAPLING_VERIFIED_BALANCE, saplingChangePending: Long = SAPLING_CHANGE_PENDING, saplingValuePending: Long = SAPLING_VALUE_PENDING, @@ -23,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 727e49e2f..cf498160e 100644 --- a/darkside-test-lib/build.gradle.kts +++ b/darkside-test-lib/build.gradle.kts @@ -25,8 +25,9 @@ dependencies { implementation(libs.androidx.multidex) implementation(libs.bundles.grpc) + androidTestImplementation(projects.sdkLib) + androidTestImplementation(projects.sdkIncubatorLib) androidTestImplementation(libs.bundles.androidx.test) - androidTestImplementation(libs.zcashwalletplgn) androidTestImplementation(libs.bip39) } 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 5ef23c105..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.saplingBalances.value + 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 7f5a12314..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,8 +7,9 @@ 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.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 @@ -55,24 +56,31 @@ class TestWallet( // Although runBlocking isn't great, this usage is OK because this is only used within the // automated tests - private val account = Account.DEFAULT + internal val account = AccountFixture.new() private val context = InstrumentationRegistry.getInstrumentation().context private val seed: ByteArray = Mnemonics.MnemonicCode(seedPhrase).toSeed() private val shieldedSpendingKey = - runBlocking { DerivationTool.getInstance().deriveUnifiedSpendingKey(seed, network = network, account) } + runBlocking { + DerivationTool.getInstance().deriveUnifiedSpendingKey( + seed = seed, + network = network, + 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.saplingBalances.value?.available + val available + get() = synchronizer.walletBalances.value?.get(account.accountUuid)?.sapling?.available val unifiedAddress = runBlocking { synchronizer.getUnifiedAddress(account) } val transparentAddress = @@ -107,7 +115,7 @@ class TestWallet( ): TestWallet { synchronizer.createProposedTransactions( synchronizer.proposeTransfer( - shieldedSpendingKey.account, + account, address, amount, memo @@ -123,13 +131,13 @@ class TestWallet( } suspend fun shieldFunds(): TestWallet { - synchronizer.refreshUtxos(Account.DEFAULT, BlockHeight.new(935000L)).let { count -> + synchronizer.refreshUtxos(account, BlockHeight.new(935000L)).let { count -> Twig.debug { "FOUND $count new UTXOs" } } 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 ee0dff0df..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,8 +8,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.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.Account import cash.z.ecc.android.sdk.model.BlockHeight import cash.z.ecc.android.sdk.model.ZcashNetwork import cash.z.ecc.android.sdk.tool.DerivationTool @@ -72,7 +73,7 @@ class SampleCodeTest { @Test fun getAddress() = runBlocking { - val address = synchronizer.getUnifiedAddress(Account.DEFAULT) + val address = synchronizer.getUnifiedAddress(AccountFixture.new()) assertFalse(address.isBlank()) log("Address: $address") } @@ -170,8 +171,9 @@ class SampleCodeTest { // /////////////////////////////////////////////////// // Create a signed transaction (with memo) and broadcast @Test - fun submitTransaction() = + 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, - Account.DEFAULT + account.hdAccountIndex!! ) synchronizer.createProposedTransactions( synchronizer.proposeTransfer( - spendingKey.account, + account, address, amount, memo @@ -197,21 +199,22 @@ class SampleCodeTest { // //////////////////////////////////////////////////// companion object { - private val seed = "Insert seed for testing".toByteArray() + 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 new file mode 100644 index 000000000..295cb15fc --- /dev/null +++ b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/Constants.kt @@ -0,0 +1,24 @@ +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 +val ANDROID_STATE_FLOW_TIMEOUT = 5.seconds + +/** + * A tiny weight, useful for spacers to fill an empty space. + */ +const val MINIMAL_WEIGHT = 0.0001f + +/** + * 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 646baf08c..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 @@ -63,6 +63,7 @@ import cash.z.ecc.android.sdk.model.Proposal import cash.z.ecc.android.sdk.model.ZcashNetwork import cash.z.ecc.android.sdk.type.ServerValidation import co.electriccoin.lightwallet.client.model.LightWalletEndpoint +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -76,11 +77,14 @@ internal fun ComposeActivity.Navigation() { NavHost(navController = navController, startDestination = HOME) { composable(HOME) { val walletSnapshot = walletViewModel.walletSnapshot.collectAsStateWithLifecycle().value + val allAccounts = walletViewModel.accounts.collectAsStateWithLifecycle().value if (null == walletSnapshot) { // Display loading indicator } else { Home( - walletSnapshot, + currentAccount = walletViewModel.getCurrentAccount(), + allAccounts = allAccounts.toImmutableList(), + walletSnapshot = walletSnapshot, goBalance = { navController.navigateJustOnce(BALANCE) }, goSend = { navController.navigateJustOnce(SEND) }, goAddressDetails = { navController.navigateJustOnce(WALLET_ADDRESS_DETAILS) }, @@ -106,9 +110,14 @@ internal fun ComposeActivity.Navigation() { if (null == synchronizer || null == walletSnapshot) { // Display loading indicator } else { + val balance = + walletSnapshot.balanceByAccountUuid( + accountUuid = walletViewModel.getCurrentAccount().accountUuid + ) val scope = rememberCoroutineScope() Balance( - walletSnapshot, + exchangeRateUsd = walletSnapshot.exchangeRateUsd, + accountBalance = balance, onShieldFunds = { walletViewModel.shieldFunds() }, sendState = walletViewModel.sendState.collectAsStateWithLifecycle().value, onBack = { @@ -129,11 +138,13 @@ internal fun ComposeActivity.Navigation() { if (null == synchronizer) { // Display loading indicator } else { + val currentAccount = walletViewModel.getCurrentAccount() val scope = rememberCoroutineScope() val snackbarHostState = remember { SnackbarHostState() } // I don't like giving synchronizer directly over to the view, but for now it isolates each of the // demo app views Addresses( + account = currentAccount, synchronizer = synchronizer, copyToClipboard = { tag, textToCopy -> copyToClipboard( @@ -161,8 +172,12 @@ internal fun ComposeActivity.Navigation() { if (null == synchronizer || null == walletSnapshot || null == spendingKey) { // Display loading indicator } else { + val currentAccount = walletViewModel.getCurrentAccount() Send( - walletSnapshot = walletSnapshot, + accountBalance = + walletSnapshot.balanceByAccountUuid( + accountUuid = currentAccount.accountUuid + ), sendState = walletViewModel.sendState.collectAsStateWithLifecycle().value, onSend = { walletViewModel.send(it) @@ -292,7 +307,9 @@ internal fun ComposeActivity.Navigation() { if (null == synchronizer) { // Display loading indicator } else { + val currentAccount = walletViewModel.getCurrentAccount() Transactions( + account = currentAccount, synchronizer = synchronizer, onBack = { navController.popBackStackJustOnce(TRANSACTIONS) } ) 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 32f00d3f8..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 @@ -7,11 +7,11 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment +import cash.z.ecc.android.sdk.demoapp.CURRENT_ZIP_32_ACCOUNT_INDEX import cash.z.ecc.android.sdk.demoapp.databinding.FragmentGetAddressBinding import cash.z.ecc.android.sdk.demoapp.ext.requireApplicationContext import cash.z.ecc.android.sdk.demoapp.util.ProvideAddressBenchmarkTrace import cash.z.ecc.android.sdk.demoapp.util.fromResources -import cash.z.ecc.android.sdk.model.Account import cash.z.ecc.android.sdk.model.UnifiedFullViewingKey import cash.z.ecc.android.sdk.model.ZcashNetwork import cash.z.ecc.android.sdk.tool.DerivationTool @@ -30,23 +30,25 @@ class GetAddressFragment : BaseDemoFragment() { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { launch { sharedViewModel.synchronizerFlow.filterNotNull().collect { synchronizer -> + val account = synchronizer.getAccounts()[CURRENT_ZIP_32_ACCOUNT_INDEX.toInt()] + binding.unifiedAddress.apply { reportTraceEvent(ProvideAddressBenchmarkTrace.Event.UNIFIED_ADDRESS_START) - val uaddress = synchronizer.getUnifiedAddress(Account.DEFAULT) + val uaddress = synchronizer.getUnifiedAddress(account) reportTraceEvent(ProvideAddressBenchmarkTrace.Event.UNIFIED_ADDRESS_END) text = uaddress setOnClickListener { copyToClipboard(uaddress) } } binding.saplingAddress.apply { reportTraceEvent(ProvideAddressBenchmarkTrace.Event.SAPLING_ADDRESS_START) - val sapling = synchronizer.getSaplingAddress(Account.DEFAULT) + val sapling = synchronizer.getSaplingAddress(account) reportTraceEvent(ProvideAddressBenchmarkTrace.Event.SAPLING_ADDRESS_END) text = sapling setOnClickListener { copyToClipboard(sapling) } } binding.transparentAddress.apply { reportTraceEvent(ProvideAddressBenchmarkTrace.Event.TRANSPARENT_ADDRESS_START) - val transparent = synchronizer.getTransparentAddress(Account.DEFAULT) + val transparent = synchronizer.getTransparentAddress(account) reportTraceEvent(ProvideAddressBenchmarkTrace.Event.TRANSPARENT_ADDRESS_END) text = transparent setOnClickListener { copyToClipboard(transparent) } 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 4469f3576..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 @@ -10,6 +10,7 @@ import cash.z.ecc.android.bip39.Mnemonics import cash.z.ecc.android.bip39.toSeed import cash.z.ecc.android.sdk.Synchronizer import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment +import cash.z.ecc.android.sdk.demoapp.CURRENT_ZIP_32_ACCOUNT_INDEX import cash.z.ecc.android.sdk.demoapp.databinding.FragmentGetBalanceBinding import cash.z.ecc.android.sdk.demoapp.ext.requireApplicationContext import cash.z.ecc.android.sdk.demoapp.util.SyncBlockchainBenchmarkTrace @@ -18,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 @@ -78,10 +79,11 @@ class GetBalanceFragment : BaseDemoFragment() { DerivationTool.getInstance().deriveUnifiedSpendingKey( seed, network, - Account.DEFAULT + 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 @@ -116,10 +118,12 @@ class GetBalanceFragment : BaseDemoFragment() { sharedViewModel.synchronizerFlow .filterNotNull() .flatMapLatest { - it.saplingBalances.combine(it.exchangeRateUsd) { b, r -> - b?.let { - b to - r.currencyConversion + val account = it.getAccounts()[CURRENT_ZIP_32_ACCOUNT_INDEX.toInt()] + it.walletBalances.combine(it.exchangeRateUsd) { balances, rate -> + balances?.let { + val walletBalance = balances[account.accountUuid]!!.sapling + walletBalance to + rate.currencyConversion ?.priceOfZec ?.toBigDecimal() } @@ -131,23 +135,28 @@ class GetBalanceFragment : BaseDemoFragment() { sharedViewModel.synchronizerFlow .filterNotNull() .flatMapLatest { - it.orchardBalances.combine(it.exchangeRateUsd) { b, r -> - b?.let { - b to - r.currencyConversion?.priceOfZec?.toBigDecimal() + val account = it.getAccounts()[CURRENT_ZIP_32_ACCOUNT_INDEX.toInt()] + it.walletBalances.combine(it.exchangeRateUsd) { balances, rate -> + balances?.let { + val walletBalance = balances[account.accountUuid]!!.orchard + walletBalance to + rate.currencyConversion + ?.priceOfZec + ?.toBigDecimal() } } } .collect { onOrchardBalance(it) } - } - launch { + sharedViewModel.synchronizerFlow .filterNotNull() .flatMapLatest { - it.transparentBalance.combine(it.exchangeRateUsd) { b, r -> - b?.let { - b to - r.currencyConversion + val account = it.getAccounts()[CURRENT_ZIP_32_ACCOUNT_INDEX.toInt()] + it.walletBalances.combine(it.exchangeRateUsd) { balances, rate -> + balances?.let { + val walletBalance = balances[account.accountUuid]!!.unshielded + walletBalance to + rate.currencyConversion ?.priceOfZec ?.toBigDecimal() } @@ -208,9 +217,24 @@ class GetBalanceFragment : BaseDemoFragment() { binding.textStatus.text = "Status: $status" sharedViewModel.synchronizerFlow.value?.let { synchronizer -> val rate = synchronizer.exchangeRateUsd.value.currencyConversion?.priceOfZec?.toBigDecimal() - onOrchardBalance(synchronizer.orchardBalances.value?.let { Pair(it, rate) }) - onSaplingBalance(synchronizer.saplingBalances.value?.let { Pair(it, rate) }) - onTransparentBalance(synchronizer.transparentBalance.value?.let { Pair(it, rate) }) + viewLifecycleOwner.lifecycleScope.launch { + 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 a21501495..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 @@ -10,12 +10,12 @@ import androidx.recyclerview.widget.LinearLayoutManager import cash.z.ecc.android.sdk.SdkSynchronizer import cash.z.ecc.android.sdk.Synchronizer import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment +import cash.z.ecc.android.sdk.demoapp.CURRENT_ZIP_32_ACCOUNT_INDEX import cash.z.ecc.android.sdk.demoapp.databinding.FragmentListUtxosBinding import cash.z.ecc.android.sdk.demoapp.ext.requireApplicationContext import cash.z.ecc.android.sdk.demoapp.util.fromResources import cash.z.ecc.android.sdk.demoapp.util.mainActivity import cash.z.ecc.android.sdk.internal.Twig -import cash.z.ecc.android.sdk.model.Account import cash.z.ecc.android.sdk.model.BlockHeight import cash.z.ecc.android.sdk.model.PercentDecimal import cash.z.ecc.android.sdk.model.TransactionOverview @@ -188,7 +188,7 @@ class ListUtxosFragment : BaseDemoFragment() { .filterNotNull() .collect { binding.inputAddress.setText( - it.getTransparentAddress(Account.DEFAULT) + 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 170cff3c5..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 @@ -10,6 +10,7 @@ import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import cash.z.ecc.android.sdk.Synchronizer import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment +import cash.z.ecc.android.sdk.demoapp.CURRENT_ZIP_32_ACCOUNT_INDEX import cash.z.ecc.android.sdk.demoapp.DemoConstants import cash.z.ecc.android.sdk.demoapp.databinding.FragmentSendBinding import cash.z.ecc.android.sdk.demoapp.util.mainActivity @@ -22,6 +23,7 @@ import cash.z.ecc.android.sdk.model.WalletBalance import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.launch /** @@ -96,7 +98,15 @@ class SendFragment : BaseDemoFragment() { launch { sharedViewModel.synchronizerFlow .filterNotNull() - .flatMapLatest { it.saplingBalances } + .flatMapLatest { + val account = it.getAccounts()[CURRENT_ZIP_32_ACCOUNT_INDEX.toInt()] + it.walletBalances.mapLatest { balances -> + balances?.let { + val walletBalance = balances[account.accountUuid]!!.sapling + walletBalance + } + } + } .collect { onBalance(it) } } } @@ -146,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 36f5a89dc..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 @@ -4,20 +4,23 @@ import cash.z.ecc.android.sdk.Synchronizer import cash.z.ecc.android.sdk.block.processor.CompactBlockProcessor 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.WalletBalanceFixture +import cash.z.ecc.android.sdk.fixture.AccountBalanceFixture +import cash.z.ecc.android.sdk.fixture.AccountFixture +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 cash.z.ecc.android.sdk.model.WalletBalance -import cash.z.ecc.android.sdk.model.Zatoshi import java.math.BigDecimal @Suppress("MagicNumber") object WalletSnapshotFixture { val STATUS = Synchronizer.Status.SYNCED val PROGRESS = PercentDecimal.ZERO_PERCENT - val TRANSPARENT_BALANCE: Zatoshi = Zatoshi(8) - val ORCHARD_BALANCE: WalletBalance = WalletBalanceFixture.new(Zatoshi(5), Zatoshi(2), Zatoshi(1)) - val SAPLING_BALANCE: WalletBalance = WalletBalanceFixture.new(Zatoshi(4), Zatoshi(4), Zatoshi(2)) val EXCHANGE_RATE_USD: BigDecimal = BigDecimal(37.4850) + val ACCOUNT = AccountFixture.new() + val WALLET_BALANCES: Map = + mapOf( + ACCOUNT.accountUuid to AccountBalanceFixture.new() + ) // Should fill in with non-empty values for better example values in tests and UI previews @Suppress("LongParameterList") @@ -29,20 +32,16 @@ object WalletSnapshotFixture { null, null ), - orchardBalance: WalletBalance = ORCHARD_BALANCE, - saplingBalance: WalletBalance = SAPLING_BALANCE, - transparentBalance: Zatoshi = TRANSPARENT_BALANCE, + walletBalances: Map = WALLET_BALANCES, exchangeRateUsd: BigDecimal? = EXCHANGE_RATE_USD, progress: PercentDecimal = PROGRESS, synchronizerError: SynchronizerError? = null ) = WalletSnapshot( - status, - processorInfo, - orchardBalance, - saplingBalance, - transparentBalance, - 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/common/Constants.kt b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/ui/common/Constants.kt deleted file mode 100644 index 51a13bc42..000000000 --- a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/ui/common/Constants.kt +++ /dev/null @@ -1,11 +0,0 @@ -package cash.z.ecc.android.sdk.demoapp.ui.common - -import kotlin.time.Duration.Companion.seconds - -// Recommended timeout for Android configuration changes to keep Kotlin Flow from restarting -val ANDROID_STATE_FLOW_TIMEOUT = 5.seconds - -/** - * A tiny weight, useful for spacers to fill an empty space. - */ -const val MINIMAL_WEIGHT = 0.0001f diff --git a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/ui/screen/addresses/view/AddressesView.kt b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/ui/screen/addresses/view/AddressesView.kt index c43c7da5a..6a14440de 100644 --- a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/ui/screen/addresses/view/AddressesView.kt +++ b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/ui/screen/addresses/view/AddressesView.kt @@ -27,6 +27,7 @@ import androidx.compose.ui.unit.dp import cash.z.ecc.android.sdk.Synchronizer import cash.z.ecc.android.sdk.demoapp.R import cash.z.ecc.android.sdk.internal.Twig +import cash.z.ecc.android.sdk.model.Account import cash.z.ecc.android.sdk.model.WalletAddresses import kotlinx.coroutines.flow.flow @@ -48,6 +49,7 @@ private fun ComposablePreview() { @Suppress("ktlint:standard:function-naming") fun Addresses( synchronizer: Synchronizer, + account: Account, copyToClipboard: (String, String) -> Unit, onBack: () -> Unit, snackbarHostState: SnackbarHostState @@ -62,7 +64,7 @@ fun Addresses( flow { emit( runCatching { - WalletAddresses.new(synchronizer) + WalletAddresses.new(account, synchronizer) }.onFailure { Twig.warn { "Wait until the SDK starts providing the addresses" } }.getOrNull() diff --git a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/ui/screen/balance/view/BalanceView.kt b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/ui/screen/balance/view/BalanceView.kt index 131de428f..fea810761 100644 --- a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/ui/screen/balance/view/BalanceView.kt +++ b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/ui/screen/balance/view/BalanceView.kt @@ -23,20 +23,22 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import cash.z.ecc.android.sdk.demoapp.R -import cash.z.ecc.android.sdk.demoapp.fixture.WalletSnapshotFixture import cash.z.ecc.android.sdk.demoapp.ui.screen.home.viewmodel.SendState -import cash.z.ecc.android.sdk.demoapp.ui.screen.home.viewmodel.WalletSnapshot import cash.z.ecc.android.sdk.ext.convertZatoshiToZec import cash.z.ecc.android.sdk.ext.toUsdString +import cash.z.ecc.android.sdk.fixture.AccountBalanceFixture +import cash.z.ecc.android.sdk.model.AccountBalance import cash.z.ecc.android.sdk.model.toZecString +import java.math.BigDecimal @Preview(name = "Balance") -@Suppress("ktlint:standard:function-naming") +@Suppress("ktlint:standard:function-naming", "MagicNumber") @Composable private fun ComposablePreview() { MaterialTheme { Balance( - walletSnapshot = WalletSnapshotFixture.new(), + exchangeRateUsd = BigDecimal(50), + accountBalance = AccountBalanceFixture.new(), sendState = SendState.None, onBack = {}, onShieldFunds = {}, @@ -46,9 +48,10 @@ private fun ComposablePreview() { } @Composable -@Suppress("ktlint:standard:function-naming") +@Suppress("ktlint:standard:function-naming", "LongParameterList") fun Balance( - walletSnapshot: WalletSnapshot, + exchangeRateUsd: BigDecimal?, + accountBalance: AccountBalance, sendState: SendState, onShieldFunds: () -> Unit, onBack: () -> Unit, @@ -63,8 +66,9 @@ fun Balance( BalanceMainContent( paddingValues = paddingValues, - walletSnapshot, - sendState, + exchangeRateUsd = exchangeRateUsd, + accountBalance = accountBalance, + sendState = sendState, onShieldFunds = onShieldFunds ) } @@ -103,8 +107,9 @@ private fun BalanceTopAppBar( @Composable @Suppress("ktlint:standard:function-naming") private fun BalanceMainContent( + exchangeRateUsd: BigDecimal?, + accountBalance: AccountBalance, paddingValues: PaddingValues, - walletSnapshot: WalletSnapshot, sendState: SendState, onShieldFunds: () -> Unit ) { @@ -117,16 +122,16 @@ private fun BalanceMainContent( Text( stringResource( id = R.string.balance_available_amount_format, - walletSnapshot.orchardBalance.available.toZecString(), - walletSnapshot.exchangeRateUsd?.multiply(walletSnapshot.orchardBalance.available.convertZatoshiToZec()) + accountBalance.orchard.available.toZecString(), + exchangeRateUsd?.multiply(accountBalance.orchard.available.convertZatoshiToZec()) .toUsdString() ) ) Text( stringResource( id = R.string.balance_pending_amount_format, - walletSnapshot.orchardBalance.pending.toZecString(), - walletSnapshot.exchangeRateUsd?.multiply(walletSnapshot.orchardBalance.pending.convertZatoshiToZec()) + accountBalance.orchard.pending.toZecString(), + exchangeRateUsd?.multiply(accountBalance.orchard.pending.convertZatoshiToZec()) .toUsdString() ) ) @@ -137,16 +142,16 @@ private fun BalanceMainContent( Text( stringResource( id = R.string.balance_available_amount_format, - walletSnapshot.saplingBalance.available.toZecString(), - walletSnapshot.exchangeRateUsd?.multiply(walletSnapshot.saplingBalance.available.convertZatoshiToZec()) + accountBalance.sapling.available.toZecString(), + exchangeRateUsd?.multiply(accountBalance.sapling.available.convertZatoshiToZec()) .toUsdString() ) ) Text( stringResource( id = R.string.balance_pending_amount_format, - walletSnapshot.saplingBalance.pending.toZecString(), - walletSnapshot.exchangeRateUsd?.multiply(walletSnapshot.saplingBalance.pending.convertZatoshiToZec()) + accountBalance.sapling.pending.toZecString(), + exchangeRateUsd?.multiply(accountBalance.sapling.pending.convertZatoshiToZec()) .toUsdString() ) ) @@ -157,14 +162,14 @@ private fun BalanceMainContent( Text( stringResource( id = R.string.balance_available_amount_format, - walletSnapshot.transparentBalance.toZecString(), - walletSnapshot.exchangeRateUsd?.multiply(walletSnapshot.transparentBalance.convertZatoshiToZec()) + accountBalance.unshielded.toZecString(), + exchangeRateUsd?.multiply(accountBalance.unshielded.convertZatoshiToZec()) .toUsdString() ) ) // This check is not entirely correct - it does not calculate the resulting fee with the new Proposal API - if (walletSnapshot.transparentBalance.value > 0L) { + if (accountBalance.unshielded.value > 0L) { // Note this implementation does not guard against multiple clicks Button(onClick = onShieldFunds) { Text(stringResource(id = R.string.action_shield)) diff --git a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/ui/screen/home/view/HomeView.kt b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/ui/screen/home/view/HomeView.kt index 2b3f888d9..7b0e30914 100644 --- a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/ui/screen/home/view/HomeView.kt +++ b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/ui/screen/home/view/HomeView.kt @@ -2,6 +2,8 @@ package cash.z.ecc.android.sdk.demoapp.ui.screen.home.view import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll @@ -25,11 +27,16 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import cash.z.ecc.android.sdk.Synchronizer import cash.z.ecc.android.sdk.demoapp.R import cash.z.ecc.android.sdk.demoapp.fixture.WalletSnapshotFixture import cash.z.ecc.android.sdk.demoapp.ui.common.DisableScreenTimeout import cash.z.ecc.android.sdk.demoapp.ui.screen.home.viewmodel.WalletSnapshot +import cash.z.ecc.android.sdk.fixture.AccountFixture +import cash.z.ecc.android.sdk.model.Account +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf @Preview(name = "Home") @Composable @@ -37,14 +44,16 @@ import cash.z.ecc.android.sdk.demoapp.ui.screen.home.viewmodel.WalletSnapshot private fun ComposablePreviewHome() { MaterialTheme { Home( - WalletSnapshotFixture.new(), + allAccounts = persistentListOf(AccountFixture.new()), + currentAccount = AccountFixture.new(), + walletSnapshot = WalletSnapshotFixture.new(), isTestnet = true, goBalance = {}, goSend = {}, goAddressDetails = {}, goTransactions = {}, - goTestnetFaucet = {}, goServer = {}, + goTestnetFaucet = {}, resetSdk = {}, rewind = {}, ) @@ -54,6 +63,8 @@ private fun ComposablePreviewHome() { @Composable @Suppress("LongParameterList", "ktlint:standard:function-naming") fun Home( + allAccounts: ImmutableList, + currentAccount: Account, walletSnapshot: WalletSnapshot, isTestnet: Boolean, goBalance: () -> Unit, @@ -75,12 +86,14 @@ fun Home( }) { paddingValues -> HomeMainContent( paddingValues = paddingValues, - walletSnapshot, + walletSnapshot = walletSnapshot, goBalance = goBalance, goSend = goSend, goServer = goServer, goAddressDetails = goAddressDetails, - goTransactions = goTransactions + goTransactions = goTransactions, + currentAccount = currentAccount, + allAccounts = allAccounts ) } } @@ -154,6 +167,8 @@ private fun DebugMenu( @Composable @Suppress("LongParameterList", "ktlint:standard:function-naming") private fun HomeMainContent( + allAccounts: ImmutableList, + currentAccount: Account, paddingValues: PaddingValues, walletSnapshot: WalletSnapshot, goBalance: () -> Unit, @@ -198,5 +213,13 @@ private fun HomeMainContent( // is different from a longer sync. DisableScreenTimeout() } + + Spacer(modifier = Modifier.height(12.dp)) + + Text(text = stringResource(id = R.string.home_accounts, allAccounts)) + + Spacer(modifier = Modifier.height(6.dp)) + + Text(text = stringResource(id = R.string.home_current_account, currentAccount)) } } 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 08370fa46..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,33 +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.AccountBalance +import cash.z.ecc.android.sdk.model.AccountUuid 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 java.math.BigDecimal data class WalletSnapshot( val status: Synchronizer.Status, val processorInfo: CompactBlockProcessor.ProcessorInfo, - val orchardBalance: WalletBalance, - val saplingBalance: WalletBalance, - val transparentBalance: Zatoshi, + val walletBalances: Map, val exchangeRateUsd: BigDecimal?, val progress: PercentDecimal, val synchronizerError: SynchronizerError? ) { - // Note: the wallet is effectively empty if it cannot cover the miner's fee - // This check is not entirely correct - it does not calculate the resulting fee with the new Proposal API - val hasFunds = saplingBalance.available.value > 0L - - val hasSaplingBalance = saplingBalance.total.value > 0L - - val isSendEnabled: Boolean get() = status == Synchronizer.Status.SYNCED && hasFunds + fun balanceByAccountUuid(accountUuid: AccountUuid): AccountBalance { + return walletBalances[accountUuid] ?: error("Balance of account? $accountUuid could not be found.") + } } - -fun WalletSnapshot.totalBalance() = orchardBalance.total + saplingBalance.total + transparentBalance - -// Note that considering both to be spendable is subject to change. -// The user experience could be confusing, and in the future we might prefer to ask users -// to transfer their balance to the latest balance type to make it spendable. -fun WalletSnapshot.spendableBalance() = orchardBalance.available + saplingBalance.available diff --git a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/ui/screen/home/viewmodel/WalletViewModel.kt b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/ui/screen/home/viewmodel/WalletViewModel.kt index 1053a16b6..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 @@ -10,15 +10,18 @@ import cash.z.ecc.android.sdk.Synchronizer import cash.z.ecc.android.sdk.WalletCoordinator import cash.z.ecc.android.sdk.WalletInitMode import cash.z.ecc.android.sdk.block.processor.CompactBlockProcessor +import cash.z.ecc.android.sdk.demoapp.ANDROID_STATE_FLOW_TIMEOUT +import cash.z.ecc.android.sdk.demoapp.CURRENT_ZIP_32_ACCOUNT_INDEX import cash.z.ecc.android.sdk.demoapp.ext.defaultForNetwork import cash.z.ecc.android.sdk.demoapp.getInstance import cash.z.ecc.android.sdk.demoapp.preference.EncryptedPreferenceKeys import cash.z.ecc.android.sdk.demoapp.preference.EncryptedPreferenceSingleton -import cash.z.ecc.android.sdk.demoapp.ui.common.ANDROID_STATE_FLOW_TIMEOUT import cash.z.ecc.android.sdk.demoapp.ui.common.throttle 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 @@ -26,7 +29,6 @@ import cash.z.ecc.android.sdk.model.PersistableWallet import cash.z.ecc.android.sdk.model.Proposal import cash.z.ecc.android.sdk.model.TransactionSubmitResult import cash.z.ecc.android.sdk.model.WalletAddresses -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.ZecSend @@ -99,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 = Account.DEFAULT - ) + getCurrentAccount().hdAccountIndex?.let { accountIndex -> + DerivationTool.getInstance().deriveUnifiedSpendingKey( + seed = bip39Seed, + network = secretState.network, + accountIndex = accountIndex + ) + } }.stateIn( viewModelScope, SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT), @@ -137,7 +141,7 @@ class WalletViewModel(application: Application) : AndroidViewModel(application) .filterNotNull() .map { runCatching { - WalletAddresses.new(it) + WalletAddresses.new(getCurrentAccount(), it) }.onFailure { Twig.warn { "Wait until the SDK starts providing the addresses" } }.getOrNull() @@ -209,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) } } @@ -232,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() @@ -257,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() @@ -285,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 @@ -353,6 +360,37 @@ class WalletViewModel(application: Application) : AndroidViewModel(application) } } + fun getAccounts(): List { + val synchronizer = synchronizer.value + + return if (null != synchronizer) { + runBlocking { + kotlin.runCatching { + synchronizer.getAccounts() + }.onFailure { + Twig.error(it) { "Failed to get wallet accounts" } + }.getOrThrow() + } + } else { + error("Unable get wallet accounts.") + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + val accounts: StateFlow> = + synchronizer + .filterNotNull() + .flatMapLatest { + it.accountsFlow.filterNotNull() + } + .stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT), + emptyList() + ) + + fun getCurrentAccount(): Account = getAccounts()[CURRENT_ZIP_32_ACCOUNT_INDEX.toInt()] + companion object { private const val QUICK_REWIND_BLOCKS = 100 } @@ -454,7 +492,7 @@ private fun Synchronizer.toCommonError(): Flow = } // No good way around needing magic numbers for the indices -@Suppress("MagicNumber") +@Suppress("MagicNumber", "UNCHECKED_CAST") private fun Synchronizer.toWalletSnapshot() = combine( // 0 @@ -462,34 +500,23 @@ private fun Synchronizer.toWalletSnapshot() = // 1 processorInfo, // 2 - orchardBalances, + walletBalances.filterNotNull(), // 3 - saplingBalances, - // 4 - transparentBalance, - // 5 exchangeRateUsd, - // 6 + // 4 progress, - // 7 + // 5 toCommonError() ) { flows -> - val orchardBalance = flows[2] as WalletBalance? - val saplingBalance = flows[3] as WalletBalance? - val transparentBalance = flows[4] as Zatoshi? - - @Suppress("UNCHECKED_CAST") - val exchangeRateUsd = flows[5] as ObserveFiatCurrencyResult - val progressPercentDecimal = (flows[6] as PercentDecimal) + val exchangeRateUsd = flows[3] as ObserveFiatCurrencyResult + val progressPercentDecimal = (flows[4] as PercentDecimal) WalletSnapshot( flows[0] as Synchronizer.Status, flows[1] as CompactBlockProcessor.ProcessorInfo, - orchardBalance ?: WalletBalance(Zatoshi(0), Zatoshi(0), Zatoshi(0)), - saplingBalance ?: WalletBalance(Zatoshi(0), Zatoshi(0), Zatoshi(0)), - transparentBalance ?: Zatoshi(0), + flows[2] as Map, exchangeRateUsd.currencyConversion?.priceOfZec?.toBigDecimal(), progressPercentDecimal, - flows[7] as SynchronizerError? + flows[5] as SynchronizerError? ) } diff --git a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/ui/screen/send/view/SendView.kt b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/ui/screen/send/view/SendView.kt index 555403938..8fa1723f8 100644 --- a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/ui/screen/send/view/SendView.kt +++ b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/ui/screen/send/view/SendView.kt @@ -36,13 +36,13 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import cash.z.ecc.android.sdk.demoapp.MINIMAL_WEIGHT import cash.z.ecc.android.sdk.demoapp.R -import cash.z.ecc.android.sdk.demoapp.fixture.WalletSnapshotFixture -import cash.z.ecc.android.sdk.demoapp.ui.common.MINIMAL_WEIGHT import cash.z.ecc.android.sdk.demoapp.ui.screen.home.viewmodel.SendState -import cash.z.ecc.android.sdk.demoapp.ui.screen.home.viewmodel.WalletSnapshot import cash.z.ecc.android.sdk.demoapp.util.fromResources +import cash.z.ecc.android.sdk.fixture.AccountBalanceFixture import cash.z.ecc.android.sdk.fixture.WalletFixture +import cash.z.ecc.android.sdk.model.AccountBalance import cash.z.ecc.android.sdk.model.Memo import cash.z.ecc.android.sdk.model.MonetarySeparators import cash.z.ecc.android.sdk.model.Proposal @@ -60,7 +60,7 @@ import java.util.Locale private fun ComposablePreview() { MaterialTheme { Send( - walletSnapshot = WalletSnapshotFixture.new(), + accountBalance = AccountBalanceFixture.new(), sendState = SendState.None, onSend = {}, onGetProposal = {}, @@ -75,7 +75,7 @@ private fun ComposablePreview() { @Composable @Suppress("ktlint:standard:function-naming", "LongParameterList") fun Send( - walletSnapshot: WalletSnapshot, + accountBalance: AccountBalance, sendState: SendState, onSend: (ZecSend) -> Unit, onGetProposal: (ZecSend) -> Unit, @@ -89,7 +89,7 @@ fun Send( }) { paddingValues -> SendMainContent( paddingValues = paddingValues, - walletSnapshot = walletSnapshot, + accountBalance = accountBalance, sendState = sendState, onSend = onSend, onGetProposal = onGetProposal, @@ -123,7 +123,7 @@ private fun SendTopAppBar(onBack: () -> Unit) { @Suppress("LongMethod", "ktlint:standard:function-naming", "LongParameterList") private fun SendMainContent( paddingValues: PaddingValues, - walletSnapshot: WalletSnapshot, + accountBalance: AccountBalance, sendState: SendState, onSend: (ZecSend) -> Unit, onGetProposal: (ZecSend) -> Unit, @@ -157,7 +157,7 @@ private fun SendMainContent( ) { Text(text = stringResource(id = R.string.send_available_balance)) Row(Modifier.fillMaxWidth()) { - Text(text = walletSnapshot.saplingBalance.available.toZecString()) + Text(text = accountBalance.sapling.available.toZecString()) } TextField( diff --git a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/ui/screen/transactions/view/TransactionsView.kt b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/ui/screen/transactions/view/TransactionsView.kt index 7fcb817bd..34722928c 100644 --- a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/ui/screen/transactions/view/TransactionsView.kt +++ b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/ui/screen/transactions/view/TransactionsView.kt @@ -32,6 +32,7 @@ import cash.z.ecc.android.sdk.Synchronizer import cash.z.ecc.android.sdk.demoapp.R import cash.z.ecc.android.sdk.demoapp.util.toTransactionState import cash.z.ecc.android.sdk.internal.Twig +import cash.z.ecc.android.sdk.model.Account import cash.z.ecc.android.sdk.model.WalletAddresses import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList @@ -43,6 +44,7 @@ import kotlinx.coroutines.launch @Composable @Suppress("ktlint:standard:function-naming", "standard:function-naming") fun Transactions( + account: Account, synchronizer: Synchronizer, onBack: () -> Unit ) { @@ -53,7 +55,9 @@ fun Transactions( val stateFlow by remember(synchronizer) { mutableStateOf( - flow { emit(WalletAddresses.new(synchronizer)) }.catch { emit(null) } + flow { + emit(WalletAddresses.new(account, synchronizer)) + }.catch { emit(null) } ) } val walletAddresses by stateFlow.collectAsStateWithLifecycle(initialValue = null) diff --git a/demo-app/src/main/res/menu/activity_main_drawer.xml b/demo-app/src/main/res/menu/activity_main_drawer.xml index 0c15f8151..41dcdd581 100644 --- a/demo-app/src/main/res/menu/activity_main_drawer.xml +++ b/demo-app/src/main/res/menu/activity_main_drawer.xml @@ -12,7 +12,7 @@ + android:title="@string/menu_keys" /> - Pending Unknown @@ -14,10 +13,10 @@ Home - Get Private Key Get Address Server Get Balance + Private Keys Get Latest Height Get Block Get Block Range @@ -53,6 +52,14 @@ Generate a new random secret phrase Restore wallet + + The wallet\'s accounts: + %1$s + + + Selected account: + %1$s + Status: %1$s Progress: %1$.2f%% @@ -101,5 +108,4 @@ Fee: Unavailable Status: - diff --git a/gradle.properties b/gradle.properties index 8906f8452..d941a70f5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -25,7 +25,7 @@ ZCASH_ASCII_GPG_KEY= # Configures whether release is an unstable snapshot, therefore published to the snapshot repository. IS_SNAPSHOT=true -LIBRARY_VERSION=2.2.6 +LIBRARY_VERSION=2.2.7 # Kotlin compiler warnings can be considered errors, failing the build. ZCASH_IS_TREAT_WARNINGS_AS_ERRORS=true diff --git a/sdk-incubator-lib/src/androidTest/kotlin/cash/z/ecc/android/sdk/internal/DerivationToolImplTest.kt b/sdk-incubator-lib/src/androidTest/kotlin/cash/z/ecc/android/sdk/internal/DerivationToolImplTest.kt index 62ea17b34..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 @@ -2,8 +2,8 @@ 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.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.test.runTest import org.junit.Test @@ -14,7 +14,7 @@ import kotlin.test.assertEquals class DerivationToolImplTest { private val seedPhrase = WalletFixture.Alice.seedPhrase private val network = ZcashNetwork.Mainnet - private val account = Account.DEFAULT + private val accountIndex = Zip32AccountIndex.new(0L) @OptIn(ExperimentalEncodingApi::class) @Test @@ -25,7 +25,7 @@ class DerivationToolImplTest { contextString = CONTEXT.toByteArray(), seed = seedPhrase.toByteArray(), network = network, - account = account, + accountIndex = accountIndex, ) assertEquals("byyNHiMfj8N2tiCHc4Mv/0ts0IuUqDPe99MvW8B03IY=", Base64.encode(key)) } @@ -63,7 +63,7 @@ class DerivationToolImplTest { contextString = "Zcash test vectors".toByteArray(), seed = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f".hexToByteArray(), network = network, - account = account, + accountIndex = accountIndex, ) assertEquals("bf60078362a09234fcbc6bf6c8a87bde9fc73776bf93f37adbcc439a85574a9a", secretKey.toHex()) } diff --git a/sdk-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/AccountBalanceFixture.kt b/sdk-incubator-lib/src/main/java/cash/z/ecc/android/sdk/fixture/AccountBalanceFixture.kt new file mode 100644 index 000000000..ab5abec8f --- /dev/null +++ b/sdk-incubator-lib/src/main/java/cash/z/ecc/android/sdk/fixture/AccountBalanceFixture.kt @@ -0,0 +1,22 @@ +package cash.z.ecc.android.sdk.fixture + +import cash.z.ecc.android.sdk.model.AccountBalance +import cash.z.ecc.android.sdk.model.WalletBalance +import cash.z.ecc.android.sdk.model.Zatoshi + +@Suppress("MagicNumber") +object AccountBalanceFixture { + val TRANSPARENT_BALANCE: Zatoshi = Zatoshi(8) + val SAPLING_BALANCE: WalletBalance = WalletBalanceFixture.new(Zatoshi(4), Zatoshi(4), Zatoshi(2)) + val ORCHARD_BALANCE: WalletBalance = WalletBalanceFixture.new(Zatoshi(5), Zatoshi(2), Zatoshi(1)) + + fun new( + orchardBalance: WalletBalance = ORCHARD_BALANCE, + saplingBalance: WalletBalance = SAPLING_BALANCE, + transparentBalance: Zatoshi = TRANSPARENT_BALANCE + ) = AccountBalance( + saplingBalance, + orchardBalance, + transparentBalance + ) +} 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 1dc303168..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 @@ -1,10 +1,9 @@ package cash.z.ecc.android.sdk.fixture -import cash.z.ecc.android.bip39.Mnemonics 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 cash.z.ecc.android.sdk.tool.DerivationTool +import java.util.UUID /** * Provides two default wallets, making it easy to test sending funds back and forth between them. @@ -12,22 +11,17 @@ import cash.z.ecc.android.sdk.tool.DerivationTool sealed class WalletFixture { abstract val seedPhrase: String + abstract val accounts: List + abstract fun getBirthday(zcashNetwork: ZcashNetwork): BlockHeight abstract fun getAddresses(zcashNetwork: ZcashNetwork): Addresses - suspend fun getUnifiedSpendingKey( - seed: String = seedPhrase, - network: ZcashNetwork, - account: Account = Account.DEFAULT - ) = DerivationTool.getInstance().deriveUnifiedSpendingKey( - Mnemonics.MnemonicCode(seed).toEntropy(), - network, - account - ) - @Suppress("MaxLineLength") data object Ben : WalletFixture() { + override val accounts: List + get() = listOf(AccountFixture.new(accountUuid = UUID.fromString("52175368-821a-4664-8a7c-6a75d850f71c"))) + override val seedPhrase: String get() = "kitchen renew wide common vague fold vacuum tilt amazing pear square gossip jewel month tree" + @@ -62,6 +56,9 @@ sealed class WalletFixture { @Suppress("MaxLineLength") data object Alice : WalletFixture() { + override val accounts: List + get() = listOf(AccountFixture.new(accountUuid = UUID.fromString("8a204240-73a5-4e7a-93c2-a6e05711a000"))) + override val seedPhrase: String get() = "wish puppy smile loan doll curve hole maze file ginger hair nose key relax knife witness cannon" + diff --git a/sdk-incubator-lib/src/main/java/cash/z/ecc/android/sdk/model/WalletAddresses.kt b/sdk-incubator-lib/src/main/java/cash/z/ecc/android/sdk/model/WalletAddresses.kt index 674247312..6abdd7cc6 100644 --- a/sdk-incubator-lib/src/main/java/cash/z/ecc/android/sdk/model/WalletAddresses.kt +++ b/sdk-incubator-lib/src/main/java/cash/z/ecc/android/sdk/model/WalletAddresses.kt @@ -11,20 +11,23 @@ data class WalletAddresses( override fun toString() = "WalletAddresses" companion object { - suspend fun new(synchronizer: Synchronizer): WalletAddresses { + suspend fun new( + account: Account, + synchronizer: Synchronizer + ): WalletAddresses { val unified = WalletAddress.Unified.new( - synchronizer.getUnifiedAddress(Account.DEFAULT) + synchronizer.getUnifiedAddress(account) ) val saplingAddress = WalletAddress.Sapling.new( - synchronizer.getSaplingAddress(Account.DEFAULT) + synchronizer.getSaplingAddress(account) ) val transparentAddress = WalletAddress.Transparent.new( - synchronizer.getTransparentAddress(Account.DEFAULT) + synchronizer.getTransparentAddress(account) ) return WalletAddresses( 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 cb7b30a9e..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,16 +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.Account +import cash.z.ecc.android.sdk.model.UnifiedSpendingKey import cash.z.ecc.android.sdk.model.ZcashNetwork object WalletFixture { + 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" @@ -20,8 +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, - account: Account = Account.DEFAULT - ) = RustDerivationTool.new().deriveUnifiedSpendingKey(Mnemonics.MnemonicCode(seed).toEntropy(), network, account) + 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 2ef137bd2..0c004dd66 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 @@ -2,12 +2,14 @@ package cash.z.ecc.android.sdk.integration import androidx.test.filters.LargeTest import androidx.test.platform.app.InstrumentationRegistry +import cash.z.ecc.android.bip39.Mnemonics import cash.z.ecc.android.sdk.Synchronizer import cash.z.ecc.android.sdk.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.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 @@ -36,6 +38,7 @@ import java.util.concurrent.CountDownLatch class TestnetIntegrationTest : ScopedTest() { var stopWatch = CountDownLatch(1) val saplingActivation = synchronizer.network.saplingActivationHeight + val account = AccountFixture.new() @Test @Ignore("This test is broken") @@ -68,7 +71,7 @@ class TestnetIntegrationTest : ScopedTest() { @Ignore("This test is broken") fun getAddress() = runBlocking { - assertEquals(address, synchronizer.getUnifiedAddress(Account.DEFAULT)) + assertEquals(address, synchronizer.getUnifiedAddress(AccountFixture.new())) } // This is an extremely slow test; it is disabled so that we can get CI set up @@ -78,8 +81,8 @@ class TestnetIntegrationTest : ScopedTest() { fun testBalance() = runBlocking { var availableBalance: Zatoshi? = null - synchronizer.saplingBalances.onFirst { - availableBalance = it?.available + synchronizer.walletBalances.onFirst { + availableBalance = it?.get(account.accountUuid)?.sapling?.available } synchronizer.status.filter { it == SYNCED }.onFirst { @@ -96,7 +99,7 @@ class TestnetIntegrationTest : ScopedTest() { fun testSpend() = runBlocking { var success = false - synchronizer.saplingBalances.filterNotNull().onEach { + synchronizer.walletBalances.filterNotNull().onEach { success = sendFunds() }.first() log("asserting $success") @@ -104,16 +107,17 @@ class TestnetIntegrationTest : ScopedTest() { } private suspend fun sendFunds(): Boolean { + val account = AccountFixture.new() val spendingKey = DerivationTool.getInstance().deriveUnifiedSpendingKey( seed, synchronizer.network, - Account.DEFAULT + account.hdAccountIndex!! ) log("sending to address") synchronizer.createProposedTransactions( synchronizer.proposeTransfer( - spendingKey.account, + account, toAddress, Zatoshi(10_000L), "first mainnet tx from the SDK" @@ -129,13 +133,18 @@ class TestnetIntegrationTest : ScopedTest() { @Suppress("UnusedPrivateProperty") companion object { - val lightWalletEndpoint = LightWalletEndpoint("lightwalletd.testnet.z.cash", 9087, true) + val lightWalletEndpoint = + LightWalletEndpoint( + host = "lightwalletd.testnet.electriccoin.co", + port = 9067, + isSecure = true + ) private const val BIRTHDAY_HEIGHT = 963150L private const val TARGET_HEIGHT = 663250 private const val SEED_PHRASE = "still champion voice habit trend flight survey between bitter process" + " artefact blind carbon truly provide dizzy crush flush breeze blouse charge solid fish spread" - val seed = "cash.z.ecc.android.sdk.integration.IntegrationTest.seed.value.64bytes".toByteArray() + val seed = Mnemonics.MnemonicCode(SEED_PHRASE).toEntropy() val address = "zs1m30y59wxut4zk9w24d6ujrdnfnl42hpy0ugvhgyhr8s0guszutqhdj05c7j472dndjstulph74m" val toAddress = "zs1vp7kvlqr4n9gpehztr76lcn6skkss9p8keqs3nv8avkdtjrcctrvmk9a7u494kluv756jeee5k0" @@ -147,14 +156,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(seed = seed), // 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 d4d2a19b4..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,9 +6,9 @@ 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.AccountCreateSetupFixture import cash.z.ecc.android.sdk.fixture.LightWalletEndpointFixture import cash.z.ecc.android.sdk.fixture.WalletFixture -import cash.z.ecc.android.sdk.model.Account import cash.z.ecc.android.sdk.model.ZcashNetwork import kotlinx.coroutines.test.runTest import java.util.UUID @@ -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(Account.DEFAULT) + 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 5b5bf15a9..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 @@ -2,7 +2,6 @@ package cash.z.ecc.android.sdk.model import androidx.test.filters.SmallTest import cash.z.ecc.android.sdk.fixture.WalletFixture -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Test import kotlin.test.assertContentEquals @@ -11,41 +10,38 @@ import kotlin.test.assertEquals class UnifiedSpendingKeyTest { @Test @SmallTest - @OptIn(ExperimentalCoroutinesApi::class) fun factory_copies_bytes() = runTest { val spendingKey = WalletFixture.getUnifiedSpendingKey() 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 @SmallTest - @OptIn(ExperimentalCoroutinesApi::class) fun get_copies_bytes() = runTest { 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 @SmallTest - @OptIn(ExperimentalCoroutinesApi::class) 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 ab1a9b13a..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 @@ -1,5 +1,6 @@ package cash.z.ecc.android.sdk.sample +import cash.z.ecc.android.sdk.fixture.AccountFixture import cash.z.ecc.android.sdk.model.Zatoshi import cash.z.ecc.android.sdk.model.ZcashNetwork import cash.z.ecc.android.sdk.util.TestWallet @@ -34,6 +35,9 @@ class ShieldFundsSample { Assert.assertEquals("foo", "${wallet.unifiedAddress} ${wallet.transparentAddress}") // wallet.shieldFunds() - Assert.assertEquals(Zatoshi(5), wallet.synchronizer.saplingBalances.value?.available) + Assert.assertEquals( + Zatoshi(5), + 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 ef9b1907d..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 @@ -1,8 +1,8 @@ package cash.z.ecc.android.sdk.util +import cash.z.ecc.android.sdk.fixture.AccountFixture import cash.z.ecc.android.sdk.internal.deriveUnifiedAddress import cash.z.ecc.android.sdk.internal.jni.RustDerivationTool -import cash.z.ecc.android.sdk.model.Account import cash.z.ecc.android.sdk.model.ZcashNetwork import cash.z.ecc.android.sdk.test.readFileLinesInFlow import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -32,7 +32,11 @@ class AddressGeneratorUtil { .map { seedPhrase -> mnemonics.toSeed(seedPhrase.toCharArray()) }.map { seed -> - RustDerivationTool.new().deriveUnifiedAddress(seed, ZcashNetwork.Mainnet, Account.DEFAULT) + RustDerivationTool.new().deriveUnifiedAddress( + seed = seed, + network = ZcashNetwork.Mainnet, + accountIndex = AccountFixture.new().hdAccountIndex!! + ) }.collect { address -> println("xrxrx2\t$address") assertTrue(address.startsWith("u1")) 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 5c9410a83..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,6 +6,8 @@ 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 @@ -56,24 +58,30 @@ class TestWallet( // Although runBlocking isn't great, this usage is OK because this is only used within the // automated tests - private val account = Account.DEFAULT + private val account = AccountFixture.new() private val context = InstrumentationRegistry.getInstrumentation().context private val seed: ByteArray = Mnemonics.MnemonicCode(seedPhrase).toSeed() private val spendingKey = - runBlocking { RustDerivationTool.new().deriveUnifiedSpendingKey(seed, network = network, account) } + runBlocking { + RustDerivationTool.new().deriveUnifiedSpendingKey( + seed = seed, + network = network, + 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.saplingBalances.value?.available + val available get() = synchronizer.walletBalances.value?.get(account.accountUuid)?.sapling?.available val unifiedAddress = runBlocking { synchronizer.getUnifiedAddress(account) } val transparentAddress = @@ -102,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 @@ -123,8 +132,8 @@ class TestWallet( return this } - suspend fun shieldFunds(): TestWallet { - synchronizer.refreshUtxos(Account.DEFAULT, BlockHeight.new(935000L)).let { count -> + suspend fun shieldFunds(account: Account): TestWallet { + synchronizer.refreshUtxos(account, BlockHeight.new(935000L)).let { count -> Twig.debug { "FOUND $count new UTXOs" } } @@ -132,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..84f36c61d 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? @@ -120,6 +120,24 @@ internal class FakeRustBackend( error("Intentionally not implemented yet.") } + override suspend fun createPcztFromProposal( + accountUuid: ByteArray, + proposal: ProposalUnsafe + ): ByteArray { + error("Intentionally not implemented yet.") + } + + override suspend fun addProofsToPczt(pczt: ByteArray): ByteArray { + error("Intentionally not implemented yet.") + } + + override suspend fun extractAndStoreTxFromPczt( + pcztWithProofs: ByteArray, + pcztWithSignatures: ByteArray + ): ByteArray { + error("Intentionally not implemented yet.") + } + override suspend fun decryptAndStoreTransaction( tx: ByteArray, minedHeight: Long? @@ -135,11 +153,30 @@ internal class FakeRustBackend( error("Intentionally not implemented yet.") } + override suspend fun getAccountForUfvk(ufvk: String): JniAccount? { + error("Intentionally not implemented yet.") + } + 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 +196,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 +208,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/assets/co.electriccoin.zcash/checkpoint/mainnet/2720000.json b/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/2720000.json new file mode 100644 index 000000000..7aae30add --- /dev/null +++ b/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/2720000.json @@ -0,0 +1,8 @@ +{ + "network": "main", + "height": "2720000", + "hash": "0000000000f274cf31b4f0532e79cf4847549f03a5644e646f10a7b030cf1f8d", + "time": 1731878266, + "saplingTree": "01fe58bbcba97e82b9d9540ce0729fa48c6fe6437a826071d43bb709b644b3f308010c41eb43ed01a041ba715f05b3410ff7b1bf852954b795119dc74d22378dec591f00000000000001beb74efc89da4b77f414a71ebbbe34312b92f98e713bb7ff1750f2476e59ca130000000001b6023b64f67b64ec121595aeae56b73556009e18e5505117f419bda47483b344000159ce19fb2d0c0b844f38a48379313529a5a08c150fc62e174d17b00c99d54d0f018de4b3e39611e40a92dc0e1214110e088110ac37dc21f3f1d9f7a449447f6a700000017d1ce2f0839bdbf1bad7ae37f845e7fe2116e0c1197536bfbad549f3876c3c590000013e2598f743726006b8de42476ed56a55a75629a7b82e430c4e7c101a69e9b02a011619f99023a69bb647eab2d2aa1a73c3673c74bb033c3c4930eacda19e6fd93b0000000160272b134ca494b602137d89e528c751c06d3ef4a87a45f33af343c15060cc1e0000000000", + "orchardTree": "0136c4aa97e22084588c59a5cf9ca2d4a3434ecb4fcc5af11f040bb666d56e973501540d700d302f3500b8572ca7e68a104675dd30cb3433aa1fdc0e6c6e9e0ba50d1f01431e492872a9869c393b7d6b828d385a9987e0964241bfb7020ccb5b7b060122000001a10f764e55a3f90bc5c8bebf76c2c5b35f93fa6db05f76de291d8c84df911c2f00016621697617587b1ff96bbe27f3971e80b010aabd930729128d33497c2032711f00010d301409081a3beb7515fba43d9e732c9afb53a263b8f2f3c9ed4ebc68ffe22f017bca8efbe69a93f2eb5281eda469de4a91c5494874356c44a88107c6125f343200010e825a535e23ae4e0e6dc98636933c34d9f2785bb21a8c6b474105a5ba65701a0001f4aa2ea0a6df01eb16a86cb7567ddf642ab210590414313df64fe3916ff83b1e017597692c9b1a0b7d4bc8e93db1c90c8331e9709ac9973ca3fbf38b2aa955382b010c104faa2b1148b1b627090aa939e321b82edcb0ce70aa4eee298997556b4f35000112278dfeae9949f887b70ae81e084f8897a5054627acef3efd01c8b29793d522000160040850b766b126a2b4843fcdfdffa5d5cab3f53bc860a3bef68958b5f066170001cc2dcaa338b312112db04b435a706d63244dd435238f0aa1e9e1598d35470810012dcc4273c8a0ed2337ecf7879380a07e7d427c7f9d82e538002bd1442978402c01daf63debf5b40df902dae98dadc029f281474d190cddecef1b10653248a234150001e2bca6a8d987d668defba89dc082196a922634ed88e065c669e526bb8815ee1b000000000000" +} diff --git a/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/2722500.json b/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/2722500.json new file mode 100644 index 000000000..a5088a410 --- /dev/null +++ b/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/2722500.json @@ -0,0 +1,8 @@ +{ + "network": "main", + "height": "2722500", + "hash": "000000000190ac03145797cdf7e8b41d5c410393c2dc1a0017084861e3e64009", + "time": 1732066993, + "saplingTree": "012bef5680ac45e64f1010e7f8e410311e7e6e8c0ce1534119684f7e4007abbe66001f000001ee973512dea527eb72e3c0ab7dc83b95d0e507ba8fa17cfb320bcba15616f613000160fcb82f16877623429b654cd9333f65e753d8378f70909afb328afdb0b7f003000001bad0b286f95a3243c40875c017ee723c4a8f99473d0f6b2144a8e49e0199ec510181b31b0ee97643e904e4792b88dc86034e6803c2e5c149f21f5c8f6bbaa8c002000001b6023b64f67b64ec121595aeae56b73556009e18e5505117f419bda47483b344000159ce19fb2d0c0b844f38a48379313529a5a08c150fc62e174d17b00c99d54d0f018de4b3e39611e40a92dc0e1214110e088110ac37dc21f3f1d9f7a449447f6a700000017d1ce2f0839bdbf1bad7ae37f845e7fe2116e0c1197536bfbad549f3876c3c590000013e2598f743726006b8de42476ed56a55a75629a7b82e430c4e7c101a69e9b02a011619f99023a69bb647eab2d2aa1a73c3673c74bb033c3c4930eacda19e6fd93b0000000160272b134ca494b602137d89e528c751c06d3ef4a87a45f33af343c15060cc1e0000000000", + "orchardTree": "01b658ec5557a386b96b1d34dd2bc1df828f20bbfa0e8d9231b2ef389784b5e10d010583d6d0b63f7d9968f83f70ae70f0bb3995903be3841c7657e9e9e4c8d7cc251f0001e09e8549ec380f356200c788d006f357ba54fc97d9cc28a0faf6f3239f5f801801acb9f28fdfb518cda501d27d28e4888be143774d7663499f3a555786165d311e01c62ac5e7bebfaac875958a15ce3387ae703bedd2ab40c3635befc6c85272831601b881392aa787ec5b91315fd1e0de614b6db57e96506e78bf4ac6d2854f9ddb1601dd6285806c70d8360e721952dffad6714ceeb598d394cecf04d569205cee1c280118bd07720f349cf2389f317458f51f9a27a58db6389c914c773d0ac0698222160000000001a7fc1bb949ed8f09941185ed0da3e9dde33a8b1791f40e59c9ca3955f34dbb0501f4aa2ea0a6df01eb16a86cb7567ddf642ab210590414313df64fe3916ff83b1e017597692c9b1a0b7d4bc8e93db1c90c8331e9709ac9973ca3fbf38b2aa955382b010c104faa2b1148b1b627090aa939e321b82edcb0ce70aa4eee298997556b4f35000112278dfeae9949f887b70ae81e084f8897a5054627acef3efd01c8b29793d522000160040850b766b126a2b4843fcdfdffa5d5cab3f53bc860a3bef68958b5f066170001cc2dcaa338b312112db04b435a706d63244dd435238f0aa1e9e1598d35470810012dcc4273c8a0ed2337ecf7879380a07e7d427c7f9d82e538002bd1442978402c01daf63debf5b40df902dae98dadc029f281474d190cddecef1b10653248a234150001e2bca6a8d987d668defba89dc082196a922634ed88e065c669e526bb8815ee1b000000000000" +} diff --git a/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/2725000.json b/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/2725000.json new file mode 100644 index 000000000..d9d37aeb7 --- /dev/null +++ b/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/2725000.json @@ -0,0 +1,8 @@ +{ + "network": "main", + "height": "2725000", + "hash": "00000000005f57490f2dcd0db94a93f02444a13108dcc3a5a695b7ee108d7aa1", + "time": 1732255050, + "saplingTree": "01ee68b0c4a4cf669b3f878be0c9815d8e91b07d90190b0f21a35c6cf4ca85b450001f01a5fe3f598d1d2bc8a489d44110e63110c9e03cc09477ed31f4e7064045bbb44f0113d15cf1773f323239f85de502a0f681dee1f8a642f9670f681bdbd8d368df5001ad68fa8a79275f89ccfe32217c4a219044c75303dbc9b9aebf6150ab610be9580101108d56650b3fa45e216c06ceb0addbfaa6fdd5668c8adc1cca07785e507b060129ba129b78669e3b12d420f0daccb7ab5a3fee1ef7074505b81230d3c600913e01d9ce3862876082848b53d638115cb5b83e83deeab9d8e9400f7042a36d7e847201bb3421dcd825621ff59b847cd682ab78a24ce1550ea81c2696d2f51033bf2f6f019007ce03dd0accfa6293ad3b68961b6c7003cfa53fe75287abd6fd5f3e407a450001bef95370a93e9f571102b7909420292c5102f4fe78d57823ab220185c8cf74440001b6023b64f67b64ec121595aeae56b73556009e18e5505117f419bda47483b344000159ce19fb2d0c0b844f38a48379313529a5a08c150fc62e174d17b00c99d54d0f018de4b3e39611e40a92dc0e1214110e088110ac37dc21f3f1d9f7a449447f6a700000017d1ce2f0839bdbf1bad7ae37f845e7fe2116e0c1197536bfbad549f3876c3c590000013e2598f743726006b8de42476ed56a55a75629a7b82e430c4e7c101a69e9b02a011619f99023a69bb647eab2d2aa1a73c3673c74bb033c3c4930eacda19e6fd93b0000000160272b134ca494b602137d89e528c751c06d3ef4a87a45f33af343c15060cc1e0000000000", + "orchardTree": "01fee4f9e9b69210894f0f94a19ce01f137dda0a7d8727bc91b5bf39ba614cc71e001f012d471b33e63c758585b73516c63083f48426612f22087a58981d880dbcd8370f0001d972911aa565082aab9e1993c15c1aac5460942675e4bab54c7b09b46ad55a14010b63ef4ab6258d6447f3f9712c4883ef2918031e7d0f6e9dfe189dbcfd1e0e0b0000000001a4b83b227de1f90c192b5441089c92de739f7b9740500d0060d8a71514fc77350185172b5ff7eb02bfd6cf624b02b3b7c0667be792efbb7e23dd3528654e0a8c070001a7fc1bb949ed8f09941185ed0da3e9dde33a8b1791f40e59c9ca3955f34dbb0501f4aa2ea0a6df01eb16a86cb7567ddf642ab210590414313df64fe3916ff83b1e017597692c9b1a0b7d4bc8e93db1c90c8331e9709ac9973ca3fbf38b2aa955382b010c104faa2b1148b1b627090aa939e321b82edcb0ce70aa4eee298997556b4f35000112278dfeae9949f887b70ae81e084f8897a5054627acef3efd01c8b29793d522000160040850b766b126a2b4843fcdfdffa5d5cab3f53bc860a3bef68958b5f066170001cc2dcaa338b312112db04b435a706d63244dd435238f0aa1e9e1598d35470810012dcc4273c8a0ed2337ecf7879380a07e7d427c7f9d82e538002bd1442978402c01daf63debf5b40df902dae98dadc029f281474d190cddecef1b10653248a234150001e2bca6a8d987d668defba89dc082196a922634ed88e065c669e526bb8815ee1b000000000000" +} diff --git a/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/2727500.json b/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/2727500.json new file mode 100644 index 000000000..239bcec2a --- /dev/null +++ b/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/2727500.json @@ -0,0 +1,8 @@ +{ + "network": "main", + "height": "2727500", + "hash": "00000000014fac0982c2d3e7efeae60638e8e15ce0551a94f646b5a5b173226d", + "time": 1732443982, + "saplingTree": "01251467d580b6f54ccb5c3f182740c1462a36fdd2bd3131cb7853b5cc277bde20001f0001d5cbd798b624e903bc8c07b34fa41acd54a013627ab0de366753eb8ea7fb08640196366685f68e1120470d68798e401d4571244fb295c268b81d1f8efed9049456000001f2335a1b047d955d2fd81147002b7f2461628e8087d2424a30fea8de66f680310000000001fc9cf2b5c8d94a51072bc892a55d95526c7ee484b37e4d3a176b00404e419d0701b6023b64f67b64ec121595aeae56b73556009e18e5505117f419bda47483b344000159ce19fb2d0c0b844f38a48379313529a5a08c150fc62e174d17b00c99d54d0f018de4b3e39611e40a92dc0e1214110e088110ac37dc21f3f1d9f7a449447f6a700000017d1ce2f0839bdbf1bad7ae37f845e7fe2116e0c1197536bfbad549f3876c3c590000013e2598f743726006b8de42476ed56a55a75629a7b82e430c4e7c101a69e9b02a011619f99023a69bb647eab2d2aa1a73c3673c74bb033c3c4930eacda19e6fd93b0000000160272b134ca494b602137d89e528c751c06d3ef4a87a45f33af343c15060cc1e0000000000", + "orchardTree": "0146da5599c50c6f4d3de7cbdb7d47cec6db7b366973730a431d47c1df2275182b01cfa73d6c96370417e1db226f3ba4e92e31ce83bd3a412f7194237f7e4210c1041f01b1f84a65688f6114ca1a2c0bbbcf673fae82494e9fbee559eb5d4a51ac1bd2220112b8b762589bdea8cd3dabc08e03dcf10bf84841b5bd55dd5a6dc316cc341f0501775028c5f1964c7c4a41e219429cf19bbdaff9c10cf51e192a5763ea4753382901d2f33c48c16e22d5f6fa97cff889d1a283eb99bfbe171cd83e8b690f60c8eb0e000169840489eda0d28a5ccb2049da76a12f8f1844f7d3f63d0a661adff9d8cf662600016796a5865b332e91b8b204bcbc93ae926f35609721782ac5550e3ba98f43421b00000127c0861baaf628b853f5fcd3670fbc9bef9d567365139251d377e6b78e5eb80601a7fc1bb949ed8f09941185ed0da3e9dde33a8b1791f40e59c9ca3955f34dbb0501f4aa2ea0a6df01eb16a86cb7567ddf642ab210590414313df64fe3916ff83b1e017597692c9b1a0b7d4bc8e93db1c90c8331e9709ac9973ca3fbf38b2aa955382b010c104faa2b1148b1b627090aa939e321b82edcb0ce70aa4eee298997556b4f35000112278dfeae9949f887b70ae81e084f8897a5054627acef3efd01c8b29793d522000160040850b766b126a2b4843fcdfdffa5d5cab3f53bc860a3bef68958b5f066170001cc2dcaa338b312112db04b435a706d63244dd435238f0aa1e9e1598d35470810012dcc4273c8a0ed2337ecf7879380a07e7d427c7f9d82e538002bd1442978402c01daf63debf5b40df902dae98dadc029f281474d190cddecef1b10653248a234150001e2bca6a8d987d668defba89dc082196a922634ed88e065c669e526bb8815ee1b000000000000" +} diff --git a/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/2730000.json b/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/2730000.json new file mode 100644 index 000000000..ce2815cfa --- /dev/null +++ b/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/2730000.json @@ -0,0 +1,8 @@ +{ + "network": "main", + "height": "2730000", + "hash": "00000000007e262001ddd4d6afbd3387c52811533648bace9dd1e1ba43f027bb", + "time": 1732632193, + "saplingTree": "01bd13f8edeef7198d8796bc9609c42bb4138badf8eb280a67a39290c4d65a2208018849804f518385a34c6790f8b12303d43985ec68617fb4929824cd8ba3b9f0451f01d0da0b02a0e5cbc989e0126408fc469f89c19c59f150e5e61673ecca5107586e01c2c2aa915799afb2eb9a64bf227163b6db27dce97436af0cdef94c6beb1fc6680000000001cafb0a89aab0afedc3428015e7baa0521d01ec834598686343136fae63a5663e000192c5a2e8343ddd61ad52c3e39d68b426d230ddf1c4badac6c071fc279d44152c0001fc9cf2b5c8d94a51072bc892a55d95526c7ee484b37e4d3a176b00404e419d0701b6023b64f67b64ec121595aeae56b73556009e18e5505117f419bda47483b344000159ce19fb2d0c0b844f38a48379313529a5a08c150fc62e174d17b00c99d54d0f018de4b3e39611e40a92dc0e1214110e088110ac37dc21f3f1d9f7a449447f6a700000017d1ce2f0839bdbf1bad7ae37f845e7fe2116e0c1197536bfbad549f3876c3c590000013e2598f743726006b8de42476ed56a55a75629a7b82e430c4e7c101a69e9b02a011619f99023a69bb647eab2d2aa1a73c3673c74bb033c3c4930eacda19e6fd93b0000000160272b134ca494b602137d89e528c751c06d3ef4a87a45f33af343c15060cc1e0000000000", + "orchardTree": "0186258c0a6122f090f1636995c41b24b9f5f0e85a82b9a21f802f040f38ed9719001f01e5951f6ec4ce8476ab1ba5464d9a7985bb1842a1f5b7297322e4837eb5d99d210001c8a7ba2c275c354ca965a45a71ca9fa93a12e198ce3c4871d862b733f4534c2c01ca910b331e947a04386c0e74050d6a894d371a4fda033b1bacd4450a07870f0a01e3c1f21074956d9aea373010d266763ff94819b662fbfd78d6f0a7804feed61600000001ea03d68e80159045e544a3609445bbd6e89d06dad003c13ebc63023fc1f1631b018614c3d75e1334fcecf0875ad1f43225455961b5e8e775f69cad697778e16e390127c0861baaf628b853f5fcd3670fbc9bef9d567365139251d377e6b78e5eb80601a7fc1bb949ed8f09941185ed0da3e9dde33a8b1791f40e59c9ca3955f34dbb0501f4aa2ea0a6df01eb16a86cb7567ddf642ab210590414313df64fe3916ff83b1e017597692c9b1a0b7d4bc8e93db1c90c8331e9709ac9973ca3fbf38b2aa955382b010c104faa2b1148b1b627090aa939e321b82edcb0ce70aa4eee298997556b4f35000112278dfeae9949f887b70ae81e084f8897a5054627acef3efd01c8b29793d522000160040850b766b126a2b4843fcdfdffa5d5cab3f53bc860a3bef68958b5f066170001cc2dcaa338b312112db04b435a706d63244dd435238f0aa1e9e1598d35470810012dcc4273c8a0ed2337ecf7879380a07e7d427c7f9d82e538002bd1442978402c01daf63debf5b40df902dae98dadc029f281474d190cddecef1b10653248a234150001e2bca6a8d987d668defba89dc082196a922634ed88e065c669e526bb8815ee1b000000000000" +} diff --git a/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/2732500.json b/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/2732500.json new file mode 100644 index 000000000..4b0bb44d2 --- /dev/null +++ b/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/2732500.json @@ -0,0 +1,8 @@ +{ + "network": "main", + "height": "2732500", + "hash": "0000000000455438aa5afcea2bf93284cb19248df06ee47ad41a0659b1438cb8", + "time": 1732820090, + "saplingTree": "0157fc2a442e4a35c6badcdac3712f146d3e92ea9f9ccb30f70c3b189890724b0c001f000000016116221824b2da24a6e6a6a0a2dc7cc63f542d80ba411981bc759354aad7875b01ced8f304b61aeb9070899570945cbab41a561b6a18d629e2f9f40cab08d2090700000126d9be67931cc0ba563b9505733a022e118b9e13fc7ce23e33cca56a590df3060001964cd80701d3d194dff4d73a2cd07bf3ed09652ef8027055c2ac136a6a2dd31f01fc9cf2b5c8d94a51072bc892a55d95526c7ee484b37e4d3a176b00404e419d0701b6023b64f67b64ec121595aeae56b73556009e18e5505117f419bda47483b344000159ce19fb2d0c0b844f38a48379313529a5a08c150fc62e174d17b00c99d54d0f018de4b3e39611e40a92dc0e1214110e088110ac37dc21f3f1d9f7a449447f6a700000017d1ce2f0839bdbf1bad7ae37f845e7fe2116e0c1197536bfbad549f3876c3c590000013e2598f743726006b8de42476ed56a55a75629a7b82e430c4e7c101a69e9b02a011619f99023a69bb647eab2d2aa1a73c3673c74bb033c3c4930eacda19e6fd93b0000000160272b134ca494b602137d89e528c751c06d3ef4a87a45f33af343c15060cc1e0000000000", + "orchardTree": "0176b3c56da71a76d99200644060194bf3de71953b6b4ea3cbc232faeda7c48e1e001f00012173ea654dd8186b7ff93cfa55c8ba059e6c18c5a6df2ed781d27760b2245c0f019e87ccbc26f5d7d0a382756fa4cc1b3a20bc2a54b7f863fda09405e801563a1301d438b87d3a7ca8ec993f8bac1205db296d90ec8783acbbd0b735f476c21dcd2501e1209a915757b65a0e3694d4ffd55f06fde20acb8af739202757258956ff0527015fc5c639d0997ed9899f4fd591a655b89d7fb01519e4f35b84d80aa3fc999938015b6f20ddbe039ee9143216f1680b9e34c7ba27fc8c96561bc4126b4fc269ce21000001bad3288ba85dc7871750a11a1460c73db4737337557af8f3c4a43d3b32035f1e0000000000016f3f63aab58e63b6449583df5658a91972a20291c6311b5b3e5240aff8d7d0020112278dfeae9949f887b70ae81e084f8897a5054627acef3efd01c8b29793d522000160040850b766b126a2b4843fcdfdffa5d5cab3f53bc860a3bef68958b5f066170001cc2dcaa338b312112db04b435a706d63244dd435238f0aa1e9e1598d35470810012dcc4273c8a0ed2337ecf7879380a07e7d427c7f9d82e538002bd1442978402c01daf63debf5b40df902dae98dadc029f281474d190cddecef1b10653248a234150001e2bca6a8d987d668defba89dc082196a922634ed88e065c669e526bb8815ee1b000000000000" +} diff --git a/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/2735000.json b/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/2735000.json new file mode 100644 index 000000000..2d6cc9b6b --- /dev/null +++ b/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/2735000.json @@ -0,0 +1,8 @@ +{ + "network": "main", + "height": "2735000", + "hash": "000000000022268ac23f7eeab64d3c4965605c9414ed61cfc376be6a30382793", + "time": 1733009252, + "saplingTree": "015b2743fbbbd97445fdbe5c3a5ea16392f41e36ba8a0782b5d92afd5baf7ec43201715f9c3c220735ff071e9b134e18a16365524074a2ea70768469a46088847e381f00015a8cf66c51df0c4cd895c0d8d153f0467864dac2e052dbc7a55a18edb7539e5901d2df99d9ab060a07c0cb7bc7c826b6de99cc9f66a5d121e483899d8cc932b65b00019ffa660dee466a4cf469c65181e47b417d4d32a0a46375720ff035706fc037020000013a47c13779bfcfe40d5871d3d44ef4236489258aa501afacd29bf186e19bd91b010fcd83cf5e59a3ad36c20cb4fce2e264ce0d763f9c52a24e1473b3740a6a783701964cd80701d3d194dff4d73a2cd07bf3ed09652ef8027055c2ac136a6a2dd31f01fc9cf2b5c8d94a51072bc892a55d95526c7ee484b37e4d3a176b00404e419d0701b6023b64f67b64ec121595aeae56b73556009e18e5505117f419bda47483b344000159ce19fb2d0c0b844f38a48379313529a5a08c150fc62e174d17b00c99d54d0f018de4b3e39611e40a92dc0e1214110e088110ac37dc21f3f1d9f7a449447f6a700000017d1ce2f0839bdbf1bad7ae37f845e7fe2116e0c1197536bfbad549f3876c3c590000013e2598f743726006b8de42476ed56a55a75629a7b82e430c4e7c101a69e9b02a011619f99023a69bb647eab2d2aa1a73c3673c74bb033c3c4930eacda19e6fd93b0000000160272b134ca494b602137d89e528c751c06d3ef4a87a45f33af343c15060cc1e0000000000", + "orchardTree": "01021999c78b8951f176e1f25ba170be71807e1bcab0a415fc9fa6c974d233d020001f0109a0f01384ac72ef3f392774ea8a54ce25994f87085ff6ebeff9b2d6a2d0e51200000139f49c9981b8e7e4f40acf05ed9a5fc745d319673e930bd92a6aba263766923e01645a1736187a1f286e43b20b37a161740cbcdf51a8eba2a352681f0b309baa1a012fb18c9dc61e5ba1acfec06ef591c123c54393701b63e436ef78c6453fc52f020001ec77bfe1d7ce64aa7ebc02a6c98b9cf124fd9934be724aed4852f463776ed91001db45c3052961c18a56f4e783e971b1146e76005d728312ab36397914fe79352700018241ee0f426194c769f5228cdab934b6af398cd4b25cba70f815a00e6b0ef92400000000016f3f63aab58e63b6449583df5658a91972a20291c6311b5b3e5240aff8d7d0020112278dfeae9949f887b70ae81e084f8897a5054627acef3efd01c8b29793d522000160040850b766b126a2b4843fcdfdffa5d5cab3f53bc860a3bef68958b5f066170001cc2dcaa338b312112db04b435a706d63244dd435238f0aa1e9e1598d35470810012dcc4273c8a0ed2337ecf7879380a07e7d427c7f9d82e538002bd1442978402c01daf63debf5b40df902dae98dadc029f281474d190cddecef1b10653248a234150001e2bca6a8d987d668defba89dc082196a922634ed88e065c669e526bb8815ee1b000000000000" +} diff --git a/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/2737500.json b/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/2737500.json new file mode 100644 index 000000000..238aad266 --- /dev/null +++ b/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/2737500.json @@ -0,0 +1,8 @@ +{ + "network": "main", + "height": "2737500", + "hash": "000000000160ecb09f8a69045fb0d5f567f4ecb4b7348b6f45bb19d69ad501bf", + "time": 1733197424, + "saplingTree": "01d3e2b4414eb7dc291c6f678a79a3da36490b71cf9e69df05953a795b44e90e090122466472f452b1ccf19d56fcf9abb82b9e78c41f3b94e86aaca9512d2905b13c1f0001191c62c4ae43696f697e5630334c5ad706a7eb0a63af333e56d71a2a86264464019c0310b14a1fc46e8b6f85e5492224b784a8650a653fb375eb84fb346139490100018615876089759f5162902b1dde4e6c3ddf7f5f74a55892a98a753f308cf18b4c0000000124683ca9db9a884e5a59646ee6c1293a54f5b2e3b4b569c579bb90743f8ce263000000017b84767ca227e22f8192b542c0688d413316e34c2059335311b1188a9c0e9c640159ce19fb2d0c0b844f38a48379313529a5a08c150fc62e174d17b00c99d54d0f018de4b3e39611e40a92dc0e1214110e088110ac37dc21f3f1d9f7a449447f6a700000017d1ce2f0839bdbf1bad7ae37f845e7fe2116e0c1197536bfbad549f3876c3c590000013e2598f743726006b8de42476ed56a55a75629a7b82e430c4e7c101a69e9b02a011619f99023a69bb647eab2d2aa1a73c3673c74bb033c3c4930eacda19e6fd93b0000000160272b134ca494b602137d89e528c751c06d3ef4a87a45f33af343c15060cc1e0000000000", + "orchardTree": "01a5e30fa3a0b238b83d373b62d6c929d00067d45a037f014540a67ef1840c9b0e01c2745cd21db5b5a1f8503d4da779632408295612dbf81c54f7e64dbd4107c43a1f012a03b15b575d73057457fafbc3768c0834349f0e95b05ce917e40055af635f2c017bf83e33c1e7a701bf392eecb07ea18a11c1a73856b1f4ede217fd153d77851b00013112d043a707710a032031c7da9b384fafc1a9bd5a96542bbdb4e283172d6d140123b2f52355b62f01fef354f9f5999670f5965b503d19465624c3bbb5067efd350001ab45698abeb7ceaa6c7e28e6fee7d0630b57eb4491509fce64ac2781fe164f0800000000013ccabb04ad965629c150f9a0bdf552f7d41a93941aa551372619b491d151572e000000016f3f63aab58e63b6449583df5658a91972a20291c6311b5b3e5240aff8d7d0020112278dfeae9949f887b70ae81e084f8897a5054627acef3efd01c8b29793d522000160040850b766b126a2b4843fcdfdffa5d5cab3f53bc860a3bef68958b5f066170001cc2dcaa338b312112db04b435a706d63244dd435238f0aa1e9e1598d35470810012dcc4273c8a0ed2337ecf7879380a07e7d427c7f9d82e538002bd1442978402c01daf63debf5b40df902dae98dadc029f281474d190cddecef1b10653248a234150001e2bca6a8d987d668defba89dc082196a922634ed88e065c669e526bb8815ee1b000000000000" +} diff --git a/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/2740000.json b/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/2740000.json new file mode 100644 index 000000000..0c640a3bf --- /dev/null +++ b/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/2740000.json @@ -0,0 +1,8 @@ +{ + "network": "main", + "height": "2740000", + "hash": "0000000001744813c0d42e1fde0cdd931ebb95b9fb0e41cc76c5fd28a8ccc9c8", + "time": 1733385626, + "saplingTree": "019594170657b3516fb6c9152313238804a70392241d66255f9249a351fed2ae62001f012d5d906a475d110d8dfe26cf27ab513641579ea0ba89ac73d3e65a8f6ca37347000162b3605a5be8a82a4c8358d685d7e298b7698fb4a5b2192791f68f80dfaf0817000001ed553bfa1242eff10a206a2aada9c3c29d7ec7a0c753593439d18fca2c3eb40f014eb21ffc6a28d679fb40d6c449b7699a4ac42fa1ea25fd2c539400fc933834140000017bbb7bcd7d81a253dee8286ba3aa42e00132e3f0ebda7612626e9dff9f44e1130000017b84767ca227e22f8192b542c0688d413316e34c2059335311b1188a9c0e9c640159ce19fb2d0c0b844f38a48379313529a5a08c150fc62e174d17b00c99d54d0f018de4b3e39611e40a92dc0e1214110e088110ac37dc21f3f1d9f7a449447f6a700000017d1ce2f0839bdbf1bad7ae37f845e7fe2116e0c1197536bfbad549f3876c3c590000013e2598f743726006b8de42476ed56a55a75629a7b82e430c4e7c101a69e9b02a011619f99023a69bb647eab2d2aa1a73c3673c74bb033c3c4930eacda19e6fd93b0000000160272b134ca494b602137d89e528c751c06d3ef4a87a45f33af343c15060cc1e0000000000", + "orchardTree": "01506294ae33a2957a998247f9d0512e40c1ba9354c95f5d81f659add74d461126011f72ddbe13a449abdb146c45aa531779168ec10ebf76517697fe1d7e3ff5ae3e1f00014f339488b2479fbc126d88ba7873045c4fc3063890b42222d14352d6c6d6f91f0001abab0edad2f2822962dc22726106412a8ad0af31a37961c297e1d085c3144b2201eebc46b0edddabb8f3f7c85615e8f0206072586e1bec39145a842d18153a3b35018f42bf28aad4ed5acec5269effd5d74983c1d2621f0a66da6b1c6464ba427d2700000119b15e2258f7e8855ec3525b81db9e75b5522887e33fa130add19b9b246c0d07011254013d089c6ae5daa047754bdb53a835e36b6e1c32dd833f974b397f49f62600013ccabb04ad965629c150f9a0bdf552f7d41a93941aa551372619b491d151572e000000016f3f63aab58e63b6449583df5658a91972a20291c6311b5b3e5240aff8d7d0020112278dfeae9949f887b70ae81e084f8897a5054627acef3efd01c8b29793d522000160040850b766b126a2b4843fcdfdffa5d5cab3f53bc860a3bef68958b5f066170001cc2dcaa338b312112db04b435a706d63244dd435238f0aa1e9e1598d35470810012dcc4273c8a0ed2337ecf7879380a07e7d427c7f9d82e538002bd1442978402c01daf63debf5b40df902dae98dadc029f281474d190cddecef1b10653248a234150001e2bca6a8d987d668defba89dc082196a922634ed88e065c669e526bb8815ee1b000000000000" +} diff --git a/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/2742500.json b/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/2742500.json new file mode 100644 index 000000000..560ee9e7a --- /dev/null +++ b/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/2742500.json @@ -0,0 +1,8 @@ +{ + "network": "main", + "height": "2742500", + "hash": "00000000011fd943a718007467e44fe531b029ffff3fc08c0b5ef8a132807124", + "time": 1733573980, + "saplingTree": "01758865404ccfe374fd3ec892df313cb91c7c679917c86cc137bd9f5d720ac36a001f0125386d7cae06a146aef38c52f521c0e2d01dd7824d116fb9aab554471764070a01ae727fd9e41add0158936da02d5073c0869c7aa9c84c760f95e7de4f3d3c2c7300000001bafdf7da6ec266d3d7c6ebfc33a47b95b03d1b8a292107878eff9f7e3d39b0040169dd6b603fcfff50898bd9ead0a397aaf4047584f1fc8c22d2cd5318e96a6343000197219288c47583b06ad70324eaf06a337bc562169c741dbb7d899be6be468c04017bbb7bcd7d81a253dee8286ba3aa42e00132e3f0ebda7612626e9dff9f44e1130000017b84767ca227e22f8192b542c0688d413316e34c2059335311b1188a9c0e9c640159ce19fb2d0c0b844f38a48379313529a5a08c150fc62e174d17b00c99d54d0f018de4b3e39611e40a92dc0e1214110e088110ac37dc21f3f1d9f7a449447f6a700000017d1ce2f0839bdbf1bad7ae37f845e7fe2116e0c1197536bfbad549f3876c3c590000013e2598f743726006b8de42476ed56a55a75629a7b82e430c4e7c101a69e9b02a011619f99023a69bb647eab2d2aa1a73c3673c74bb033c3c4930eacda19e6fd93b0000000160272b134ca494b602137d89e528c751c06d3ef4a87a45f33af343c15060cc1e0000000000", + "orchardTree": "0132118230b634f75bf7e81db74cb371d1664ad166020e76d851d8ded3cd6f182a013c5aefaa6ced40d83ff02c36ed0071e29e9331f464f34491787d2ac932658b161f00000157ba35cb488e595149e2e934d0b3f01053eccfbdb93847c7b461a0ac300ec83d00000000014769f044c37f5f3060f5a555468efc8c52d2c2c24d4a3f3824cefcf1234e5709012aad2c7436456d23f15c5c1275dda685d1108376ad8307f57f3fc4952afbc6190001c3bcc872f6613e725d994ff3b50a3e4915a3bbd35ba68f5d56fe01c9a432c220013ccabb04ad965629c150f9a0bdf552f7d41a93941aa551372619b491d151572e000000016f3f63aab58e63b6449583df5658a91972a20291c6311b5b3e5240aff8d7d0020112278dfeae9949f887b70ae81e084f8897a5054627acef3efd01c8b29793d522000160040850b766b126a2b4843fcdfdffa5d5cab3f53bc860a3bef68958b5f066170001cc2dcaa338b312112db04b435a706d63244dd435238f0aa1e9e1598d35470810012dcc4273c8a0ed2337ecf7879380a07e7d427c7f9d82e538002bd1442978402c01daf63debf5b40df902dae98dadc029f281474d190cddecef1b10653248a234150001e2bca6a8d987d668defba89dc082196a922634ed88e065c669e526bb8815ee1b000000000000" +} diff --git a/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/2745000.json b/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/2745000.json new file mode 100644 index 000000000..15b7b5407 --- /dev/null +++ b/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/2745000.json @@ -0,0 +1,8 @@ +{ + "network": "main", + "height": "2745000", + "hash": "0000000000dfb5ed2417f4db712e2c0eb11613664b0c9e1e29cb8a06db6c35fc", + "time": 1733763099, + "saplingTree": "01aaa117c81fc3bb6c1621bbd1ad1fe4e44ec169ed658b1c996a429f47c0abdb27001f00018978baf286d37936196eeec9b0c9ca0a3df96a484c04899de9be7fbbffe39b0d0001982132183464f82b1726fd064b1a4138db64f0a899ae5d195bb1afdd371fea3000014e8a0f88c127267e7aa007a218ae662918305aaf9a2b726096d2201ff7a5732d000149c28b50e9fe41f3cc931a89a2306cf5f985978bed023a6262e9539e44158a6c0000019dd8c1939a8c069a447e4383e4aa1d6636e824632b56fdc3dc7447ba30f6a83b00017b84767ca227e22f8192b542c0688d413316e34c2059335311b1188a9c0e9c640159ce19fb2d0c0b844f38a48379313529a5a08c150fc62e174d17b00c99d54d0f018de4b3e39611e40a92dc0e1214110e088110ac37dc21f3f1d9f7a449447f6a700000017d1ce2f0839bdbf1bad7ae37f845e7fe2116e0c1197536bfbad549f3876c3c590000013e2598f743726006b8de42476ed56a55a75629a7b82e430c4e7c101a69e9b02a011619f99023a69bb647eab2d2aa1a73c3673c74bb033c3c4930eacda19e6fd93b0000000160272b134ca494b602137d89e528c751c06d3ef4a87a45f33af343c15060cc1e0000000000", + "orchardTree": "01f0ee4ae0dbd4ce2e404151dec31c15e62c2e7dbde68199581bcfda6920aac6240142751867c579596c3f701ab4b76c278ff5e7cd35d2a10a01d2038d6af47be93e1f00000001d4e66784751b3c7e65c1cdc254d8fa520d6652411a5dc397a3fe268075c3852e0000000000000000019d2e26bdef115ced7bd36305a54386996133c4e65759f3731637a40eba67da100000016f3f63aab58e63b6449583df5658a91972a20291c6311b5b3e5240aff8d7d0020112278dfeae9949f887b70ae81e084f8897a5054627acef3efd01c8b29793d522000160040850b766b126a2b4843fcdfdffa5d5cab3f53bc860a3bef68958b5f066170001cc2dcaa338b312112db04b435a706d63244dd435238f0aa1e9e1598d35470810012dcc4273c8a0ed2337ecf7879380a07e7d427c7f9d82e538002bd1442978402c01daf63debf5b40df902dae98dadc029f281474d190cddecef1b10653248a234150001e2bca6a8d987d668defba89dc082196a922634ed88e065c669e526bb8815ee1b000000000000" +} diff --git a/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/2747500.json b/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/2747500.json new file mode 100644 index 000000000..5c1b6dedb --- /dev/null +++ b/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/2747500.json @@ -0,0 +1,8 @@ +{ + "network": "main", + "height": "2747500", + "hash": "0000000000b0b98fc0deae5afab2639873f6e0ca10578ef3ee3859eb4bfc7234", + "time": 1733950969, + "saplingTree": "018072cc2372f65c6e3b4a8c8c959dd409e3311fc24117c5f88a5b15e48f8f934b001f01732c62e4d3cf56c9fba03b087b611df05a09dce17856b6ef081545969cd2af3a00000001879fcf86791a7b3737db13a837eb0ef9af340b463c9713447566f91aa4b0763501cc7b33e8eb3be22d491de116a4356c45efce87f336a7a0c483834d0ca42a224d01a4870184417b43e1763b2fe6f8d9c1c086ff7d27e3f546f7a9b3952434fe0756017915355e4f6ea968c1bafd215535160ba62b79ad4ef502d50ef58dab083a0a64013b65c857bcb6d0d00c72eb10a2b85e078f8790cd38d12a8472166ce171846a0e00019dd8c1939a8c069a447e4383e4aa1d6636e824632b56fdc3dc7447ba30f6a83b00017b84767ca227e22f8192b542c0688d413316e34c2059335311b1188a9c0e9c640159ce19fb2d0c0b844f38a48379313529a5a08c150fc62e174d17b00c99d54d0f018de4b3e39611e40a92dc0e1214110e088110ac37dc21f3f1d9f7a449447f6a700000017d1ce2f0839bdbf1bad7ae37f845e7fe2116e0c1197536bfbad549f3876c3c590000013e2598f743726006b8de42476ed56a55a75629a7b82e430c4e7c101a69e9b02a011619f99023a69bb647eab2d2aa1a73c3673c74bb033c3c4930eacda19e6fd93b0000000160272b134ca494b602137d89e528c751c06d3ef4a87a45f33af343c15060cc1e0000000000", + "orchardTree": "01935a532ee16f669700d51a3e707f724175244d50ac278dfa30e7cbd1ae0b9531011c0351005863be3daecc695180e4072908b4d9b5e14fb1d665bd6ab7c199ba021f0001cd0250a0dddbc962674a776772a4dad7055059751ca259b33d3cbe082c487d3e000168ea114ceceba91674c81b5ab404d2550466915620b732147b6e5f090290e0320001995a337993f9c60f7bba1cb689d99d08964ee140f879173ed5f32ebce114f60a0125819d28f26db89f3746eafb36709463ca8a4afba7e6b4a863f8c13f2e5d0f1c000001b51ef2387bb883295be8ceaf72ae149234718de7a11d91b934d93d96340787160000019d2e26bdef115ced7bd36305a54386996133c4e65759f3731637a40eba67da100000016f3f63aab58e63b6449583df5658a91972a20291c6311b5b3e5240aff8d7d0020112278dfeae9949f887b70ae81e084f8897a5054627acef3efd01c8b29793d522000160040850b766b126a2b4843fcdfdffa5d5cab3f53bc860a3bef68958b5f066170001cc2dcaa338b312112db04b435a706d63244dd435238f0aa1e9e1598d35470810012dcc4273c8a0ed2337ecf7879380a07e7d427c7f9d82e538002bd1442978402c01daf63debf5b40df902dae98dadc029f281474d190cddecef1b10653248a234150001e2bca6a8d987d668defba89dc082196a922634ed88e065c669e526bb8815ee1b000000000000" +} diff --git a/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/2750000.json b/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/2750000.json new file mode 100644 index 000000000..bf69abacf --- /dev/null +++ b/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/2750000.json @@ -0,0 +1,8 @@ +{ + "network": "main", + "height": "2750000", + "hash": "00000000014ecf7213e20802e0899d5559109e9a92b48d7279345eb01286aaeb", + "time": 1734139390, + "saplingTree": "01377858035db2c123dc35eba73dc227b2dce33a1fbdbad34063871104aad8875f0106c2b8a59786ef0e70c86611e7f2f91ae654a19338dbf990e79acc3a7487e1381f0001e9e8d954d51078697805b56e198acf66377ec3f320fac8979c245664f55eea600001e1cbc59e5c832b5ad42b8012ef186126d34fbd0cc21e82674ce3161072501b5e01ec71dbca2ee5bb43f3ce4c9da4445cd87f4cb74d7248982eef5ab36f281f4d300001c42e8f0e00b7d4befe6cd34d67a16507fed0191824ed36f8e8032377ee5242090178c5875fc444f872e1e1df98db2aed799f9b1e6e664bd71d8c7e4f7b3739eb48000165e17bec2a1b8a2205ee927446bdae210f5d4d475c76c1178ad913b770bcd413019dd8c1939a8c069a447e4383e4aa1d6636e824632b56fdc3dc7447ba30f6a83b00017b84767ca227e22f8192b542c0688d413316e34c2059335311b1188a9c0e9c640159ce19fb2d0c0b844f38a48379313529a5a08c150fc62e174d17b00c99d54d0f018de4b3e39611e40a92dc0e1214110e088110ac37dc21f3f1d9f7a449447f6a700000017d1ce2f0839bdbf1bad7ae37f845e7fe2116e0c1197536bfbad549f3876c3c590000013e2598f743726006b8de42476ed56a55a75629a7b82e430c4e7c101a69e9b02a011619f99023a69bb647eab2d2aa1a73c3673c74bb033c3c4930eacda19e6fd93b0000000160272b134ca494b602137d89e528c751c06d3ef4a87a45f33af343c15060cc1e0000000000", + "orchardTree": "01d42f7d7a921175978a2f05a76421b2cc6cdd9094208774069c0daac6a1cc2e3d001f01736daf1dd19669a284dde4466e4adffb734f36921b3b2e5c8fc9f268f4cd2b0c01c976589de2ce272a89d8a42bc9fe9b67eef8e412febb77522f5cc6f6a2d28911000001ca55840c108bf99da4fe54815f38befeaee62b0a675a667b4cc3ba938f79ec3401b5ed5be30d698e1761fda70b24a7cabd3ef50b59a2a5ebeb6466240b42f9250501f31d8aa8c94cbb20ebf305b1f697885c005742a8475a4c373fc9c528ad38d11501dfc7853b2d5bc8c06e13425a66875f84e7a63351d456473d3ef5fc8261c31018000001fd8d40ccb9fb2e104aa0357a122141052c9a741c4c2c736b6dd0363ddbf27e2900019d2e26bdef115ced7bd36305a54386996133c4e65759f3731637a40eba67da100000016f3f63aab58e63b6449583df5658a91972a20291c6311b5b3e5240aff8d7d0020112278dfeae9949f887b70ae81e084f8897a5054627acef3efd01c8b29793d522000160040850b766b126a2b4843fcdfdffa5d5cab3f53bc860a3bef68958b5f066170001cc2dcaa338b312112db04b435a706d63244dd435238f0aa1e9e1598d35470810012dcc4273c8a0ed2337ecf7879380a07e7d427c7f9d82e538002bd1442978402c01daf63debf5b40df902dae98dadc029f281474d190cddecef1b10653248a234150001e2bca6a8d987d668defba89dc082196a922634ed88e065c669e526bb8815ee1b000000000000" +} diff --git a/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/2752500.json b/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/2752500.json new file mode 100644 index 000000000..d781d7bbd --- /dev/null +++ b/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/2752500.json @@ -0,0 +1,8 @@ +{ + "network": "main", + "height": "2752500", + "hash": "00000000006d5090c40a8b31f5f0be4f6916b0c6fdb3a9463bb1f87c6c2e91ba", + "time": 1734327484, + "saplingTree": "014c1e2fe9d6e3865a3293a064395785bf333d0bcb0e2ee560bb517a57b585bc68001f01361564bf128c089150e813aa42bcbb9baa77d5343658e35176bcff5cc98bb44d000000000001bc23494606747713798819a894eb3d2070722923d7984267b361244c3e1cb304013f3090ba125e5bcb7392c3d550687ed574474b997de7ba688be89a315de28557014d18bf94770ee9fad6aea6bfaa11c25734d3844d359d962128953be2e06d32400165e17bec2a1b8a2205ee927446bdae210f5d4d475c76c1178ad913b770bcd413019dd8c1939a8c069a447e4383e4aa1d6636e824632b56fdc3dc7447ba30f6a83b00017b84767ca227e22f8192b542c0688d413316e34c2059335311b1188a9c0e9c640159ce19fb2d0c0b844f38a48379313529a5a08c150fc62e174d17b00c99d54d0f018de4b3e39611e40a92dc0e1214110e088110ac37dc21f3f1d9f7a449447f6a700000017d1ce2f0839bdbf1bad7ae37f845e7fe2116e0c1197536bfbad549f3876c3c590000013e2598f743726006b8de42476ed56a55a75629a7b82e430c4e7c101a69e9b02a011619f99023a69bb647eab2d2aa1a73c3673c74bb033c3c4930eacda19e6fd93b0000000160272b134ca494b602137d89e528c751c06d3ef4a87a45f33af343c15060cc1e0000000000", + "orchardTree": "011ba122ece2417fce7bd84472c4e913cee4ade839c3ff94748f859e423a911509001f00000000010f5acbc8c7077c83b07db45ab5ecb29d3d94b384b76274de92dc8ac3c069d8290156c7a06eea3ed6ef382f9dcefeb8a865c3e65db5050991d9e8f07ef847afca3801cbabaf82980a66657d94bac8e58e868bf8af6a75c5d4640a1311c4b691ee612401697f5d2858d9c5da1b32874ccb063d97494888613bbe7b1a2b542d0e38acd71300016fac33462496e7df033277558b54ac238f073776cb533551ced42caeba694f3101fd8d40ccb9fb2e104aa0357a122141052c9a741c4c2c736b6dd0363ddbf27e2900019d2e26bdef115ced7bd36305a54386996133c4e65759f3731637a40eba67da100000016f3f63aab58e63b6449583df5658a91972a20291c6311b5b3e5240aff8d7d0020112278dfeae9949f887b70ae81e084f8897a5054627acef3efd01c8b29793d522000160040850b766b126a2b4843fcdfdffa5d5cab3f53bc860a3bef68958b5f066170001cc2dcaa338b312112db04b435a706d63244dd435238f0aa1e9e1598d35470810012dcc4273c8a0ed2337ecf7879380a07e7d427c7f9d82e538002bd1442978402c01daf63debf5b40df902dae98dadc029f281474d190cddecef1b10653248a234150001e2bca6a8d987d668defba89dc082196a922634ed88e065c669e526bb8815ee1b000000000000" +} diff --git a/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/testnet/3080000.json b/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/testnet/3080000.json new file mode 100644 index 000000000..e42982f05 --- /dev/null +++ b/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/testnet/3080000.json @@ -0,0 +1,8 @@ +{ + "network": "test", + "height": "3080000", + "hash": "002a90e74aafa829d2aeefe4f8572edddc7f1c121000c21f6f87aa373671687c", + "time": 1731869154, + "saplingTree": "0185ff90435d7b02593330cbdcc70eae8672e4d1bd0e03152020a2cb8677099e5201d6153e39af6e652c027f718299c09e824adb84963536f3234f517b9b3a41df0111014af4f83098e0a2423625ebfeae509234b6e1e7567ff6910efa6b0c1a9dfa8c0a01cca7ee1769be9faf1d1d2b0e225d704a3d4e98cfe0bec0cddf8dbb9b2714907201b5042b69142c1a01ae6dbb59c6d0bdf1552818f982a82b171ee649ef69f63e0a01509f1781bc56cbc94bde1863a196cbc73ccf051e8a926feb36ccf7e8ff2b050b000159ccc52272ddf6003f0cb858e0cb22098a812d094f8213dccdb157cf404cf91400000001dff6434111d79111f5ef072c5a72198b3f1d76ecd9064c0203bb2006186ccd0f0000000130f076389232b609870dff6263783bec5933c52c6baf433291507c04fd03961e0104eff5c9dbbdba73d2a4a5160727aaba4719e1ddb0368e2f54bd0444577d232d00018f624406b4ebda9ee13c062063e16f65ab5d725b80645037be7760e4ca788773", + "orchardTree": "019c23cfc38b5ff7052f14cc58f448de9370d53ba613c307e9db12f3f843116b22001f0001f895e2051f4ea01a4e924eb4dc4e55a11442a0277d53f9019479e0179c661f14000001de45ad5b0bd93da3cca98735f6803c857e6fa7351b539412005e5a47d5c416330000015f658aa7664968cef5e3208c17d94c3409064547d3768fc6bcb37be0c430353800000000013cb44365352b5ccec9ad593c5b5dea65eba0a79be0edd55d875a8462f5f2bb3b0001f3bbdc62260c4fca5c84bf3487246d4542da48eeeec8ec40c1029b6908eef83c00000000000000000000000000000000" +} diff --git a/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/testnet/3090000.json b/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/testnet/3090000.json new file mode 100644 index 000000000..cead26065 --- /dev/null +++ b/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/testnet/3090000.json @@ -0,0 +1,8 @@ +{ + "network": "test", + "height": "3090000", + "hash": "0000005a823524433c61fd1cbb2fede136319c2a8ff583ec0a7376fb00c10584", + "time": 1732259591, + "saplingTree": "010254475cbed32d84d4bb6bbc66d7273daeb2a6180ab5599945f84bb0af5edd29013c6f479a24996fe1d8cffdcb06b8a690ea2d815ce708ee824ba8cf942289ce661101467cc34200a1ea2acf6cb61819340da463687a215f5a5562827e6acfe126551f013f0f7a7abfc4be57af75e153ca7a57be5fe46e3d55cea3bd318e6c06029a9f150001f5e6ab0ccf3d939cf3de3cdf09c4cfca351b07f27eb71e0ea1de89eabf5d615c000000000001e30a6afd14083c447f49d2cb429b5541d11e3d081d085f7b6a5d7f5c1704526c00015583dbbc045ee3cdd67165bf38b2bff1c12957b08d58a4f05b294cd0a0138310000130f076389232b609870dff6263783bec5933c52c6baf433291507c04fd03961e0104eff5c9dbbdba73d2a4a5160727aaba4719e1ddb0368e2f54bd0444577d232d00018f624406b4ebda9ee13c062063e16f65ab5d725b80645037be7760e4ca788773", + "orchardTree": "018f235affd927c542ad81e0add50ec41d5e9df14537adfb96ecdaae8d7925023a001f00018ec519a4125d3b0203c76093a41b38d8eea060999aa9e11be7b3994c3f7857120001a0d9ec8a81bedb4ed74a88fed08f28deca3838ceed7270dbf3f29ee494ca041a01de45ad5b0bd93da3cca98735f6803c857e6fa7351b539412005e5a47d5c416330000015f658aa7664968cef5e3208c17d94c3409064547d3768fc6bcb37be0c430353800000000013cb44365352b5ccec9ad593c5b5dea65eba0a79be0edd55d875a8462f5f2bb3b0001f3bbdc62260c4fca5c84bf3487246d4542da48eeeec8ec40c1029b6908eef83c00000000000000000000000000000000" +} diff --git a/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/testnet/3100000.json b/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/testnet/3100000.json new file mode 100644 index 000000000..89e3aabf6 --- /dev/null +++ b/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/testnet/3100000.json @@ -0,0 +1,8 @@ +{ + "network": "test", + "height": "3100000", + "hash": "00472745b17d64bd1830f36128b3c6f8c0c9c519a367f00bd7995af941938e5f", + "time": 1732650769, + "saplingTree": "01e434fcc210bf1f354123bfe27c5711e0008ec0adb0ec2c10238e49dc3dca5c3b00110185758c1ae702378b1b39090bf297fa6bc8c737688c5ce57ddddda82d2578cc4a00012b539b8d23156b45a586150ac04f5a079019114a0f5a2026746d5934ea39895e0001f045d47b510a40eddc86b6d376606f716364d8cf628f591ca68393259c6e3c5301c200586c76044a927f19a346a27cf31de12c67e905dcd8b1ec75c2d5facc6d5600000001a8b0d3284e7e6adf08223ac72322e48be6de769c139b215973f65435a8a3ee20000001b476dc8311f78e98fd6314681739d145f132f5e4111240a81f35a561809ec1130130f076389232b609870dff6263783bec5933c52c6baf433291507c04fd03961e0104eff5c9dbbdba73d2a4a5160727aaba4719e1ddb0368e2f54bd0444577d232d00018f624406b4ebda9ee13c062063e16f65ab5d725b80645037be7760e4ca788773", + "orchardTree": "01e4553c6167f826dcf4865f0adc97b664bb835e310cc09e4e2aa9a40ffc963b32001f013f4eb100d180f0397454e1798de5fbf31be44b0dafc1b80ed8d019b54d75380b01763f9773b153fd361a54a7d5867b976ebdc31ee0bd7b77096d5214a43d1d0130015ca82fdd6aa20daf3287524893b1e746588c59b859ea81693c501d5ba0745f0901a0d9ec8a81bedb4ed74a88fed08f28deca3838ceed7270dbf3f29ee494ca041a01de45ad5b0bd93da3cca98735f6803c857e6fa7351b539412005e5a47d5c416330000015f658aa7664968cef5e3208c17d94c3409064547d3768fc6bcb37be0c430353800000000013cb44365352b5ccec9ad593c5b5dea65eba0a79be0edd55d875a8462f5f2bb3b0001f3bbdc62260c4fca5c84bf3487246d4542da48eeeec8ec40c1029b6908eef83c00000000000000000000000000000000" +} diff --git a/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/testnet/3110000.json b/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/testnet/3110000.json new file mode 100644 index 000000000..ff4ea3547 --- /dev/null +++ b/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/testnet/3110000.json @@ -0,0 +1,8 @@ +{ + "network": "test", + "height": "3110000", + "hash": "001875fb89d1618d412c27476047f4d816a71c30b1f83762bfce00294f7d5742", + "time": 1733125005, + "saplingTree": "0138838467aa21548d2d4466831fe2ef4f96d846eb4396d1e01ddcfbf9b6dde660001100014db08e4c534c606631a742e566bb0e702cc9627c96832709a18d99124eefba73012ad63339fb5a3aec2ecf2a541d62b9c2cba8d4b6154e7cbbeae908ea25e559070001679d223b82880f1e5f0c74de928eaf4154d48919687c7d059bbaf8e95300c91d016b0672b7fdd8b59b2124aff09986d91c03c348ad9d4b5743ef1a599ecd1aaa2d01bfe7864be6b6193de7ca9d9e402bf359e3abc66410898b08e7e247893869db09012223083558c03f0dfe11a3555c8f383f76f480a17f3304cb47e810136b10c6540001cff1939d0818839b7c798bfeaa442bd93f38a132bf7af927724445f1c87ed60c0001825e9db93188bec280600c2f2cdba326850fe8bb1c5db4b6e182fe7eced7073d01b476dc8311f78e98fd6314681739d145f132f5e4111240a81f35a561809ec1130130f076389232b609870dff6263783bec5933c52c6baf433291507c04fd03961e0104eff5c9dbbdba73d2a4a5160727aaba4719e1ddb0368e2f54bd0444577d232d00018f624406b4ebda9ee13c062063e16f65ab5d725b80645037be7760e4ca788773", + "orchardTree": "01c34138721af834499f591ee3717e7ea15c8d21ff1a109b2c380e47677b297f13001f00017254103a74032e86b00ee81c9f5e0d5241a85968a5abcad6c2686447430dc50c01a93d711993ca56c8a9b2b7aa542743a29ed21e592615d0c046c584e216776e08018f282139bf4f0792273c5aacbea5c97a33df1a68cf6cb2c8922e3d6e4d28c21a00012c0c1df2b675da9fd1519d693fa6b0c6ba508314d789a664224e86c1feee951d00015f658aa7664968cef5e3208c17d94c3409064547d3768fc6bcb37be0c430353800000000013cb44365352b5ccec9ad593c5b5dea65eba0a79be0edd55d875a8462f5f2bb3b0001f3bbdc62260c4fca5c84bf3487246d4542da48eeeec8ec40c1029b6908eef83c00000000000000000000000000000000" +} diff --git a/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/testnet/3120000.json b/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/testnet/3120000.json new file mode 100644 index 000000000..024a8eafb --- /dev/null +++ b/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/testnet/3120000.json @@ -0,0 +1,8 @@ +{ + "network": "test", + "height": "3120000", + "hash": "001401bf335c8926ef6be415b395519bbcf062553c84208996cc2b4e7147f0d0", + "time": 1733607746, + "saplingTree": "01cb12b6a3c55d44a8993ec9a506e60c3b636931331b75917dfe035435de8c22660011000000015d8b1752b0bdd324f8e6d4b778266cc5f9be8dff2196d727924dd37a9de9cf46016da5dfeac532245c91ed2f976180bfc00a9d1af95f725af60a41bad7c43baa0b0001b6fa71a8547976c1a69e15361fbe1628eee7555439dfe1a84f11c94df28170140000000115e8085e33bd0ffbb89aef538b6330dc8818e4103a9c8105ea0c97abd5b9350c0000000001b5a29ce44fa0c86b45faa10078c343c3de74f8f7b47a9c3d116b4c9569a0d608018f624406b4ebda9ee13c062063e16f65ab5d725b80645037be7760e4ca788773", + "orchardTree": "01f87c87cad21096b4984d7820a87eeaa5f7382a9c0591898ef7e6f7b52a4d8226001f01aacc971837e965dae11f9d60b9f1a5f0408a0d465109ac3f6ced7341abc0d01d0001930cc9557cb67a0e73494835d32d3abe0687cc678bc4d67848817781e7c64d2000012d4e61cb4e63f8675061dafd93cedb5150f4cf74a594b09641287b91c859dd1401f6099f365bf6282b205d744f8408a196fcae07305fb6bbd0aec5d2b4c441981e016651d9c8fe72b13818bf5629e74422e8c7285e1729178c752c97b3bb458dc001015f658aa7664968cef5e3208c17d94c3409064547d3768fc6bcb37be0c430353800000000013cb44365352b5ccec9ad593c5b5dea65eba0a79be0edd55d875a8462f5f2bb3b0001f3bbdc62260c4fca5c84bf3487246d4542da48eeeec8ec40c1029b6908eef83c00000000000000000000000000000000" +} diff --git a/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/testnet/3130000.json b/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/testnet/3130000.json new file mode 100644 index 000000000..db053bf95 --- /dev/null +++ b/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/testnet/3130000.json @@ -0,0 +1,8 @@ +{ + "network": "test", + "height": "3130000", + "hash": "00063844e1af63384456562b5571bbe4c3a100626f3131eb7b533c13e429e70e", + "time": 1734081366, + "saplingTree": "01983b4c282b57740088615ebf03faf1fd992d71eca410000b8000042340f4e57101c0482494d340d0d6a9c442c11ccac5615e6d690b328cd1488fda0bfe875fce561101537690f4de5b197cf5d7afb638be551623a05869e1b9b9e8ffaf77964fb79b4d00019349d5f32b60b12456c5b5ef172a4c2554f4c5769c1291456c06d2720861c22100000001392eb52d8642d95d5520bab8281a0b48ea2eb0b738c937db8e97654e41b92b1e0128ec3a13dc534f033dfedd6fd86657ec94413b64120e3c310642ce9823847172013e1339ca97044ef2487f4962b57e27cbb233d463d43313e2c5db9b0eb8bbb73c0001f6ca66e66afcdda5237060e43d85754ced29dec23c9290f12a0f75da9fadaf030129040959eb2b835b7bfefd7cae00348bb233063bb3c8358fd760e5f45a116e1700000001b5a29ce44fa0c86b45faa10078c343c3de74f8f7b47a9c3d116b4c9569a0d608018f624406b4ebda9ee13c062063e16f65ab5d725b80645037be7760e4ca788773", + "orchardTree": "016bfa17a05f171afcf52ea2cf58e1f4170e73fb82555657f0c22281192713f427001f01675423c79ad16737ba585de064d3e70f5f6d109a445c072c4deb1314c5383e0001d66008ce69313ea5908092ac213163ad82fe504e109108858164a6253db06a1401930cc9557cb67a0e73494835d32d3abe0687cc678bc4d67848817781e7c64d2000012d4e61cb4e63f8675061dafd93cedb5150f4cf74a594b09641287b91c859dd1401f6099f365bf6282b205d744f8408a196fcae07305fb6bbd0aec5d2b4c441981e016651d9c8fe72b13818bf5629e74422e8c7285e1729178c752c97b3bb458dc001015f658aa7664968cef5e3208c17d94c3409064547d3768fc6bcb37be0c430353800000000013cb44365352b5ccec9ad593c5b5dea65eba0a79be0edd55d875a8462f5f2bb3b0001f3bbdc62260c4fca5c84bf3487246d4542da48eeeec8ec40c1029b6908eef83c00000000000000000000000000000000" +} 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 c46f15932..9f2f6bc54 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 @@ -2,18 +2,18 @@ package cash.z.ecc.android.sdk import android.content.Context import cash.z.ecc.android.sdk.Synchronizer.Status.DISCONNECTED +import cash.z.ecc.android.sdk.Synchronizer.Status.INITIALIZING import cash.z.ecc.android.sdk.Synchronizer.Status.STOPPED import cash.z.ecc.android.sdk.Synchronizer.Status.SYNCED import cash.z.ecc.android.sdk.Synchronizer.Status.SYNCING import cash.z.ecc.android.sdk.block.processor.CompactBlockProcessor import cash.z.ecc.android.sdk.block.processor.CompactBlockProcessor.State.Disconnected -import cash.z.ecc.android.sdk.block.processor.CompactBlockProcessor.State.Initialized +import cash.z.ecc.android.sdk.block.processor.CompactBlockProcessor.State.Initializing import cash.z.ecc.android.sdk.block.processor.CompactBlockProcessor.State.Stopped import cash.z.ecc.android.sdk.block.processor.CompactBlockProcessor.State.Synced import cash.z.ecc.android.sdk.block.processor.CompactBlockProcessor.State.Syncing import cash.z.ecc.android.sdk.exception.CompactBlockProcessorException import cash.z.ecc.android.sdk.exception.InitializeException -import cash.z.ecc.android.sdk.exception.LightWalletException import cash.z.ecc.android.sdk.exception.TransactionEncoderException import cash.z.ecc.android.sdk.ext.ConsensusBranchId import cash.z.ecc.android.sdk.ext.ZcashSdk @@ -43,10 +43,16 @@ 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.Pczt import cash.z.ecc.android.sdk.model.PercentDecimal import cash.z.ecc.android.sdk.model.Proposal import cash.z.ecc.android.sdk.model.TransactionOutput @@ -57,6 +63,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 @@ -88,6 +95,7 @@ import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map @@ -119,6 +127,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, @@ -150,6 +159,7 @@ class SdkSynchronizer private constructor( */ @Suppress("LongParameterList") internal suspend fun new( + context: Context, zcashNetwork: ZcashNetwork, alias: String, repository: DerivedDataRepository, @@ -166,6 +176,7 @@ class SdkSynchronizer private constructor( checkForExistingSynchronizers(synchronizerKey) SdkSynchronizer( + context, synchronizerKey, repository, txManager, @@ -215,9 +226,7 @@ class SdkSynchronizer private constructor( val coroutineScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) - override val orchardBalances = processor.orchardBalances.asStateFlow() - override val saplingBalances = processor.saplingBalances.asStateFlow() - override val transparentBalance = processor.transparentBalance.asStateFlow() + override val walletBalances = processor.walletBalances.asStateFlow() private val refreshExchangeRateUsd = MutableSharedFlow(replay = 1).apply { tryEmit(Unit) } @@ -462,6 +471,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 // @@ -511,7 +536,8 @@ class SdkSynchronizer private constructor( // Triggering UTXOs and transactions fetching at the beginning of the block synchronization right after the // app starts makes the transparent transactions appear faster. launch(CoroutineExceptionHandler(::onCriticalError)) { - refreshUtxos(Account.DEFAULT) + // Refresh UTXOs and transactions for all the wallet's accounts + refreshAllAccountsUtxos() refreshTransactions() } @@ -523,6 +549,7 @@ class SdkSynchronizer private constructor( processor.state.onEach { when (it) { + is Initializing -> INITIALIZING is Synced -> { val now = System.currentTimeMillis() // do a bit of housekeeping and then report synced status @@ -533,7 +560,7 @@ class SdkSynchronizer private constructor( is Stopped -> STOPPED is Disconnected -> DISCONNECTED - is Syncing, Initialized -> SYNCING + is Syncing -> SYNCING }.let { synchronizerStatus -> _status.value = synchronizerStatus } @@ -621,9 +648,10 @@ class SdkSynchronizer private constructor( val shouldRefresh = !scannedRange.isNullOrEmpty() || elapsedMillis > (ZcashSdk.POLL_INTERVAL * 5) val reason = if (scannedRange.isNullOrEmpty()) "it's been a while" else "new blocks were scanned" + // Refresh UTXOs, balances, and transactions for all the wallet's accounts if (shouldRefresh) { Twig.debug { "Triggering utxo refresh since $reason!" } - refreshUtxos(Account.DEFAULT) + refreshAllAccountsUtxos() Twig.debug { "Triggering balance refresh since $reason!" } refreshAllBalances() @@ -633,27 +661,104 @@ class SdkSynchronizer private constructor( } } + private suspend fun refreshAllAccountsUtxos() { + getAccounts().forEach { refreshUtxos(it) } + } + // // Account management // + private val refreshAccountsBus = MutableSharedFlow() + // 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 - ) + ).also { + refreshAccountsBus.emit(Unit) + } }.onFailure { Twig.error(it) { "Create account failed." } - }.getOrNull() + }.getOrElse { + throw InitializeException.CreateAccountException(it) + } + } + + 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() + }.onFailure { + Twig.error(it) { "Get wallet accounts failed." } + }.getOrElse { + throw InitializeException.GetAccountsException(it) + } + } + + override val accountsFlow: Flow?> = + channelFlow { + send(getAccounts()) + launch { + refreshAccountsBus.collect { + send(getAccounts()) + } + } + awaitClose() + }.stateIn( + coroutineScope, + SharingStarted.WhileSubscribed(), + null + ) + /** * Returns the current Unified Address for this account. */ @@ -756,72 +861,20 @@ class SdkSynchronizer private constructor( } } - @Deprecated( - message = "Upcoming SDK 2.1 will create multiple transactions at once for some recipients.", - replaceWith = - ReplaceWith( - "createProposedTransactions(proposeTransfer(usk.account, toAddress, amount, memo), usk)" - ) - ) - @Throws(TransactionEncoderException::class, LightWalletException.TransactionSubmitException::class) - override suspend fun sendToAddress( - usk: UnifiedSpendingKey, - amount: Zatoshi, - toAddress: String, - memo: String - ): Long { - val encodedTx = - txManager.encode( - usk, - amount, - TransactionRecipient.Address(toAddress), - memo, - usk.account - ) - - when (txManager.submit(encodedTx)) { - is TransactionSubmitResult.Success -> { - return storage.findMatchingTransactionId(encodedTx.txId.byteArray)!! - } - else -> { - throw LightWalletException.TransactionSubmitException() - } - } - } + override suspend fun createPcztFromProposal( + accountUuid: AccountUuid, + proposal: Proposal + ) = txManager.createPcztFromProposal(accountUuid, proposal) - @Deprecated( - message = "Upcoming SDK 2.1 will create multiple transactions at once for some recipients.", - replaceWith = - ReplaceWith( - "proposeShielding(usk.account, shieldingThreshold, memo)?.let { createProposedTransactions(it, usk) }" - ) - ) - @Throws(TransactionEncoderException::class, LightWalletException.TransactionSubmitException::class) - override suspend fun shieldFunds( - usk: UnifiedSpendingKey, - memo: String - ): Long { - Twig.debug { "Initializing shielding transaction" } - val tAddr = CompactBlockProcessor.getTransparentAddress(backend, usk.account) - val tBalance = processor.getUtxoCacheBalance(tAddr) - - val encodedTx = - txManager.encode( - usk, - tBalance, - TransactionRecipient.Account(usk.account), - memo, - usk.account - ) + override suspend fun addProofsToPczt(pczt: Pczt) = txManager.addProofsToPczt(pczt) - when (txManager.submit(encodedTx)) { - is TransactionSubmitResult.Success -> { - return storage.findMatchingTransactionId(encodedTx.txId.byteArray)!! - } - else -> { - throw LightWalletException.TransactionSubmitException() - } - } + override suspend fun createTransactionFromPczt( + pcztWithProofs: Pczt, + pcztWithSignatures: Pczt + ): Flow { + // Internally, this logic submits and checks the newly stored and encoded transaction + return flowOf(txManager.extractAndStoreTxFromPczt(pcztWithProofs, pcztWithSignatures)) + .map { transaction -> txManager.submit(transaction) } } override suspend fun refreshUtxos( @@ -1025,22 +1078,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 b59ce29af..079cf840c 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 @@ -6,8 +6,9 @@ import cash.z.ecc.android.sdk.WalletInitMode.NewWallet 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.exception.PcztException +import cash.z.ecc.android.sdk.exception.RustLayerException 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 @@ -16,9 +17,14 @@ import cash.z.ecc.android.sdk.internal.db.DatabaseCoordinator 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 +import cash.z.ecc.android.sdk.model.Pczt import cash.z.ecc.android.sdk.model.PercentDecimal import cash.z.ecc.android.sdk.model.Proposal import cash.z.ecc.android.sdk.model.TransactionOutput @@ -26,7 +32,6 @@ import cash.z.ecc.android.sdk.model.TransactionOverview import cash.z.ecc.android.sdk.model.TransactionRecipient import cash.z.ecc.android.sdk.model.TransactionSubmitResult import cash.z.ecc.android.sdk.model.UnifiedSpendingKey -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.tool.CheckpointTool @@ -78,19 +83,9 @@ interface Synchronizer { val networkHeight: StateFlow /** - * A stream of balance values for the orchard pool. + * A stream of wallet balances */ - val orchardBalances: StateFlow - - /** - * A stream of balance values for the sapling pool. - */ - val saplingBalances: StateFlow - - /** - * A stream of a balance for the transparent pool. - */ - val transparentBalance: StateFlow + val walletBalances: StateFlow?> /** * The latest known USD/ZEC exchange rate, paired with the time it was queried. @@ -124,16 +119,41 @@ interface Synchronizer { // /** - * Measure connection quality and speed of given [servers]. + * Returns all the wallet accounts or throws [InitializeException.GetAccountsException] * - * @return a [Flow] of fastest servers which updates it's state during measurement stages + * @return List of all wallet accounts + * @throws [InitializeException.GetAccountsException] in case of the operation failure */ - suspend fun getFastestServers( - context: Context, - servers: List - ): Flow + suspend fun getAccounts(): List + + /** + * Returns all the wallet accounts or throws [InitializeException.GetAccountsException] + * + * It's a Flow version of [getAccounts] + * + * @return Flow of all wallet accounts + * @throws [InitializeException.GetAccountsException] in case of the operation failure + */ + val accountsFlow: Flow?> + + /** + * 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, 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 importAccountByUfvk(setup: AccountImportSetup): Account - @Suppress("ktlint:standard:no-consecutive-comments") /** * Adds the next available account-level spend authority, given the current set of * [ZIP 316](https://zips.z.cash/zip-0316) account identifiers known, to the wallet @@ -153,43 +173,68 @@ 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 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. * - * This is not yet ready to be a public API! - * suspend fun createAccount(seed: ByteArray): UnifiedSpendingKey + * @throws [InitializeException.CreateAccountException] in case of the operation failure **/ + @Suppress("standard:no-consecutive-comments") + /* Not ready to be a public API; internal for testing only + suspend fun createAccount( + 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. * - * @param account the account whose address is of interest. Use Account.DEFAULT to get a result for the first - * account. + * @param account the account whose address is of interest. * * @return the current unified address for the given account. + * + * @throws RustLayerException.GetAddressException in case of the operation */ + @Throws(RustLayerException.GetAddressException::class) suspend fun getUnifiedAddress(account: Account): String /** * Gets the legacy Sapling address corresponding to the current unified address for the given account. * - * @param account the account whose address is of interest. Use Account.DEFAULT to get a result for the first - * account. + * @param account the account whose address is of interest. * * @return a legacy Sapling address for the given account. + * + * @throws RustLayerException.GetAddressException in case of the operation */ + @Throws(RustLayerException.GetAddressException::class) suspend fun getSaplingAddress(account: Account): String /** * Gets the legacy transparent address corresponding to the current unified address for the given account. * - * @param account the account whose address is of interest. Use Account.DEFAULT to get a result for the first - * account. + * @param account the account whose address is of interest. * * @return a legacy transparent address for the given account. + * + * @throws RustLayerException.GetAddressException in case of the operation */ + @Throws(RustLayerException.GetAddressException::class) suspend fun getTransparentAddress(account: Account): String /** @@ -269,43 +314,52 @@ interface Synchronizer { ): Flow /** - * Sends zatoshi. + * Creates a partially-created (unsigned without proofs) transaction from the given proposal. * - * @param usk the unified spending key associated with the notes that will be spent. - * @param amount the amount of zatoshi to send. - * @param toAddress the recipient's address. - * @param memo the optional memo to include as part of the transaction. - * - * @return a flow of PendingTransaction objects representing changes to the state of the - * transaction. Any time the state changes a new instance will be emitted by this flow. This is - * useful for updating the UI without needing to poll. Of course, polling is always an option - * for any wallet that wants to ignore this return value. - */ - @Deprecated( - message = "Upcoming SDK 2.1 will create multiple transactions at once for some recipients.", - replaceWith = - ReplaceWith( - "createProposedTransactions(proposeTransfer(usk.account, toAddress, amount, memo), usk)" - ) - ) - suspend fun sendToAddress( - usk: UnifiedSpendingKey, - amount: Zatoshi, - toAddress: String, - memo: String = "" - ): Long + * Do not call this multiple times in parallel, or you will generate PCZT instances that, if + * finalized, would double-spend the same notes. + * + * @param accountUuid The account for which the proposal was created. + * @param proposal The proposal for which to create the transaction. + * + * @return The partially created transaction in [Pczt] format. + * + * @throws PcztException.CreatePcztFromProposalException as a common indicator of the operation failure + */ + @Throws(PcztException.CreatePcztFromProposalException::class) + suspend fun createPcztFromProposal( + accountUuid: AccountUuid, + proposal: Proposal + ): Pczt - @Deprecated( - message = "Upcoming SDK 2.1 will create multiple transactions at once for some recipients.", - replaceWith = - ReplaceWith( - "proposeShielding(usk.account, shieldingThreshold, memo)?.let { createProposedTransactions(it, usk) }" - ) - ) - suspend fun shieldFunds( - usk: UnifiedSpendingKey, - memo: String = ZcashSdk.DEFAULT_SHIELD_FUNDS_MEMO_PREFIX - ): Long + /** + * Adds proofs to the given PCZT. + * + * @param pczt The partially created transaction in its serialized format. + * + * @return The updated PCZT in its serialized format. + * + * @throws PcztException.AddProofsToPcztException as a common indicator of the operation failure + */ + @Throws(PcztException.AddProofsToPcztException::class) + suspend fun addProofsToPczt(pczt: Pczt): Pczt + + /** + * Takes a PCZT that has been separately proven and signed, finalizes it, and stores + * it in the wallet. Internally, this logic also submits and checks the newly stored and encoded transaction. + * + * @param pcztWithProofs + * @param pcztWithSignatures + * + * @return The submission result of the completed transaction. + * + * @throws PcztException.ExtractAndStoreTxFromPcztException as a common indicator of the operation failure + */ + @Throws(PcztException.ExtractAndStoreTxFromPcztException::class) + suspend fun createTransactionFromPczt( + pcztWithProofs: Pczt, + pcztWithSignatures: Pczt, + ): Flow // TODO [#1534]: Add RustLayerException.ValidateAddressException // TODO [#1534]: https://github.com/Electric-Coin-Company/zcash-android-wallet-sdk/issues/1534 @@ -409,8 +463,7 @@ interface Synchronizer { /** * Download all UTXOs for the given account addresses and store any new ones in the database. * - * @param account The Account, for which all addresses blocks will be downloaded. Use Account.DEFAULT to get a - * result for the first account. + * @param account The Account, for which all addresses blocks will be downloaded. * @param since The BlockHeight, from which blocks will be downloaded. * * @return the number of utxos that were downloaded and added to the UTXO table. @@ -476,6 +529,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 // @@ -532,6 +593,11 @@ interface Synchronizer { * Represents the status of this Synchronizer, which is useful for communicating to the user. */ enum class Status { + /** + * Indicates the initial state of Synchronizer + */ + INITIALIZING, + /** * Indicates that [stop] has been called on this Synchronizer and it will no longer be used. */ @@ -586,14 +652,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 @@ -612,13 +678,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 @@ -679,9 +745,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) @@ -700,6 +765,7 @@ interface Synchronizer { ) return SdkSynchronizer.new( + context = context.applicationContext, zcashNetwork = zcashNetwork, alias = alias, repository = repository, @@ -723,16 +789,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 f04d4f420..af2683465 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 @@ -55,11 +55,12 @@ import cash.z.ecc.android.sdk.internal.model.ext.toTransactionStatus 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 import cash.z.ecc.android.sdk.model.TransactionSubmitResult -import cash.z.ecc.android.sdk.model.WalletBalance import cash.z.ecc.android.sdk.model.Zatoshi import co.electriccoin.lightwallet.client.model.BlockHeightUnsafe import co.electriccoin.lightwallet.client.model.GetAddressUtxosReplyUnsafe @@ -160,16 +161,12 @@ class CompactBlockProcessor internal constructor( ) ) - private val _state: MutableStateFlow = MutableStateFlow(State.Initialized) + private val _state: MutableStateFlow = MutableStateFlow(State.Initializing) private val _progress = MutableStateFlow(PercentDecimal.ZERO_PERCENT) private val _processorInfo = MutableStateFlow(ProcessorInfo(null, null, null)) private val _networkHeight = MutableStateFlow(null) private val _fullyScannedHeight = MutableStateFlow(null) - - // pools - internal val saplingBalances = MutableStateFlow(null) - internal val orchardBalances = MutableStateFlow(null) - internal val transparentBalance = MutableStateFlow(null) + internal val walletBalances = MutableStateFlow?>(null) private val processingMutex = Mutex() @@ -511,12 +508,12 @@ class CompactBlockProcessor internal constructor( if (fullyScannedHeight.value == null) { Twig.info { "Postponing UTXOs fetching because fullyScannedHeight is null" } } else { - val fetchedCount = + backend.getAccounts().forEach { refreshUtxos( - account = Account.DEFAULT, + account = it, startHeight = fullyScannedHeight.value!! ) - Twig.info { "UTXOs fetched count: $fetchedCount" } + } } } is SyncingResult.Failure -> { @@ -621,12 +618,12 @@ class CompactBlockProcessor internal constructor( if (fullyScannedHeight.value == null) { Twig.info { "Postponing UTXOs fetching because fullyScannedHeight is null" } } else { - val fetchedCount = + backend.getAccounts().forEach { refreshUtxos( - account = Account.DEFAULT, + account = it, startHeight = fullyScannedHeight.value!! ) - Twig.debug { "UTXOs fetched count: $fetchedCount" } + } } SyncingResult.AllSuccess } @@ -773,17 +770,13 @@ class CompactBlockProcessor internal constructor( /** * Update the latest balances using the given wallet summary, and transmit this information - * into the related internal flows. + * into the related internal flow. */ internal suspend fun updateAllBalances(summary: WalletSummary) { - summary.accountBalances[Account.DEFAULT]?.let { - Twig.debug { "Updating balances" } - saplingBalances.value = it.sapling - orchardBalances.value = it.orchard - // We only allow stored transparent balance to be shielded, and we do so with - // a zero-conf transaction, so treat all unshielded balance as available. - transparentBalance.value = it.unshielded - } + // We only allow stored transparent balance to be shielded, and we do so with + // a zero-conf transaction, so treat all unshielded balance as available. + Twig.debug { "Updating balances" } + walletBalances.value = summary.accountBalances } /** @@ -972,11 +965,17 @@ class CompactBlockProcessor internal constructor( Twig.error { "Downloading UTXO from height: $startHeight failed with: ${response.description}." } - throw LightWalletException.FetchUtxosException( - response.code, - response.description, - response.toThrowable() - ) + if (response is Response.Failure.Server.Unavailable) { + Twig.error { "Download UTXOs failed - setting Disconnected state" } + setState(State.Disconnected) + } else { + Twig.error { "Download UTXOs failed - throwing exception" } + throw LightWalletException.FetchUtxosException( + response.code, + response.description, + response.toThrowable() + ) + } } } }.onCompletion { @@ -2567,7 +2566,7 @@ class CompactBlockProcessor internal constructor( /** * [State] the initial state of the processor, once it is constructed. */ - object Initialized : State() + object Initializing : State() } /** 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 7cd75089a..fa1d1a448 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 @@ -24,7 +24,7 @@ open class SdkException(message: String, cause: Throwable?) : RuntimeException(m * It's important for the SDK to provide helpful messages whenever these errors are encountered. */ sealed class RustLayerException(message: String, cause: Throwable? = null) : SdkException(message, cause) { - class GetCurrentAddressException(cause: Throwable) : RustLayerException( + class GetAddressException(cause: Throwable) : RustLayerException( "Error while requesting the current address from the Rust layer over JNI. This might mean that the SDK is " + "not yet correctly set up.", cause @@ -204,7 +204,20 @@ sealed class InitializeException(message: String, cause: Throwable? = null) : Sd private fun readResolve(): Any = SeedNotRelevant } - class FalseStart(cause: Throwable?) : InitializeException("Failed to initialize accounts due to: $cause", cause) + class GetAccountsException(cause: Throwable?) : InitializeException( + "Failed to get accounts due to: ${cause?.message}", + cause + ) + + class CreateAccountException(cause: Throwable?) : InitializeException( + "Failed to create new account due to: ${cause?.message}", + 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" + @@ -314,6 +327,38 @@ sealed class LightWalletException(message: String, cause: Throwable? = null) : S ) } +/** + * Potentially user-facing exceptions thrown while creating transactions + */ +sealed class PcztException( + message: String, + cause: Throwable? = null +) : SdkException(message, cause) { + class CreatePcztFromProposalException internal constructor( + description: String?, + cause: Throwable? + ) : PcztException( + "Failed to create PCZT from proposal with message: ${description ?: "-"}", + cause + ) + + class AddProofsToPcztException internal constructor( + description: String?, + cause: Throwable? + ) : PcztException( + "Failed to add proofs to PCZT with message: ${description ?: "-"}", + cause + ) + + class ExtractAndStoreTxFromPcztException internal constructor( + description: String?, + cause: Throwable? + ) : PcztException( + "Failed to extract and store transaction from PCZT with message: ${description ?: "-"}", + cause + ) +} + /** * Potentially user-facing exceptions thrown while encoding transactions. */ 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 new file mode 100644 index 000000000..3e679806b --- /dev/null +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/fixture/AccountFixture.kt @@ -0,0 +1,88 @@ +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 + +/** + * This test fixture class provides a unified way for getting a fixture account for test purposes across the SDK's + * modules. + * + * Note that these values are used in the automated tests only and are not passed across the JNI. + */ +object AccountFixture { + const val ZIP_32_ACCOUNT_INDEX = 0L + + val ACCOUNT_UUID = UUID.fromString("01234567-89ab-cdef-0123-456789abcdef") + const val ACCOUNT_NAME = "Test Account" + const val UFVK = "ufvk1d68jqrx0q98rl0w8f5085y898x0p9z5k0sksqre87949w9494949" + const val KEY_SOURCE = "zcash" + + // 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] +private fun UUID.toByteArray(): ByteArray = + ByteBuffer + .allocate(UUID_V4_BYTE_SIZE) + .putLong(mostSignificantBits) + .putLong(leastSignificantBits) + .array() diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/fixture/WalletBalanceFixture.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/fixture/WalletBalanceFixture.kt index 664593970..011af6e7c 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/fixture/WalletBalanceFixture.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/fixture/WalletBalanceFixture.kt @@ -3,7 +3,6 @@ package cash.z.ecc.android.sdk.fixture import cash.z.ecc.android.sdk.model.WalletBalance import cash.z.ecc.android.sdk.model.Zatoshi -@Suppress("MagicNumber") object WalletBalanceFixture { const val AVAILABLE: Long = 8L const val CHANGE_PENDING: Long = 4 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..af48a6205 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,9 +13,13 @@ 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.Pczt import cash.z.ecc.android.sdk.model.Proposal +import cash.z.ecc.android.sdk.model.UnifiedFullViewingKey import cash.z.ecc.android.sdk.model.UnifiedSpendingKey import cash.z.ecc.android.sdk.model.Zatoshi import cash.z.ecc.android.sdk.model.ZcashNetwork @@ -27,10 +31,20 @@ 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 getAccountForUfvk(ufvk: UnifiedFullViewingKey): Account? suspend fun proposeTransferFromUri( account: Account, @@ -57,7 +71,44 @@ internal interface TypesafeBackend { usk: UnifiedSpendingKey ): List - @Throws(RustLayerException.GetCurrentAddressException::class) + /** + * Creates a partially-created (unsigned without proofs) transaction from the given proposal. + * + * Do not call this multiple times in parallel, or you will generate PCZT instances that, if + * finalized, would double-spend the same notes. + * + * @return the partially created transaction in its serialized format. + * + * @throws RuntimeException as a common indicator of the operation failure + */ + suspend fun createPcztFromProposal( + account: Account, + proposal: Proposal + ): Pczt + + /** + * Adds proofs to the given PCZT. + * + * @return the updated PCZT in its serialized format. + * + * @throws RuntimeException as a common indicator of the operation failure + */ + suspend fun addProofsToPczt(pczt: Pczt): Pczt + + /** + * Takes a PCZT that has been separately proven and signed, finalizes it, and stores + * it in the wallet. + * + * @return the txid of the completed transaction. + * + * @throws RuntimeException as a common indicator of the operation failure + */ + suspend fun extractAndStoreTxFromPczt( + pcztWithProofs: Pczt, + pcztWithSignatures: Pczt, + ): FirstClassByteArray + + @Throws(RustLayerException.GetAddressException::class) suspend fun getCurrentAddress(account: Account): String suspend fun listTransparentReceivers(account: Account): List @@ -90,7 +141,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..2da33a230 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,9 +14,14 @@ 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.Pczt import cash.z.ecc.android.sdk.model.Proposal +import cash.z.ecc.android.sdk.model.UnifiedFullViewingKey import cash.z.ecc.android.sdk.model.UnifiedSpendingKey import cash.z.ecc.android.sdk.model.Zatoshi import cash.z.ecc.android.sdk.model.ZcashNetwork @@ -27,29 +32,71 @@ 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 getAccountForUfvk(ufvk: UnifiedFullViewingKey): Account? { + return backend.getAccountForUfvk(ufvk = ufvk.encoding)?.let { Account.new(it) } + } + override suspend fun proposeTransferFromUri( account: Account, uri: String ): Proposal = Proposal.fromUnsafe( backend.proposeTransferFromUri( - account.value, + account.accountUuid.value, uri ) ) @@ -63,7 +110,7 @@ internal class TypesafeBackendImpl(private val backend: Backend) : TypesafeBacke ): Proposal = Proposal.fromUnsafe( backend.proposeTransfer( - account.value, + account.accountUuid.value, to, value, memo @@ -77,7 +124,7 @@ internal class TypesafeBackendImpl(private val backend: Backend) : TypesafeBacke transparentReceiver: String? ): Proposal? = backend.proposeShielding( - account.value, + account.accountUuid.value, shieldingThreshold, memo, transparentReceiver @@ -96,16 +143,40 @@ internal class TypesafeBackendImpl(private val backend: Backend) : TypesafeBacke usk.copyBytes() ).map { FirstClassByteArray(it) } + override suspend fun createPcztFromProposal( + account: Account, + proposal: Proposal + ): Pczt = + Pczt( + backend.createPcztFromProposal( + account.accountUuid.value, + proposal.toUnsafe() + ) + ) + + override suspend fun addProofsToPczt(pczt: Pczt): Pczt = Pczt(backend.addProofsToPczt(pczt.toByteArray())) + + override suspend fun extractAndStoreTxFromPczt( + pcztWithProofs: Pczt, + pcztWithSignatures: Pczt + ): FirstClassByteArray = + FirstClassByteArray( + backend.extractAndStoreTxFromPczt( + pcztWithProofs.toByteArray(), + pcztWithSignatures.toByteArray() + ) + ) + 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) } + }.getOrElse { throw RustLayerException.GetAddressException(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 +237,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 d70fe4cf4..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,13 +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)) { - TransactionRecipient.Account(Account(it.getInt(toAccountIndex))) + TransactionRecipient.RecipientAccount(accountUuid = it.getBlob(toAccountIndex)) } else { - TransactionRecipient.Address(it.getString(toAddressIndex)) + TransactionRecipient.RecipientAddress(addressValue = it.getString(toAddressIndex)) } } ) @@ -92,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 c28946007..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,10 +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, @@ -15,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/OutboundTransactionManager.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/OutboundTransactionManager.kt index c3fa8248f..548b1e7bc 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/OutboundTransactionManager.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/OutboundTransactionManager.kt @@ -2,8 +2,9 @@ package cash.z.ecc.android.sdk.internal.transaction import cash.z.ecc.android.sdk.internal.model.EncodedTransaction import cash.z.ecc.android.sdk.model.Account +import cash.z.ecc.android.sdk.model.AccountUuid +import cash.z.ecc.android.sdk.model.Pczt import cash.z.ecc.android.sdk.model.Proposal -import cash.z.ecc.android.sdk.model.TransactionRecipient import cash.z.ecc.android.sdk.model.TransactionSubmitResult import cash.z.ecc.android.sdk.model.UnifiedSpendingKey import cash.z.ecc.android.sdk.model.Zatoshi @@ -15,26 +16,6 @@ import cash.z.ecc.android.sdk.model.Zatoshi */ @Suppress("TooManyFunctions") internal interface OutboundTransactionManager { - /** - * Encode the pending transaction using the given spending key. This is a local operation that - * produces a raw transaction to submit to lightwalletd. - * - * @param usk the unified spending key to use for constructing the transaction. - * @param amount the amount to send. - * @param recipient the recipient of the transaction. - * @param memo the memo to include in the transaction. - * @param account the account to use for the transaction. - * - * @return The encoded transaction, which can be submitted to lightwalletd. - */ - suspend fun encode( - usk: UnifiedSpendingKey, - amount: Zatoshi, - recipient: TransactionRecipient, - memo: String, - account: Account - ): EncodedTransaction - /** * Creates a proposal for transferring funds from a ZIP-321 compliant payment URI * @@ -114,6 +95,18 @@ internal interface OutboundTransactionManager { */ suspend fun submit(encodedTransaction: EncodedTransaction): TransactionSubmitResult + suspend fun createPcztFromProposal( + accountUuid: AccountUuid, + proposal: Proposal + ): Pczt + + suspend fun addProofsToPczt(pczt: Pczt): Pczt + + suspend fun extractAndStoreTxFromPczt( + pcztWithProofs: Pczt, + pcztWithSignatures: Pczt + ): EncodedTransaction + /** * Return true when the given address is a valid t-addr. * 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..39bf5c704 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 @@ -4,8 +4,9 @@ import cash.z.ecc.android.sdk.internal.Twig import cash.z.ecc.android.sdk.internal.ext.toHexReversed import cash.z.ecc.android.sdk.internal.model.EncodedTransaction import cash.z.ecc.android.sdk.model.Account +import cash.z.ecc.android.sdk.model.AccountUuid +import cash.z.ecc.android.sdk.model.Pczt import cash.z.ecc.android.sdk.model.Proposal -import cash.z.ecc.android.sdk.model.TransactionRecipient import cash.z.ecc.android.sdk.model.TransactionSubmitResult import cash.z.ecc.android.sdk.model.UnifiedSpendingKey import cash.z.ecc.android.sdk.model.Zatoshi @@ -17,38 +18,6 @@ internal class OutboundTransactionManagerImpl( internal val encoder: TransactionEncoder, private val service: LightWalletClient ) : OutboundTransactionManager { - override suspend fun encode( - usk: UnifiedSpendingKey, - amount: Zatoshi, - recipient: TransactionRecipient, - memo: String, - account: Account - ): EncodedTransaction { - val memoBytes = - if (memo.isBlank()) { - null - } else { - memo.toByteArray() - } - return when (recipient) { - is TransactionRecipient.Account -> { - encoder.createShieldingTransaction( - usk, - recipient, - memoBytes - ) - } - is TransactionRecipient.Address -> { - encoder.createTransaction( - usk, - amount, - recipient, - memoBytes - ) - } - } - } - /** * Creates a proposal for transferring funds from a ZIP-321 compliant payment URI * @@ -136,6 +105,18 @@ internal class OutboundTransactionManagerImpl( } } + override suspend fun createPcztFromProposal( + accountUuid: AccountUuid, + proposal: Proposal + ) = encoder.createPcztFromProposal(accountUuid, proposal) + + override suspend fun addProofsToPczt(pczt: Pczt) = encoder.addProofsToPczt(pczt) + + override suspend fun extractAndStoreTxFromPczt( + pcztWithProofs: Pczt, + pcztWithSignatures: Pczt + ) = encoder.extractAndStoreTxFromPczt(pcztWithProofs, pcztWithSignatures) + override suspend fun isValidShieldedAddress(address: String) = encoder.isValidShieldedAddress(address) override suspend fun isValidTransparentAddress(address: String) = encoder.isValidTransparentAddress(address) 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..7b983c283 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 @@ -3,45 +3,15 @@ package cash.z.ecc.android.sdk.internal.transaction import cash.z.ecc.android.sdk.exception.TransactionEncoderException import cash.z.ecc.android.sdk.internal.model.EncodedTransaction import cash.z.ecc.android.sdk.model.Account +import cash.z.ecc.android.sdk.model.AccountUuid import cash.z.ecc.android.sdk.model.BlockHeight +import cash.z.ecc.android.sdk.model.Pczt import cash.z.ecc.android.sdk.model.Proposal -import cash.z.ecc.android.sdk.model.TransactionRecipient import cash.z.ecc.android.sdk.model.UnifiedSpendingKey import cash.z.ecc.android.sdk.model.Zatoshi @Suppress("TooManyFunctions") internal interface TransactionEncoder { - /** - * Creates a transaction, throwing an exception whenever things are missing. When the provided - * wallet implementation doesn't throw an exception, we wrap the issue into a descriptive - * exception ourselves (rather than using double-bangs for things). - * - * @param usk the unified spending key associated with the notes that will be spent. - * @param amount the amount of zatoshi to send. - * @param toAddress the recipient's address. - * @param memo the optional memo to include as part of the transaction. - * - * @return the successfully encoded transaction or an exception - */ - suspend fun createTransaction( - usk: UnifiedSpendingKey, - amount: Zatoshi, - recipient: TransactionRecipient, - memo: ByteArray? = byteArrayOf() - ): EncodedTransaction - - /** - * Creates a transaction that shields any transparent funds sent to the given usk's account. - * - * @param usk the unified spending key associated with the transparent funds that will be shielded. - * @param memo the optional memo to include as part of the transaction. - */ - suspend fun createShieldingTransaction( - usk: UnifiedSpendingKey, - recipient: TransactionRecipient, - memo: ByteArray? = byteArrayOf() - ): EncodedTransaction - /** * Creates a proposal for transferring from a valid ZIP-321 Payment URI string * @@ -124,9 +94,20 @@ internal interface TransactionEncoder { usk: UnifiedSpendingKey ): List + suspend fun createPcztFromProposal( + accountUuid: AccountUuid, + proposal: Proposal + ): Pczt + + suspend fun addProofsToPczt(pczt: Pczt): Pczt + + suspend fun extractAndStoreTxFromPczt( + pcztWithProofs: Pczt, + pcztWithSignatures: Pczt + ): EncodedTransaction + /** - * Utility function to help with validation. This is not called during [createTransaction] - * because this class asserts that all validation is done externally by the UI, for now. + * Utility function to help with validation. * * @param address the address to validate * @@ -135,8 +116,7 @@ internal interface TransactionEncoder { suspend fun isValidShieldedAddress(address: String): Boolean /** - * Utility function to help with validation. This is not called during [createTransaction] - * because this class asserts that all validation is done externally by the UI, for now. + * Utility function to help with validation. * * @param address the address to validate * @@ -145,8 +125,7 @@ internal interface TransactionEncoder { suspend fun isValidTransparentAddress(address: String): Boolean /** - * Utility function to help with validation. This is not called during [createTransaction] - * because this class asserts that all validation is done externally by the UI, for now. + * Utility function to help with validation. * * @param address the address to validate * 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..9f9cd03de 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 @@ -1,6 +1,6 @@ package cash.z.ecc.android.sdk.internal.transaction -import cash.z.ecc.android.sdk.exception.SdkException +import cash.z.ecc.android.sdk.exception.PcztException import cash.z.ecc.android.sdk.exception.TransactionEncoderException import cash.z.ecc.android.sdk.ext.masked import cash.z.ecc.android.sdk.internal.SaplingParamTool @@ -9,16 +9,16 @@ import cash.z.ecc.android.sdk.internal.TypesafeBackend 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.Account +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.Pczt import cash.z.ecc.android.sdk.model.Proposal -import cash.z.ecc.android.sdk.model.TransactionRecipient import cash.z.ecc.android.sdk.model.UnifiedSpendingKey import cash.z.ecc.android.sdk.model.Zatoshi /** * Class responsible for encoding a transaction in a consistent way. This bridges the gap by - * behaving like a stateless API so that callers can request [createTransaction] and receive a + * behaving like a stateless API so that callers can request create a transaction and receive a * result, even though there are intermediate database interactions. * * @property backend the instance of RustBackendWelding to use for creating and validating. @@ -31,45 +31,6 @@ internal class TransactionEncoderImpl( private val saplingParamTool: SaplingParamTool, private val repository: DerivedDataRepository ) : TransactionEncoder { - /** - * Creates a transaction, throwing an exception whenever things are missing. When the provided - * wallet implementation doesn't throw an exception, we wrap the issue into a descriptive - * exception ourselves (rather than using double-bangs for things). - * - * @param usk the unified spending key associated with the notes that will be spent. - * @param amount the amount of zatoshi to send. - * @param recipient the recipient's address. - * @param memo the optional memo to include as part of the transaction. - * - * @return the successfully encoded transaction or an exception - * - * @throws TransactionEncoderException.TransactionNotFoundException in case the encoded transaction not found - */ - override suspend fun createTransaction( - usk: UnifiedSpendingKey, - amount: Zatoshi, - recipient: TransactionRecipient, - memo: ByteArray? - ): EncodedTransaction { - require(recipient is TransactionRecipient.Address) - - val transactionId = createSpend(usk, amount, recipient.addressValue, memo) - return repository.findEncodedTransactionByTxId(transactionId) - ?: throw TransactionEncoderException.TransactionNotFoundException(transactionId) - } - - override suspend fun createShieldingTransaction( - usk: UnifiedSpendingKey, - recipient: TransactionRecipient, - memo: ByteArray? - ): EncodedTransaction { - require(recipient is TransactionRecipient.Account) - - val transactionId = createShieldingSpend(usk, memo) - return repository.findEncodedTransactionByTxId(transactionId) - ?: throw TransactionEncoderException.TransactionNotFoundException(transactionId) - } - /** * Creates a proposal for transferring from a valid ZIP-321 Payment URI string * @@ -186,9 +147,62 @@ internal class TransactionEncoderImpl( return txs } + override suspend fun createPcztFromProposal( + accountUuid: AccountUuid, + proposal: Proposal + ): Pczt { + return runCatching { + backend.createPcztFromProposal( + account = Account.new(accountUuid), + proposal = proposal + ) + }.onSuccess { + Twig.debug { "Result of createPcztFromProposal: $it" } + }.onFailure { + Twig.error(it) { "Caught exception while creating PCZT." } + }.getOrElse { + throw PcztException.CreatePcztFromProposalException(it.message, it.cause) + } + } + + override suspend fun addProofsToPczt(pczt: Pczt): Pczt { + return runCatching { + saplingParamTool.ensureParams(saplingParamTool.properties.paramsDirectory) + Twig.debug { "params exist! attempting to send..." } + backend.addProofsToPczt(pczt = pczt) + }.onSuccess { + Twig.debug { "Result of addProofsToPczt: $it" } + }.onFailure { + Twig.error(it) { "Caught exception while adding proofs to PCZT." } + }.getOrElse { + throw PcztException.AddProofsToPcztException(it.message, it.cause) + } + } + + override suspend fun extractAndStoreTxFromPczt( + pcztWithProofs: Pczt, + pcztWithSignatures: Pczt + ): EncodedTransaction { + val txId = + runCatching { + backend.extractAndStoreTxFromPczt( + pcztWithProofs = pcztWithProofs, + pcztWithSignatures = pcztWithSignatures + ) + }.onSuccess { + Twig.debug { "Result of extractAndStoreTxFromPczt: $it" } + }.onFailure { + Twig.error(it) { "Caught exception while extracting and storing transaction from PCZT." } + }.getOrElse { + throw PcztException.ExtractAndStoreTxFromPcztException(it.message, it.cause) + } + + return repository.findEncodedTransactionByTxId(txId) + ?: throw TransactionEncoderException.TransactionNotFoundException(txId) + } + /** - * Utility function to help with validation. This is not called during [createTransaction] - * because this class asserts that all validation is done externally by the UI, for now. + * Utility function to help with validation. * * @param address the address to validate * @@ -197,8 +211,7 @@ internal class TransactionEncoderImpl( override suspend fun isValidShieldedAddress(address: String): Boolean = backend.isValidSaplingAddr(address) /** - * Utility function to help with validation. This is not called during [createTransaction] - * because this class asserts that all validation is done externally by the UI, for now. + * Utility function to help with validation. * * @param address the address to validate * @@ -207,8 +220,7 @@ internal class TransactionEncoderImpl( override suspend fun isValidTransparentAddress(address: String): Boolean = backend.isValidTransparentAddr(address) /** - * Utility function to help with validation. This is not called during [createTransaction] - * because this class asserts that all validation is done externally by the UI, for now. + * Utility function to help with validation. * * @param address the address to validate * @@ -241,77 +253,4 @@ internal class TransactionEncoderImpl( } return backend.getBranchIdForHeight(height) } - - /** - * Does the proofs and processing required to create a transaction to spend funds and inserts - * the result in the database. On average, this call takes over 10 seconds. - * - * @param usk the unified spending key associated with the notes that will be spent. - * @param amount the amount of zatoshi to send. - * @param toAddress the recipient's address. - * @param memo the optional memo to include as part of the transaction. - * - * @return the row id in the transactions table that contains the spend transaction or -1 if it - * failed. - */ - private suspend fun createSpend( - usk: UnifiedSpendingKey, - amount: Zatoshi, - toAddress: String, - memo: ByteArray? = null - ): FirstClassByteArray { - Twig.debug { - "creating transaction to spend $amount zatoshi to" + - " ${toAddress.masked()} with memo: ${memo?.decodeToString()}" - } - - return runCatching { - saplingParamTool.ensureParams(saplingParamTool.properties.paramsDirectory) - Twig.debug { "params exist! attempting to send..." } - val proposal = - backend.proposeTransfer( - usk.account, - toAddress, - amount.value, - memo - ) - val transactionIds = backend.createProposedTransactions(proposal, usk) - assert(transactionIds.size == 1) - transactionIds[0] - }.onFailure { - Twig.error(it) { "Caught exception while creating transaction." } - }.onSuccess { result -> - Twig.debug { "result of sendToAddress: $result" } - }.getOrThrow() - } - - private suspend fun createShieldingSpend( - 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) - ?: throw SdkException( - "Insufficient balance (have 0, need $SHIELDING_THRESHOLD including fee)", - null - ) - val transactionIds = backend.createProposedTransactions(proposal, usk) - assert(transactionIds.size == 1) - transactionIds[0] - }.onFailure { - // TODO [#680]: if this error matches: Insufficient balance (have 0, need 1000 including fee) - // then consider custom error that says no UTXOs existed to shield - // TODO [#680]: https://github.com/zcash/zcash-android-wallet-sdk/issues/680 - Twig.error(it) { "Shield failed" } - }.onSuccess { result -> - Twig.debug { "result of shieldToAddress: $result" } - }.getOrThrow() - } - - companion object { - private const val SHIELDING_THRESHOLD = 100000L - } } diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/Account.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/Account.kt index 0c82fad26..5b085935b 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/Account.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/Account.kt @@ -1,16 +1,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 32. + * @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 { - val DEFAULT = Account(0) + 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/internal/model/AccountBalance.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/AccountBalance.kt similarity index 83% rename from sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/AccountBalance.kt rename to sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/AccountBalance.kt index 9b363baa3..5293de208 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/AccountBalance.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/AccountBalance.kt @@ -1,9 +1,8 @@ -package cash.z.ecc.android.sdk.internal.model +package cash.z.ecc.android.sdk.model -import cash.z.ecc.android.sdk.model.WalletBalance -import cash.z.ecc.android.sdk.model.Zatoshi +import cash.z.ecc.android.sdk.internal.model.JniAccountBalance -internal data class AccountBalance( +data class AccountBalance( val sapling: WalletBalance, val orchard: WalletBalance, val unshielded: Zatoshi 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..7470e3fc1 --- /dev/null +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/AccountImportSetup.kt @@ -0,0 +1,20 @@ +package cash.z.ecc.android.sdk.model + +/** + * Wrapper for the import account API based on viewing key. + * + * @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..3f9a71704 --- /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..d4e7558bb --- /dev/null +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/AccountUsk.kt @@ -0,0 +1,33 @@ +package cash.z.ecc.android.sdk.model + +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 [ZIP 316](https://zips.z.cash/zip-0316) Unified Spending Key. + */ + private val usk: UnifiedSpendingKey +) { + override fun toString() = "AccountUsk(account=$accountUuid, usk=$usk)" + + companion object { + suspend fun new(jniAccountUsk: JniAccountUsk): AccountUsk = + AccountUsk( + accountUuid = AccountUuid.new(jniAccountUsk.accountUuid), + usk = UnifiedSpendingKey.new(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/Pczt.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/Pczt.kt new file mode 100644 index 000000000..e2a5c600a --- /dev/null +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/Pczt.kt @@ -0,0 +1,20 @@ +package cash.z.ecc.android.sdk.model + +class Pczt( + private val inner: ByteArray +) { + /** + * Exposes this PCZT's serialized [ByteArray] for conveyance purposes. + */ + fun toByteArray(): ByteArray { + return inner + } + + /** + * Clones this object with its inner data + */ + fun clonePczt() = Pczt(this.toByteArray().copyOf()) + + // Override to prevent leaking data in logs + override fun toString() = "Pczt(size=${inner.size})" +} 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 657f336ac..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 accountValue: cash.z.ecc.android.sdk.model.Account) : 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 a21eeaa42..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,18 +4,17 @@ 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 { /** * Given a seed and a number of accounts, return the associated Unified Full Viewing Keys. * * @param seed the seed from which to derive viewing keys. - * @param numberOfAccounts the number of accounts to use. Multiple accounts are not fully - * supported so the default value of 1 is recommended. + * @param numberOfAccounts the number of accounts to use. * * @return the UFVKs derived from the seed, encoded as Strings. */ @@ -45,28 +44,28 @@ interface DerivationTool { * the returned spending key in a secure fashion. * * @param seed the seed from which to derive spending keys. - * @param account the account to derive. + * @param accountIndex the ZIP 32 account index to derive. * * @return the unified spending key for the account. */ suspend fun deriveUnifiedSpendingKey( seed: ByteArray, network: ZcashNetwork, - account: Account + accountIndex: 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 /** @@ -108,13 +107,15 @@ interface DerivationTool { * * @param contextString a globally-unique non-empty sequence of at most 252 bytes that * identifies the desired context. + * @param seed the seed from which to derive the arbitrary key. + * @param accountIndex the ZIP 32 account index for which to derive the arbitrary key. * @return an array of 32 bytes. */ suspend fun deriveArbitraryAccountKey( contextString: ByteArray, seed: ByteArray, network: ZcashNetwork, - account: Account + accountIndex: 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..5feb163c3 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() { + fun uuid_wrong_length() { assertFailsWith(IllegalArgumentException::class) { - Account(-1) + AccountFixture.new(accountUuid = UUID.fromString("random")) } } }