diff --git a/CHANGELOG.md b/CHANGELOG.md index 054924b40..9322b928f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,8 +12,11 @@ and this library adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `Synchronizer.walletBalances: StateFlow?>` that is replacement for the removed `orchardBalances`, `saplingBalances`, and `transparentBalance` - `getTransactions(accountUuid: AccountUuid)` to get transactions belonging to the given account -- `Zip32AccountIndex`, `AccountUuid`, `AccountUsk`, `AccountPurpose`, `AccountCreateSetup`, and `AcountImportSetup` - model classes have been added to support the new or the changed APIs +- `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 diff --git a/backend-lib/Cargo.lock b/backend-lib/Cargo.lock index a0e977306..d62290434 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" @@ -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" @@ -625,6 +640,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 +1135,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" @@ -1130,7 +1163,7 @@ dependencies = [ [[package]] name = "equihash" version = "0.2.0" -source = "git+https://github.com/zcash/librustzcash?rev=c2ebc05be118a972352801a328e6c61f69bb8a16#c2ebc05be118a972352801a328e6c61f69bb8a16" +source = "git+https://github.com/zcash/librustzcash.git?rev=205d4c930319b7b6d24aeb4efde69e9b4d1b6f7b#205d4c930319b7b6d24aeb4efde69e9b4d1b6f7b" dependencies = [ "blake2b_simd", "byteorder", @@ -1166,7 +1199,7 @@ dependencies = [ [[package]] name = "f4jumble" version = "0.1.0" -source = "git+https://github.com/zcash/librustzcash?rev=c2ebc05be118a972352801a328e6c61f69bb8a16#c2ebc05be118a972352801a328e6c61f69bb8a16" +source = "git+https://github.com/zcash/librustzcash.git?rev=205d4c930319b7b6d24aeb4efde69e9b4d1b6f7b#205d4c930319b7b6d24aeb4efde69e9b4d1b6f7b" dependencies = [ "blake2b_simd", ] @@ -1429,6 +1462,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" @@ -1493,6 +1538,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" @@ -1523,6 +1577,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" @@ -2248,14 +2316,14 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "orchard" version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f18e997fa121de5c73e95cdc7e8512ae43b7de38904aeea5e5713cc48f3c0ba" +source = "git+https://github.com/zcash/orchard.git?rev=bcd08e1d23e70c42a338f3e3f79d6f4c0c219805#bcd08e1d23e70c42a338f3e3f79d6f4c0c219805" dependencies = [ "aes", "bitvec", "blake2b_simd", "ff", "fpe", + "getset", "group", "halo2_gadgets", "halo2_proofs", @@ -2402,6 +2470,31 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pczt" +version = "0.0.0" +source = "git+https://github.com/zcash/librustzcash.git?rev=205d4c930319b7b6d24aeb4efde69e9b4d1b6f7b#205d4c930319b7b6d24aeb4efde69e9b4d1b6f7b" +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", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -2554,6 +2647,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" @@ -2608,6 +2714,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" @@ -3074,8 +3202,7 @@ dependencies = [ [[package]] name = "sapling-crypto" version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfff8cfce16aeb38da50b8e2ed33c9018f30552beff2210c266662a021b17f38" +source = "git+https://github.com/zcash/sapling-crypto.git?rev=29cff9683cdf2f0c522ff3224081dfb4fbc80248#29cff9683cdf2f0c522ff3224081dfb4fbc80248" dependencies = [ "aes", "bellman", @@ -3087,6 +3214,7 @@ dependencies = [ "document-features", "ff", "fpe", + "getset", "group", "hex", "incrementalmerkletree", @@ -3430,6 +3558,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" @@ -3482,6 +3613,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" @@ -4729,6 +4866,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" dependencies = [ "getrandom", + "serde", ] [[package]] @@ -5215,6 +5353,7 @@ dependencies = [ "log-panics", "orchard", "paranoid-android", + "pczt", "prost", "rayon", "rusqlite", @@ -5229,6 +5368,7 @@ dependencies = [ "zcash_address", "zcash_client_backend", "zcash_client_sqlite", + "zcash_note_encryption", "zcash_primitives", "zcash_proofs", "zip32", @@ -5237,11 +5377,12 @@ dependencies = [ [[package]] name = "zcash_address" version = "0.6.0" -source = "git+https://github.com/zcash/librustzcash?rev=c2ebc05be118a972352801a328e6c61f69bb8a16#c2ebc05be118a972352801a328e6c61f69bb8a16" +source = "git+https://github.com/zcash/librustzcash.git?rev=205d4c930319b7b6d24aeb4efde69e9b4d1b6f7b#205d4c930319b7b6d24aeb4efde69e9b4d1b6f7b" dependencies = [ "bech32", "bs58", "f4jumble", + "serde", "zcash_encoding", "zcash_protocol", ] @@ -5249,7 +5390,7 @@ dependencies = [ [[package]] name = "zcash_client_backend" version = "0.15.0" -source = "git+https://github.com/zcash/librustzcash?rev=c2ebc05be118a972352801a328e6c61f69bb8a16#c2ebc05be118a972352801a328e6c61f69bb8a16" +source = "git+https://github.com/zcash/librustzcash.git?rev=205d4c930319b7b6d24aeb4efde69e9b4d1b6f7b#205d4c930319b7b6d24aeb4efde69e9b4d1b6f7b" dependencies = [ "arti-client", "base64", @@ -5273,7 +5414,9 @@ dependencies = [ "nonempty", "orchard", "pasta_curves", + "pczt", "percent-encoding", + "postcard", "prost", "rand", "rand_core", @@ -5308,7 +5451,7 @@ dependencies = [ [[package]] name = "zcash_client_sqlite" version = "0.13.0" -source = "git+https://github.com/zcash/librustzcash?rev=c2ebc05be118a972352801a328e6c61f69bb8a16#c2ebc05be118a972352801a328e6c61f69bb8a16" +source = "git+https://github.com/zcash/librustzcash.git?rev=205d4c930319b7b6d24aeb4efde69e9b4d1b6f7b#205d4c930319b7b6d24aeb4efde69e9b4d1b6f7b" dependencies = [ "bip32", "bs58", @@ -5327,6 +5470,7 @@ dependencies = [ "schemerz", "schemerz-rusqlite", "secrecy", + "serde", "shardtree", "static_assertions", "subtle", @@ -5345,7 +5489,7 @@ dependencies = [ [[package]] name = "zcash_encoding" version = "0.2.1" -source = "git+https://github.com/zcash/librustzcash?rev=c2ebc05be118a972352801a328e6c61f69bb8a16#c2ebc05be118a972352801a328e6c61f69bb8a16" +source = "git+https://github.com/zcash/librustzcash.git?rev=205d4c930319b7b6d24aeb4efde69e9b4d1b6f7b#205d4c930319b7b6d24aeb4efde69e9b4d1b6f7b" dependencies = [ "byteorder", "nonempty", @@ -5354,7 +5498,7 @@ dependencies = [ [[package]] name = "zcash_keys" version = "0.5.0" -source = "git+https://github.com/zcash/librustzcash?rev=c2ebc05be118a972352801a328e6c61f69bb8a16#c2ebc05be118a972352801a328e6c61f69bb8a16" +source = "git+https://github.com/zcash/librustzcash.git?rev=205d4c930319b7b6d24aeb4efde69e9b4d1b6f7b#205d4c930319b7b6d24aeb4efde69e9b4d1b6f7b" dependencies = [ "bech32", "bip32", @@ -5381,9 +5525,9 @@ dependencies = [ [[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", @@ -5395,7 +5539,7 @@ dependencies = [ [[package]] name = "zcash_primitives" version = "0.20.0" -source = "git+https://github.com/zcash/librustzcash?rev=c2ebc05be118a972352801a328e6c61f69bb8a16#c2ebc05be118a972352801a328e6c61f69bb8a16" +source = "git+https://github.com/zcash/librustzcash.git?rev=205d4c930319b7b6d24aeb4efde69e9b4d1b6f7b#205d4c930319b7b6d24aeb4efde69e9b4d1b6f7b" dependencies = [ "aes", "bip32", @@ -5406,6 +5550,7 @@ dependencies = [ "equihash", "ff", "fpe", + "getset", "group", "hex", "incrementalmerkletree", @@ -5433,7 +5578,7 @@ dependencies = [ [[package]] name = "zcash_proofs" version = "0.20.0" -source = "git+https://github.com/zcash/librustzcash?rev=c2ebc05be118a972352801a328e6c61f69bb8a16#c2ebc05be118a972352801a328e6c61f69bb8a16" +source = "git+https://github.com/zcash/librustzcash.git?rev=205d4c930319b7b6d24aeb4efde69e9b4d1b6f7b#205d4c930319b7b6d24aeb4efde69e9b4d1b6f7b" dependencies = [ "bellman", "blake2b_simd", @@ -5455,7 +5600,7 @@ dependencies = [ [[package]] name = "zcash_protocol" version = "0.4.1" -source = "git+https://github.com/zcash/librustzcash?rev=c2ebc05be118a972352801a328e6c61f69bb8a16#c2ebc05be118a972352801a328e6c61f69bb8a16" +source = "git+https://github.com/zcash/librustzcash.git?rev=205d4c930319b7b6d24aeb4efde69e9b4d1b6f7b#205d4c930319b7b6d24aeb4efde69e9b4d1b6f7b" dependencies = [ "document-features", "memuse", @@ -5526,7 +5671,7 @@ dependencies = [ [[package]] name = "zip321" version = "0.2.0" -source = "git+https://github.com/zcash/librustzcash?rev=c2ebc05be118a972352801a328e6c61f69bb8a16#c2ebc05be118a972352801a328e6c61f69bb8a16" +source = "git+https://github.com/zcash/librustzcash.git?rev=205d4c930319b7b6d24aeb4efde69e9b4d1b6f7b#205d4c930319b7b6d24aeb4efde69e9b4d1b6f7b" dependencies = [ "base64", "nom", diff --git a/backend-lib/Cargo.toml b/backend-lib/Cargo.toml index cb64523fc..d0a6673ec 100644 --- a/backend-lib/Cargo.toml +++ b/backend-lib/Cargo.toml @@ -13,10 +13,12 @@ rust-version = "1.82" [dependencies] # Zcash dependencies orchard = "0.10" +pczt = "0.0" sapling = { package = "sapling-crypto", version = "0.3", 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_client_backend = { version = "0.15", features = ["orchard", "tor", "transparent-inputs", "unstable", "pczt"] } +zcash_client_sqlite = { version = "0.13", features = ["orchard", "transparent-inputs", "unstable", "serde"] } +zcash_note_encryption = "0.4.1" zcash_primitives = "0.20" zcash_proofs = "0.20" zip32 = "0.1.2" @@ -72,8 +74,14 @@ path = "src/main/rust/lib.rs" crate-type = ["staticlib", "cdylib"] [patch.crates-io] -zcash_address = { git = "https://github.com/zcash/librustzcash", rev = "c2ebc05be118a972352801a328e6c61f69bb8a16" } -zcash_client_backend = { git = "https://github.com/zcash/librustzcash", rev = "c2ebc05be118a972352801a328e6c61f69bb8a16" } -zcash_client_sqlite = { git = "https://github.com/zcash/librustzcash", rev = "c2ebc05be118a972352801a328e6c61f69bb8a16" } -zcash_primitives = { git = "https://github.com/zcash/librustzcash", rev = "c2ebc05be118a972352801a328e6c61f69bb8a16" } -zcash_proofs = { git = "https://github.com/zcash/librustzcash", rev = "c2ebc05be118a972352801a328e6c61f69bb8a16" } +pczt = { git = "https://github.com/zcash/librustzcash.git", rev = "205d4c930319b7b6d24aeb4efde69e9b4d1b6f7b" } +zcash_address = { git = "https://github.com/zcash/librustzcash.git", rev = "205d4c930319b7b6d24aeb4efde69e9b4d1b6f7b" } +zcash_client_backend = { git = "https://github.com/zcash/librustzcash.git", rev = "205d4c930319b7b6d24aeb4efde69e9b4d1b6f7b" } +zcash_client_sqlite = { git = "https://github.com/zcash/librustzcash.git", rev = "205d4c930319b7b6d24aeb4efde69e9b4d1b6f7b" } +zcash_encoding = { git = "https://github.com/zcash/librustzcash.git", rev = "205d4c930319b7b6d24aeb4efde69e9b4d1b6f7b" } +zcash_keys = { git = "https://github.com/zcash/librustzcash.git", rev = "205d4c930319b7b6d24aeb4efde69e9b4d1b6f7b" } +zcash_primitives = { git = "https://github.com/zcash/librustzcash.git", rev = "205d4c930319b7b6d24aeb4efde69e9b4d1b6f7b" } +zcash_proofs = { git = "https://github.com/zcash/librustzcash.git", rev = "205d4c930319b7b6d24aeb4efde69e9b4d1b6f7b" } +zcash_protocol = { git = "https://github.com/zcash/librustzcash.git", rev = "205d4c930319b7b6d24aeb4efde69e9b4d1b6f7b" } +orchard = { git = "https://github.com/zcash/orchard.git", rev = "bcd08e1d23e70c42a338f3e3f79d6f4c0c219805" } +sapling-crypto = { git = "https://github.com/zcash/sapling-crypto.git", rev = "29cff9683cdf2f0c522ff3224081dfb4fbc80248" } 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 e30ad36e3..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 @@ -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,6 +117,12 @@ 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 */ 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 491a95967..41dea40c2 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 @@ -89,6 +89,16 @@ 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?, @@ -418,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, @@ -533,6 +578,13 @@ class RustBackend private constructor( recoverUntil: Long, ): JniAccountUsk + @JvmStatic + private external fun getAccountForUfvk( + dbDataPath: String, + networkId: Int, + ufvk: String, + ): JniAccount? + @JvmStatic @Suppress("LongParameterList") private external fun importAccountUfvk( @@ -757,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/rust/lib.rs b/backend-lib/src/main/rust/lib.rs index b015ced61..705c54296 100644 --- a/backend-lib/src/main/rust/lib.rs +++ b/backend-lib/src/main/rust/lib.rs @@ -12,6 +12,10 @@ 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; @@ -32,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, 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}, @@ -83,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!"); @@ -284,7 +285,9 @@ fn encode_account<'a, P: Parameters>( }; let seed_fingerprint = match account.source().key_derivation() { - Some(d) => env.byte_array_from_slice(&d.seed_fingerprint().to_bytes()[..])?.into(), + Some(d) => env + .byte_array_from_slice(&d.seed_fingerprint().to_bytes()[..])? + .into(), None => JObject::null(), }; @@ -303,7 +306,7 @@ fn encode_account<'a, P: Parameters>( hd_account_index, (&key_source).into(), (&seed_fingerprint).into(), - (&ufvk).into() + (&ufvk).into(), ], ) } @@ -341,6 +344,40 @@ 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_uuid: AccountUuid, @@ -518,18 +555,23 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_importAcc let account_name = java_string_to_rust(env, &account_name); let key_source = java_nullable_string_to_rust(env, &key_source); - let seed_fingerprint = - <[u8; 32]>::try_from(&env.convert_byte_array(seed_fingerprint_bytes)?[..]) - .ok() - .map(SeedFingerprint::from_bytes); - let hd_account_index = zip32::AccountId::try_from(hd_account_index_raw).ok(); - - let derivation = seed_fingerprint - .zip(hd_account_index) - .map(|(seed_fp, idx)| Zip32Derivation::new(seed_fp, idx)); - let purpose = match purpose { - 0 => Ok(AccountPurpose::Spending { derivation }), + 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(AccountPurpose::Spending { derivation }) + } 1 => Ok(AccountPurpose::ViewOnly), _ => Err(anyhow!( "Account purpose must be either 0 (Spending) or 1 (ViewOnly)" @@ -1285,7 +1327,7 @@ 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, @@ -1962,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>, @@ -2082,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 @@ -2119,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) 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 5a7d91cd0..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,6 +2,7 @@ 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 @@ -132,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" @@ -154,7 +160,7 @@ class TestnetIntegrationTest : ScopedTest() { context = context, birthday = BlockHeight.new(BIRTHDAY_HEIGHT), lightWalletEndpoint = lightWalletEndpoint, - setup = AccountCreateSetupFixture.new(), + setup = AccountCreateSetupFixture.new(seed = seed), // Using existing wallet init mode as simplification for the test walletInitMode = WalletInitMode.ExistingWallet, zcashNetwork = ZcashNetwork.Testnet, 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 116bc0c5c..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 @@ -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,6 +153,10 @@ 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?, 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 b7404130e..927c1a291 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 @@ -51,6 +51,7 @@ 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 @@ -93,6 +94,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 @@ -857,6 +859,22 @@ class SdkSynchronizer private constructor( } } + override suspend fun createPcztFromProposal( + accountUuid: AccountUuid, + proposal: Proposal + ) = txManager.createPcztFromProposal(accountUuid, proposal) + + override suspend fun addProofsToPczt(pczt: Pczt) = txManager.addProofsToPczt(pczt) + + 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( account: Account, since: BlockHeight 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 46d7fb9e3..386ec6854 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,6 +6,7 @@ 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.ext.ZcashSdk import cash.z.ecc.android.sdk.internal.FastestServerFetcher import cash.z.ecc.android.sdk.internal.Files @@ -22,6 +23,7 @@ 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 @@ -301,6 +303,54 @@ interface Synchronizer { usk: UnifiedSpendingKey ): Flow + /** + * 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. + * + * @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 + + /** + * 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 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 fa90e0880..cd0f93dda 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 @@ -327,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/internal/TypesafeBackend.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/TypesafeBackend.kt index 0b00839ce..1e67808d8 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 @@ -17,7 +17,9 @@ 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 @@ -42,6 +44,8 @@ internal interface TypesafeBackend { treeState: TreeState, ): Account + suspend fun getAccountForUfvk(ufvk: UnifiedFullViewingKey): Account? + suspend fun proposeTransferFromUri( account: Account, uri: String @@ -67,6 +71,43 @@ internal interface TypesafeBackend { usk: UnifiedSpendingKey ): 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( + 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.GetCurrentAddressException::class) suspend fun getCurrentAddress(account: Account): String 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 5f2d1e2ec..a5082c6a4 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 @@ -19,7 +19,9 @@ 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 @@ -84,6 +86,10 @@ internal class TypesafeBackendImpl(private val backend: Backend) : TypesafeBacke ) } + 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 @@ -137,6 +143,30 @@ 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.accountUuid.value) 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 e2f72c836..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,6 +2,8 @@ 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.TransactionSubmitResult import cash.z.ecc.android.sdk.model.UnifiedSpendingKey @@ -93,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 7b6156e27..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,6 +4,8 @@ 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.TransactionSubmitResult import cash.z.ecc.android.sdk.model.UnifiedSpendingKey @@ -103,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 cf2297315..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,11 +3,14 @@ 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.UnifiedSpendingKey import cash.z.ecc.android.sdk.model.Zatoshi +@Suppress("TooManyFunctions") internal interface TransactionEncoder { /** * Creates a proposal for transferring from a valid ZIP-321 Payment URI string @@ -91,6 +94,18 @@ 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. * 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 898472c62..ef974811b 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,5 +1,6 @@ package cash.z.ecc.android.sdk.internal.transaction +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 @@ -8,7 +9,9 @@ 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.Pczt import cash.z.ecc.android.sdk.model.Proposal import cash.z.ecc.android.sdk.model.UnifiedSpendingKey import cash.z.ecc.android.sdk.model.Zatoshi @@ -22,6 +25,7 @@ import cash.z.ecc.android.sdk.model.Zatoshi * @property repository the repository that stores information about the transactions being created * such as the raw bytes and raw txId. */ +@Suppress("TooManyFunctions") internal class TransactionEncoderImpl( private val backend: TypesafeBackend, private val saplingParamTool: SaplingParamTool, @@ -143,6 +147,60 @@ 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 { + 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. * 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..e27294675 --- /dev/null +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/Pczt.kt @@ -0,0 +1,15 @@ +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 + } + + // Override to prevent leaking data in logs + override fun toString() = "Pczt(size=${inner.size})" +}