diff --git a/CHANGELOG.md b/CHANGELOG.md index 36a126b3..875341da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,15 +6,32 @@ and this library adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [2.0.7] - 2024-03-08 + +### Fixed +- `Synchronizer.sendToAddress` and `Synchronizer.shieldFunds` now throw an + exception if the created transaction successfully reaches `lightwalletd` but + fails to reach its backing full node's mempool. + ### Changed - `WalletBalance` now contains new fields `changePending` and `valuePending`. Fields `total` and `pending` are still provided. See more in the class documentation `sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/WalletBalance.kt` - `Synchronizer.transparentBalances: WalletBalance` to `Synchronizer.transparentBalance: Zatoshi` -- `WalletSnapshot.transparentBalance: WalletBalance` to `WalletSnapshot.transparentBalance: Zatoshi` +- `WalletSnapshot.transparentBalance: WalletBalance` to `WalletSnapshot.transparentBalance: Zatoshi` - `Memo.MAX_MEMO_LENGTH_BYTES` is now available in public API +- `Synchronizer.sendToAddress` and `Synchronizer.shieldFunds` have been + deprecated, and will be removed in 2.1.0 (which will create multiple + transactions at once for some recipients). ### Added +- APIs that enable constructing a proposal for transferring or shielding funds, + and then creating transactions from a proposal. The intermediate proposal can + be used to determine the required fee, before committing to producing + transactions. + - `Synchronizer.proposeTransfer` + - `Synchronizer.proposeShielding` + - `Synchronizer.createProposedTransactions` - `WalletBalanceFixture` class with mock values that are supposed to be used only for testing purposes - `Memo.countLength(memoString: String)` to count memo length in bytes - `PersistableWallet.toSafeString` is a safe alternative for the regular [toString] function that prints only diff --git a/backend-lib/Cargo.lock b/backend-lib/Cargo.lock index 0d35da5a..21acf33f 100644 --- a/backend-lib/Cargo.lock +++ b/backend-lib/Cargo.lock @@ -29,9 +29,9 @@ dependencies = [ [[package]] name = "aes" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher", @@ -40,9 +40,9 @@ dependencies = [ [[package]] name = "ahash" -version = "0.8.7" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77c3a9648d43b9cd48db467b3f87fdd6e146bcc88ab0180006cef2179fe11d01" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", "once_cell", @@ -67,9 +67,9 @@ checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" [[package]] name = "anyhow" -version = "1.0.79" +version = "1.0.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" +checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1" [[package]] name = "arrayref" @@ -163,12 +163,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - [[package]] name = "bitflags" version = "2.4.2" @@ -243,9 +237,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.14.0" +version = "3.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" +checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" [[package]] name = "byteorder" @@ -270,12 +264,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.83" +version = "1.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" -dependencies = [ - "libc", -] +checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" [[package]] name = "cesu8" @@ -351,9 +342,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.11" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "176dc175b78f56c0f321911d9c8eb2b77a78a4860b9c19db83835fea1a46649b" +checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95" dependencies = [ "crossbeam-utils", ] @@ -448,7 +439,7 @@ checksum = "f2b99bf03862d7f545ebc28ddd33a665b50865f4dfd84031a393823879bd4c54" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.52", ] [[package]] @@ -462,14 +453,15 @@ dependencies = [ [[package]] name = "either" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" [[package]] name = "equihash" version = "0.2.0" -source = "git+https://github.com/zcash/librustzcash.git?rev=9b5abe1a386f2c2c625d8f3fb3b6e9ce9aff641a#9b5abe1a386f2c2c625d8f3fb3b6e9ce9aff641a" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab579d7cf78477773b03e80bc2f89702ef02d7112c711d54ca93dcdce68533d5" dependencies = [ "blake2b_simd", "byteorder", @@ -494,7 +486,8 @@ dependencies = [ [[package]] name = "f4jumble" version = "0.1.0" -source = "git+https://github.com/zcash/librustzcash.git?rev=9b5abe1a386f2c2c625d8f3fb3b6e9ce9aff641a#9b5abe1a386f2c2c625d8f3fb3b6e9ce9aff641a" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a83e8d7fd0c526af4aad893b7c9fe41e2699ed8a776a6c74aecdeafe05afc75" dependencies = [ "blake2b_simd", ] @@ -707,9 +700,9 @@ checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" -version = "0.3.4" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d3d0e0f38255e7fa3cf31335b3a56f05febd18025f4db5ef7a0cfb4f8da651f" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "hex" @@ -746,9 +739,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.1" +version = "2.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "433de089bd45971eecf4668ee0ee8f4cec17db4f8bd8f7bc3197a6ce37aa7d9b" +checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4" dependencies = [ "equivalent", "hashbrown", @@ -802,9 +795,9 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "js-sys" -version = "0.3.67" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a1d36f1235bc969acba30b7f5990b864423a6068a10f7c90ae8f0112e3a59d1" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ "wasm-bindgen", ] @@ -843,9 +836,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.152" +version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "libm" @@ -878,9 +871,9 @@ checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" [[package]] name = "log" -version = "0.4.20" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "log-panics" @@ -924,9 +917,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" dependencies = [ "adler", ] @@ -984,20 +977,25 @@ dependencies = [ ] [[package]] -name = "num-integer" -version = "0.1.45" +name = "num-conv" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" dependencies = [ - "autocfg", "num-traits", ] [[package]] name = "num-traits" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" dependencies = [ "autocfg", ] @@ -1029,15 +1027,15 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "opaque-debug" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "orchard" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92c801aeaccd19bb6916d71f25694b62d223061872900e8022221c1ad8dcad2d" +checksum = "1fb255c3ffdccd3c84fe9ebed72aef64fdc72e6a3e4180dd411002d47abaad42" dependencies = [ "aes", "bitvec", @@ -1152,9 +1150,9 @@ checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" [[package]] name = "pkg-config" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2900ede94e305130c13ddd391e0ab7cbaeb783945ae07a279c268cb05109c6cb" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "poly1305" @@ -1186,7 +1184,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a41cf62165e97c7f814d2221421dbb9afcbcdb0a88068e5ea206e19951c2cbb5" dependencies = [ "proc-macro2", - "syn 2.0.48", + "syn 2.0.52", ] [[package]] @@ -1225,7 +1223,7 @@ dependencies = [ "prost", "prost-types", "regex", - "syn 2.0.48", + "syn 2.0.52", "tempfile", "which", ] @@ -1240,7 +1238,7 @@ dependencies = [ "itertools", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.52", ] [[package]] @@ -1299,9 +1297,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7237101a77a10773db45d62004a272517633fbcc3df19d96455ede1122e051" +checksum = "e4963ed1bc86e4f3ee217022bd855b297cef07fb9eac5dfa1f788b220b49b3bd" dependencies = [ "either", "rayon-core", @@ -1348,15 +1346,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "redox_syscall" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" -dependencies = [ - "bitflags 1.3.2", -] - [[package]] name = "regex" version = "1.10.3" @@ -1371,9 +1360,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" +checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" dependencies = [ "aho-corasick", "memchr", @@ -1416,7 +1405,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "549b9d036d571d42e6e85d1c1425e2ac83491075078ca9a15be021c56b1641f2" dependencies = [ - "bitflags 2.4.2", + "bitflags", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -1433,11 +1422,11 @@ checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustix" -version = "0.38.30" +version = "0.38.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca" +checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" dependencies = [ - "bitflags 2.4.2", + "bitflags", "errno", "libc", "linux-raw-sys", @@ -1455,9 +1444,9 @@ dependencies = [ [[package]] name = "sapling-crypto" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f5de898a7cdb7f6d9c8fb888341b6ae6e2aeae88227b7f435f1dda49ecf9e62" +checksum = "d183012062dfdde85f7e3e758328fcf6e9846d8dd3fce35b04d0efcb6677b0e0" dependencies = [ "aes", "bellman", @@ -1537,22 +1526,22 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.196" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.196" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.52", ] [[package]] @@ -1581,7 +1570,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbf20c7a2747d9083092e3a3eeb9a7ed75577ae364896bebbc5e0bdcd4e97735" dependencies = [ - "bitflags 2.4.2", + "bitflags", "either", "incrementalmerkletree", "tracing", @@ -1624,9 +1613,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.48" +version = "2.0.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" dependencies = [ "proc-macro2", "quote", @@ -1653,42 +1642,41 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.9.0" +version = "3.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", "fastrand", - "redox_syscall", "rustix", "windows-sys 0.52.0", ] [[package]] name = "thiserror" -version = "1.0.56" +version = "1.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" +checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.56" +version = "1.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" +checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.52", ] [[package]] name = "thread_local" -version = "1.1.7" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" dependencies = [ "cfg-if", "once_cell", @@ -1696,12 +1684,13 @@ dependencies = [ [[package]] name = "time" -version = "0.3.31" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e" +checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" dependencies = [ "deranged", "itoa", + "num-conv", "powerfmt", "serde", "time-core", @@ -1716,10 +1705,11 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26197e33420244aeb70c3e8c78376ca46571bc4e701e4791c2cd9f57dcb3a43f" +checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" dependencies = [ + "num-conv", "time-core", ] @@ -1748,7 +1738,7 @@ dependencies = [ "proc-macro2", "prost-build", "quote", - "syn 2.0.48", + "syn 2.0.52", ] [[package]] @@ -1770,7 +1760,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.52", ] [[package]] @@ -1834,9 +1824,9 @@ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" dependencies = [ "tinyvec", ] @@ -1889,9 +1879,9 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "walkdir" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", @@ -1905,9 +1895,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.90" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1223296a201415c7fad14792dbefaace9bd52b62d33453ade1c5b5f07555406" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -1915,24 +1905,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.90" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcdc935b63408d58a32f8cc9738a0bffd8f05cc7c002086c6ef20b7312ad9dcd" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.52", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.90" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e4c238561b2d428924c49815533a8b9121c664599558a5d9ec51f8a1740a999" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1940,28 +1930,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.90" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.52", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.90" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "web-sys" -version = "0.3.67" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58cd2333b6e0be7a39605f0e255892fd7418a682d8da8fe042fe25128794d2ed" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" dependencies = [ "js-sys", "wasm-bindgen", @@ -2025,7 +2015,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.0", + "windows-targets 0.52.4", ] [[package]] @@ -2045,17 +2035,17 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" dependencies = [ - "windows_aarch64_gnullvm 0.52.0", - "windows_aarch64_msvc 0.52.0", - "windows_i686_gnu 0.52.0", - "windows_i686_msvc 0.52.0", - "windows_x86_64_gnu 0.52.0", - "windows_x86_64_gnullvm 0.52.0", - "windows_x86_64_msvc 0.52.0", + "windows_aarch64_gnullvm 0.52.4", + "windows_aarch64_msvc 0.52.4", + "windows_i686_gnu 0.52.4", + "windows_i686_msvc 0.52.4", + "windows_x86_64_gnu 0.52.4", + "windows_x86_64_gnullvm 0.52.4", + "windows_x86_64_msvc 0.52.4", ] [[package]] @@ -2066,9 +2056,9 @@ checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" +checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" [[package]] name = "windows_aarch64_msvc" @@ -2078,9 +2068,9 @@ checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_aarch64_msvc" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" +checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" [[package]] name = "windows_i686_gnu" @@ -2090,9 +2080,9 @@ checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] name = "windows_i686_gnu" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" +checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" [[package]] name = "windows_i686_msvc" @@ -2102,9 +2092,9 @@ checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] name = "windows_i686_msvc" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" +checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" [[package]] name = "windows_x86_64_gnu" @@ -2114,9 +2104,9 @@ checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" [[package]] name = "windows_x86_64_gnu" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" +checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" [[package]] name = "windows_x86_64_gnullvm" @@ -2126,9 +2116,9 @@ checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" +checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" [[package]] name = "windows_x86_64_msvc" @@ -2138,9 +2128,9 @@ checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] name = "windows_x86_64_msvc" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" +checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" [[package]] name = "wyz" @@ -2169,7 +2159,6 @@ dependencies = [ "jni", "libc", "log-panics", - "orchard", "paranoid-android", "prost", "rayon", @@ -2189,19 +2178,22 @@ dependencies = [ [[package]] name = "zcash_address" -version = "0.3.1" -source = "git+https://github.com/zcash/librustzcash.git?rev=9b5abe1a386f2c2c625d8f3fb3b6e9ce9aff641a#9b5abe1a386f2c2c625d8f3fb3b6e9ce9aff641a" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "827c17a1f7e3a69f0d44e991ff610c7a842228afdc9dc2325ffdd1a67fee01e9" dependencies = [ "bech32", "bs58", "f4jumble", "zcash_encoding", + "zcash_protocol", ] [[package]] name = "zcash_client_backend" -version = "0.10.0" -source = "git+https://github.com/zcash/librustzcash.git?rev=9b5abe1a386f2c2c625d8f3fb3b6e9ce9aff641a#9b5abe1a386f2c2c625d8f3fb3b6e9ce9aff641a" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185913267d824529b9547c933674963fca2b5bd84ad377a59d0f8ab6159ce798" dependencies = [ "base64", "bech32", @@ -2239,8 +2231,9 @@ dependencies = [ [[package]] name = "zcash_client_sqlite" -version = "0.8.1" -source = "git+https://github.com/zcash/librustzcash.git?rev=9b5abe1a386f2c2c625d8f3fb3b6e9ce9aff641a#9b5abe1a386f2c2c625d8f3fb3b6e9ce9aff641a" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e883405989b8d7275a0e1180000b7568bb3fa33e36b4806c174eb802678e2dbf" dependencies = [ "bs58", "byteorder", @@ -2250,7 +2243,6 @@ dependencies = [ "incrementalmerkletree", "jubjub", "maybe-rayon", - "orchard", "prost", "rusqlite", "sapling-crypto", @@ -2271,7 +2263,8 @@ dependencies = [ [[package]] name = "zcash_encoding" version = "0.2.0" -source = "git+https://github.com/zcash/librustzcash.git?rev=9b5abe1a386f2c2c625d8f3fb3b6e9ce9aff641a#9b5abe1a386f2c2c625d8f3fb3b6e9ce9aff641a" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f03391b81727875efa6ac0661a20883022b6fba92365dc121c48fa9b00c5aac0" dependencies = [ "byteorder", "nonempty", @@ -2279,8 +2272,9 @@ dependencies = [ [[package]] name = "zcash_keys" -version = "0.0.0" -source = "git+https://github.com/zcash/librustzcash.git?rev=9b5abe1a386f2c2c625d8f3fb3b6e9ce9aff641a#9b5abe1a386f2c2c625d8f3fb3b6e9ce9aff641a" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f22d3407fdd6992b49f037f23862ab376be6013be6f2d0bc85948a635edc1f5" dependencies = [ "bech32", "bls12_381", @@ -2317,8 +2311,9 @@ dependencies = [ [[package]] name = "zcash_primitives" -version = "0.13.0" -source = "git+https://github.com/zcash/librustzcash.git?rev=9b5abe1a386f2c2c625d8f3fb3b6e9ce9aff641a#9b5abe1a386f2c2c625d8f3fb3b6e9ce9aff641a" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9070e084570bb78aed4f8d71fd6254492e62c87a5d01e084183980e98117092d" dependencies = [ "aes", "bip0039", @@ -2354,8 +2349,9 @@ dependencies = [ [[package]] name = "zcash_proofs" -version = "0.13.0" -source = "git+https://github.com/zcash/librustzcash.git?rev=9b5abe1a386f2c2c625d8f3fb3b6e9ce9aff641a#9b5abe1a386f2c2c625d8f3fb3b6e9ce9aff641a" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a02eb1f151d9b9a6e16408d2c55ff440bd2fb232b7377277146d0fa2df9bc8" dependencies = [ "bellman", "blake2b_simd", @@ -2374,6 +2370,16 @@ dependencies = [ "zcash_primitives", ] +[[package]] +name = "zcash_protocol" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c651f9f95d319cd1e5211178108dcd3aa73063806d09af54b799e1a329a575bf" +dependencies = [ + "document-features", + "memuse", +] + [[package]] name = "zcash_spec" version = "0.1.0" @@ -2400,7 +2406,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.52", ] [[package]] @@ -2420,7 +2426,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.52", ] [[package]] diff --git a/backend-lib/Cargo.toml b/backend-lib/Cargo.toml index 8885ab59..ca135271 100644 --- a/backend-lib/Cargo.toml +++ b/backend-lib/Cargo.toml @@ -23,11 +23,10 @@ schemer = "0.2" secp256k1 = "0.26" secrecy = "0.8" zcash_address = "0.3" -zcash_client_backend = { version = "0.10", features = ["transparent-inputs", "unstable"] } -zcash_client_sqlite = { version = "^0.8.1", features = ["transparent-inputs", "unstable"] } -zcash_primitives = "0.13" -zcash_proofs = "0.13" -orchard = { version = "0.7", default-features = false } +zcash_client_backend = { version = "0.11", features = ["transparent-inputs", "unstable"] } +zcash_client_sqlite = { version = "0.9", features = ["transparent-inputs", "unstable"] } +zcash_primitives = "0.14" +zcash_proofs = "0.14" # Initialization rayon = "1.7" @@ -62,11 +61,3 @@ libc = "0.2" name = "zcashwalletsdk" path = "src/main/rust/lib.rs" crate-type = ["staticlib", "cdylib"] - -[patch.crates-io] -# Tag `ecc_sdk-20240130a` -zcash_address = { git = "https://github.com/zcash/librustzcash.git", rev = "9b5abe1a386f2c2c625d8f3fb3b6e9ce9aff641a" } -zcash_client_backend = { git = "https://github.com/zcash/librustzcash.git", rev = "9b5abe1a386f2c2c625d8f3fb3b6e9ce9aff641a" } -zcash_client_sqlite = { git = "https://github.com/zcash/librustzcash.git", rev = "9b5abe1a386f2c2c625d8f3fb3b6e9ce9aff641a" } -zcash_primitives = { git = "https://github.com/zcash/librustzcash.git", rev = "9b5abe1a386f2c2c625d8f3fb3b6e9ce9aff641a" } -zcash_proofs = { git = "https://github.com/zcash/librustzcash.git", rev = "9b5abe1a386f2c2c625d8f3fb3b6e9ce9aff641a" } 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 eb68e94f..0fef4da1 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 @@ -30,13 +30,15 @@ interface Backend { suspend fun proposeShielding( account: Int, - memo: ByteArray? = byteArrayOf() - ): ProposalUnsafe + shieldingThreshold: Long, + memo: ByteArray? = byteArrayOf(), + transparentReceiver: String? = null + ): ProposalUnsafe? - suspend fun createProposedTransaction( + suspend fun createProposedTransactions( proposal: ProposalUnsafe, unifiedSpendingKey: ByteArray - ): ByteArray + ): List suspend fun decryptAndStoreTransaction(tx: ByteArray) 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 5cc71682..82f5f0bc 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 @@ -307,34 +307,40 @@ class RustBackend private constructor( override suspend fun proposeShielding( account: Int, - memo: ByteArray? - ): ProposalUnsafe { + shieldingThreshold: Long, + memo: ByteArray?, + transparentReceiver: String? + ): ProposalUnsafe? { return withContext(SdkDispatchers.DATABASE_IO) { - ProposalUnsafe.parse( - proposeShielding( - dataDbFile.absolutePath, - account, - memo ?: ByteArray(0), - networkId = networkId, - useZip317Fees = IS_USE_ZIP_317_FEES + proposeShielding( + dataDbFile.absolutePath, + account, + shieldingThreshold, + memo ?: ByteArray(0), + transparentReceiver, + networkId = networkId, + useZip317Fees = IS_USE_ZIP_317_FEES + )?.let { + ProposalUnsafe.parse( + it ) - ) + } } } - override suspend fun createProposedTransaction( + override suspend fun createProposedTransactions( proposal: ProposalUnsafe, unifiedSpendingKey: ByteArray - ): ByteArray = + ): List = withContext(SdkDispatchers.DATABASE_IO) { - createProposedTransaction( + createProposedTransactions( dataDbFile.absolutePath, proposal.toByteArray(), unifiedSpendingKey, spendParamsPath = saplingSpendFile.absolutePath, outputParamsPath = saplingOutputFile.absolutePath, networkId = networkId - ) + ).asList() } override suspend fun putUtxo( @@ -584,21 +590,23 @@ class RustBackend private constructor( private external fun proposeShielding( dbDataPath: String, account: Int, + shieldingThreshold: Long, memo: ByteArray, + transparentReceiver: String?, networkId: Int, useZip317Fees: Boolean - ): ByteArray + ): ByteArray? @JvmStatic @Suppress("LongParameterList") - private external fun createProposedTransaction( + private external fun createProposedTransactions( dbDataPath: String, proposal: ByteArray, usk: ByteArray, spendParamsPath: String, outputParamsPath: String, networkId: Int - ): ByteArray + ): Array @JvmStatic private external fun branchIdForHeight( diff --git a/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/ProposalUnsafe.kt b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/ProposalUnsafe.kt index f9af1fa6..87d2d0c6 100644 --- a/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/ProposalUnsafe.kt +++ b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/ProposalUnsafe.kt @@ -38,9 +38,21 @@ class ProposalUnsafe( } /** - * Returns the fee required by this proposal. + * Returns the number of transactions that this proposal will create. + * + * This is equal to the number of `TransactionSubmitResult`s that will be returned + * from `Synchronizer.createProposedTransactions`. + * + * Proposals always create at least one transaction. */ - fun feeRequired(): Long { - return inner.balance.feeRequired + fun transactionCount(): Int { + return inner.stepsCount + } + + /** + * Returns the total fee required by this proposal for its transactions. + */ + fun totalFeeRequired(): Long { + return inner.stepsList.fold(0) { acc, step -> acc + step.balance.feeRequired } } } diff --git a/backend-lib/src/main/proto/proposal.proto b/backend-lib/src/main/proto/proposal.proto index 84a8aed4..4084efb2 100644 --- a/backend-lib/src/main/proto/proposal.proto +++ b/backend-lib/src/main/proto/proposal.proto @@ -6,12 +6,29 @@ syntax = "proto3"; package cash.z.wallet.sdk.ffi; option java_package = "cash.z.wallet.sdk.internal.ffi"; +// A data structure that describes a series of transactions to be created. +message Proposal { + // The version of this serialization format. + uint32 protoVersion = 1; + // The fee rule used in constructing this proposal + FeeRule feeRule = 2; + // The target height for which the proposal was constructed + // + // The chain must contain at least this many blocks in order for the proposal to + // be executed. + uint32 minTargetHeight = 3; + // The series of transactions to be created. + repeated ProposalStep steps = 4; +} + // A data structure that describes the inputs to be consumed and outputs to // be produced in a proposed transaction. -message Proposal { - uint32 protoVersion = 1; +message ProposalStep { // ZIP 321 serialized transaction request - string transactionRequest = 2; + string transactionRequest = 1; + // The vector of selected payment index / output pool mappings. Payment index + // 0 corresponds to the payment with no explicit index. + repeated PaymentOutputPool paymentOutputPools = 2; // The anchor height to be used in creating the transaction, if any. // Setting the anchor height to zero will disallow the use of any shielded // inputs. @@ -21,16 +38,9 @@ message Proposal { // The total value, fee value, and change outputs of the proposed // transaction TransactionBalance balance = 5; - // The fee rule used in constructing this proposal - FeeRule feeRule = 6; - // The target height for which the proposal was constructed - // - // The chain must contain at least this many blocks in order for the proposal to - // be executed. - uint32 minTargetHeight = 7; - // A flag indicating whether the proposal is for a shielding transaction, + // A flag indicating whether the step is for a shielding transaction, // used for determining which OVK to select for wallet-internal outputs. - bool isShielding = 8; + bool isShielding = 6; } enum ValuePool { @@ -47,14 +57,45 @@ enum ValuePool { Orchard = 3; } -// The unique identifier and value for each proposed input. -message ProposedInput { +// A mapping from ZIP 321 payment index to the output pool that has been chosen +// for that payment, based upon the payment address and the selected inputs to +// the transaction. +message PaymentOutputPool { + uint32 paymentIndex = 1; + ValuePool valuePool = 2; +} + +// The unique identifier and value for each proposed input that does not +// require a back-reference to a prior step of the proposal. +message ReceivedOutput { bytes txid = 1; ValuePool valuePool = 2; uint32 index = 3; uint64 value = 4; } +// A reference a payment in a prior step of the proposal. This payment must +// belong to the wallet. +message PriorStepOutput { + uint32 stepIndex = 1; + uint32 paymentIndex = 2; +} + +// A reference a change output from a prior step of the proposal. +message PriorStepChange { + uint32 stepIndex = 1; + uint32 changeIndex = 2; +} + +// The unique identifier and value for an input to be used in the transaction. +message ProposedInput { + oneof value { + ReceivedOutput receivedOutput = 1; + PriorStepOutput priorStepOutput = 2; + PriorStepChange priorStepChange = 3; + } +} + // The fee rule used in constructing a Proposal enum FeeRule { // Protobuf requires that enums have a zero discriminant as the default @@ -72,15 +113,21 @@ enum FeeRule { // The proposed change outputs and fee value. message TransactionBalance { + // A list of change output values. repeated ChangeValue proposedChange = 1; + // The fee to be paid by the proposed transaction, in zatoshis. uint64 feeRequired = 2; } // A proposed change output. If the transparent value pool is selected, // the `memo` field must be null. message ChangeValue { + // The value of a change output to be created, in zatoshis. uint64 value = 1; + // The value pool in which the change output should be created. ValuePool valuePool = 2; + // The optional memo that should be associated with the newly created change output. + // Memos must not be present for transparent change outputs. MemoBytes memo = 3; } diff --git a/backend-lib/src/main/rust/lib.rs b/backend-lib/src/main/rust/lib.rs index 98c53545..02861c03 100644 --- a/backend-lib/src/main/rust/lib.rs +++ b/backend-lib/src/main/rust/lib.rs @@ -24,7 +24,7 @@ use zcash_client_backend::{ chain::{scan_cached_blocks, CommitmentTreeRoot, ScanSummary}, scanning::{ScanPriority, ScanRange}, wallet::{ - create_proposed_transaction, decrypt_and_store_transaction, + create_proposed_transactions, decrypt_and_store_transaction, input_selection::GreedyInputSelector, propose_shielding, propose_transfer, }, AccountBalance, AccountBirthday, InputSource, WalletCommitmentTrees, WalletRead, @@ -524,10 +524,10 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_getTransp if let Some(taddr) = ua.0.transparent() { let taddr = match taddr { - TransparentAddress::PublicKey(data) => { + TransparentAddress::PublicKeyHash(data) => { ZcashAddress::from_transparent_p2pkh(network, *data) } - TransparentAddress::Script(data) => { + TransparentAddress::ScriptHash(data) => { ZcashAddress::from_transparent_p2sh(network, *data) } }; @@ -1145,7 +1145,7 @@ fn encode_account_balance<'a>( /// pending. fn encode_wallet_summary<'a>( env: &mut JNIEnv<'a>, - summary: WalletSummary, + summary: WalletSummary, ) -> jni::errors::Result> { let account_balances = utils::rust_vec_to_java( env, @@ -1397,7 +1397,7 @@ fn zip317_helper( StandardFeeRule::PreZip313 }; GreedyInputSelector::new( - SingleOutputChangeStrategy::new(fee_rule, change_memo), + SingleOutputChangeStrategy::new(fee_rule, change_memo, ShieldedProtocol::Sapling), DustOutputPolicy::default(), ) } @@ -1463,13 +1463,13 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_proposeTr ) .map_err(|e| format_err!("Error creating transaction proposal: {}", e))?; - utils::rust_bytes_to_java( + Ok(utils::rust_bytes_to_java( &env, Proposal::from_standard_proposal(&network, &proposal) .encode_to_vec() .as_ref(), - ) - .map(|arr| arr.into_raw()) + )? + .into_raw()) }); unwrap_exc_or(&mut env, res, ptr::null_mut()) } @@ -1480,7 +1480,9 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_proposeSh _: JClass<'local>, db_data: JString<'local>, account: jint, + shielding_threshold: jlong, memo: JByteArray<'local>, + transparent_receiver: JString<'local>, network_id: jint, use_zip317_fees: jboolean, ) -> jbyteArray { @@ -1489,12 +1491,40 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_proposeSh let network = parse_network(network_id as u32)?; let mut db_data = wallet_db(env, network, db_data)?; let account = account_id_from_jint(account)?; + let shielding_threshold = NonNegativeAmount::from_nonnegative_i64(shielding_threshold) + .map_err(|()| format_err!("Invalid shielding threshold, out of range"))?; let memo_bytes = env.convert_byte_array(memo).unwrap(); + let transparent_receiver = + match utils::java_nullable_string_to_rust(env, &transparent_receiver) { + None => Ok(None), + Some(addr) => match Address::decode(&network, &addr) { + None => Err(format_err!("Transparent receiver is for the wrong network")), + Some(addr) => match addr { + Address::Sapling(_) | Address::Unified(_) => Err(format_err!( + "Transparent receiver is not a transparent address" + )), + Address::Transparent(addr) => { + if db_data + .get_transparent_receivers(account)? + .contains_key(&addr) + { + Ok(Some(addr)) + } else { + Err(format_err!( + "Transparent receiver does not belong to account {}", + u32::from(account), + )) + } + } + }, + }, + }?; + let min_confirmations = 0; let min_confirmations_for_heights = NonZeroU32::new(1).unwrap(); - let from_addrs: Vec = db_data + let account_receivers = db_data .get_target_and_anchor_heights(min_confirmations_for_heights) .map_err(|e| format_err!("Error while fetching anchor height: {}", e)) .and_then(|opt_anchor| { @@ -1512,16 +1542,28 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_proposeSh e ) }) - })? - .into_keys() - .collect(); + })?; + + let from_addrs = if let Some((addr, _)) = transparent_receiver.map_or_else(|| + if account_receivers.len() > 1 { + Err(format_err!( + "Account has more than one transparent receiver with funds to shield; this is not yet supported by the SDK. Provide a specific transparent receiver to shield funds from." + )) + } else { + Ok(account_receivers.iter().next().map(|(a, v)| (*a, *v))) + }, + |addr| Ok(account_receivers.get(&addr).map(|value| (addr, *value))) + )?.filter(|(_, value)| *value >= shielding_threshold.into()) { + [addr] + } else { + // There are no transparent funds to shield; don't create a proposal. + return Ok(ptr::null_mut()); + }; let memo = Memo::from_bytes(&memo_bytes).unwrap(); let input_selector = zip317_helper(Some(MemoBytes::from(&memo)), use_zip317_fees); - let shielding_threshold = NonNegativeAmount::from_u64(100000).unwrap(); - let proposal = propose_shielding::<_, _, _, Infallible>( &mut db_data, &network, @@ -1532,19 +1574,19 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_proposeSh ) .map_err(|e| format_err!("Error while shielding transaction: {}", e))?; - utils::rust_bytes_to_java( + Ok(utils::rust_bytes_to_java( &env, Proposal::from_standard_proposal(&network, &proposal) .encode_to_vec() .as_ref(), - ) - .map(|arr| arr.into_raw()) + )? + .into_raw()) }); unwrap_exc_or(&mut env, res, ptr::null_mut()) } #[no_mangle] -pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_createProposedTransaction< +pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_createProposedTransactions< 'local, >( mut env: JNIEnv<'local>, @@ -1555,7 +1597,7 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_createPro spend_params: JString<'local>, output_params: JString<'local>, network_id: jint, -) -> jbyteArray { +) -> jobjectArray { let res = catch_unwind(&mut env, |env| { let _span = tracing::info_span!("RustBackend.createProposedTransaction").entered(); let network = parse_network(network_id as u32)?; @@ -1570,7 +1612,7 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_createPro .map_err(|e| format_err!("Invalid proposal: {}", e))? .try_into_standard_proposal(&network, &db_data)?; - let txid = create_proposed_transaction::<_, _, Infallible, _, _>( + let txids = create_proposed_transactions::<_, _, Infallible, _, _>( &mut db_data, &network, &prover, @@ -1579,9 +1621,16 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_createPro OvkPolicy::Sender, &proposal, ) - .map_err(|e| format_err!("Error while creating transaction: {}", e))?; + .map_err(|e| format_err!("Error while creating transactions: {}", e))?; - utils::rust_bytes_to_java(&env, txid.as_ref()).map(|arr| arr.into_raw()) + Ok(utils::rust_vec_to_java( + env, + txids.into(), + "[B", + |env, txid| utils::rust_bytes_to_java(env, txid.as_ref()), + |env| env.new_byte_array(32), + )? + .into_raw()) }); unwrap_exc_or(&mut env, res, ptr::null_mut()) } @@ -1657,10 +1706,10 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_listTrans .iter() .map(|(taddr, _)| { let taddr = match taddr { - TransparentAddress::PublicKey(data) => { + TransparentAddress::PublicKeyHash(data) => { ZcashAddress::from_transparent_p2pkh(zcash_network, *data) } - TransparentAddress::Script(data) => { + TransparentAddress::ScriptHash(data) => { ZcashAddress::from_transparent_p2sh(zcash_network, *data) } }; diff --git a/backend-lib/src/main/rust/utils.rs b/backend-lib/src/main/rust/utils.rs index da7b7aac..e1867926 100644 --- a/backend-lib/src/main/rust/utils.rs +++ b/backend-lib/src/main/rust/utils.rs @@ -38,10 +38,11 @@ pub(crate) fn java_string_to_rust(env: &mut JNIEnv, jstring: &JString) -> String .into() } -pub(crate) fn rust_bytes_to_java<'a>( - env: &JNIEnv<'a>, - data: &[u8], -) -> Result, failure::Error> { +pub(crate) fn java_nullable_string_to_rust(env: &mut JNIEnv, jstring: &JString) -> Option { + (!jstring.is_null()).then(|| java_string_to_rust(env, jstring)) +} + +pub(crate) fn rust_bytes_to_java<'a>(env: &JNIEnv<'a>, data: &[u8]) -> JNIResult> { // SAFETY: jbyte (i8) has the same size and alignment as u8, and a well-defined // twos-complement representation with no "trap representations". let buf = unsafe { slice::from_raw_parts(data.as_ptr().cast(), data.len()) }; 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 d51fbf62..2bbd69aa 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 @@ -105,7 +105,15 @@ class TestWallet( memo: String = "", amount: Zatoshi = Zatoshi(500L) ): TestWallet { - synchronizer.sendToAddress(shieldedSpendingKey, amount, address, memo) + synchronizer.createProposedTransactions( + synchronizer.proposeTransfer( + shieldedSpendingKey.account, + address, + amount, + memo + ), + shieldedSpendingKey + ) return this } @@ -121,7 +129,12 @@ class TestWallet( synchronizer.getTransparentBalance(transparentAddress).let { walletBalance -> if (walletBalance.value > 0L) { - synchronizer.shieldFunds(shieldedSpendingKey) + synchronizer.proposeShielding(shieldedSpendingKey.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 3b68a48a..c5562420 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 @@ -189,7 +189,15 @@ class SampleCodeTest { ZcashNetwork.Mainnet, Account.DEFAULT ) - synchronizer.sendToAddress(spendingKey, amount, address, memo) + synchronizer.createProposedTransactions( + synchronizer.proposeTransfer( + spendingKey.account, + address, + amount, + memo + ), + spendingKey + ) } // ///////////////////////////////////////////////////// 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 0a7e7453..d221e9ad 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 @@ -47,6 +47,7 @@ class GetBalanceFragment : BaseDemoFragment() { reportTraceEvent(SyncBlockchainBenchmarkTrace.Event.BALANCE_SCREEN_END) } + @Suppress("MagicNumber") override fun onViewCreated( view: View, savedInstanceState: Bundle? @@ -60,13 +61,20 @@ class GetBalanceFragment : BaseDemoFragment() { binding.shield.apply { setOnClickListener { lifecycleScope.launch { - sharedViewModel.synchronizerFlow.value?.shieldFunds( + val usk = DerivationTool.getInstance().deriveUnifiedSpendingKey( seed, network, Account.DEFAULT ) - ) + sharedViewModel.synchronizerFlow.value?.let { synchronizer -> + synchronizer.proposeShielding(usk.account, Zatoshi(100000))?.let { it1 -> + synchronizer.createProposedTransactions( + it1, + usk + ) + } + } } } } 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 a9d8bc8b..170cff3c 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 @@ -145,12 +145,17 @@ class SendFragment : BaseDemoFragment() { val amount = amountInput.text.toString().toDouble().convertZecToZatoshi() val toAddress = addressInput.text.toString().trim() lifecycleScope.launch { - sharedViewModel.synchronizerFlow.value?.sendToAddress( - spendingKey, - amount, - toAddress, - "Funds from Demo App" - ) + sharedViewModel.synchronizerFlow.value?.let { synchronizer -> + synchronizer.createProposedTransactions( + synchronizer.proposeTransfer( + spendingKey.account, + toAddress, + amount, + "Funds from Demo App" + ), + spendingKey + ) + } } mainActivity()?.hideKeyboard() 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 94d23bb0..df66cb47 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 @@ -22,6 +22,7 @@ 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.PersistableWallet +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 @@ -47,6 +48,7 @@ import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.toList import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -202,7 +204,7 @@ class WalletViewModel(application: Application) : AndroidViewModel(application) viewModelScope.launch { val spendingKey = spendingKey.filterNotNull().first() runCatching { synchronizer.send(spendingKey, zecSend) } - .onSuccess { mutableSendState.value = SendState.Sent(it) } + .onSuccess { mutableSendState.value = SendState.Sent(it.toList()) } .onFailure { mutableSendState.value = SendState.Error(it) } } } else { @@ -215,6 +217,7 @@ class WalletViewModel(application: Application) : AndroidViewModel(application) * * Observe the result via [sendState]. */ + @Suppress("MagicNumber") fun shieldFunds() { if (sendState.value is SendState.Sending) { return @@ -226,8 +229,15 @@ class WalletViewModel(application: Application) : AndroidViewModel(application) if (null != synchronizer) { viewModelScope.launch { val spendingKey = spendingKey.filterNotNull().first() - kotlin.runCatching { synchronizer.shieldFunds(spendingKey) } - .onSuccess { mutableSendState.value = SendState.Sent(it) } + kotlin.runCatching { + synchronizer.proposeShielding(spendingKey.account, Zatoshi(100000))?.let { + synchronizer.createProposedTransactions( + it, + spendingKey + ) + } + } + .onSuccess { it?.let { mutableSendState.value = SendState.Sent(it.toList()) } } .onFailure { mutableSendState.value = SendState.Error(it) } } } else { @@ -302,7 +312,7 @@ sealed class SendState { override fun toString(): String = "Sending" } - class Sent(val localTxId: Long) : SendState() { + class Sent(val txIds: List) : SendState() { override fun toString(): String = "Sent" } diff --git a/gradle.properties b/gradle.properties index d2ce73bd..5d8f6f47 100644 --- a/gradle.properties +++ b/gradle.properties @@ -26,7 +26,7 @@ ZCASH_ASCII_GPG_KEY= # Configures whether release is an unstable snapshot, therefore published to the snapshot repository. IS_SNAPSHOT=true -LIBRARY_VERSION=2.0.6 +LIBRARY_VERSION=2.0.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/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 dcf61e6c..2acb6767 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 @@ -9,9 +9,12 @@ data class ZecSend(val destination: WalletAddress, val amount: Zatoshi, val memo suspend fun Synchronizer.send( spendingKey: UnifiedSpendingKey, send: ZecSend -) = sendToAddress( - spendingKey, - send.amount, - send.destination.address, - send.memo.value +) = createProposedTransactions( + proposeTransfer( + spendingKey.account, + send.destination.address, + send.amount, + send.memo.value + ), + spendingKey ) 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 76fe8b05..98638f66 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 @@ -112,11 +112,14 @@ class TestnetIntegrationTest : ScopedTest() { Account.DEFAULT ) log("sending to address") - synchronizer.sendToAddress( - spendingKey, - ZcashSdk.MINERS_FEE, - toAddress, - "first mainnet tx from the SDK" + synchronizer.createProposedTransactions( + synchronizer.proposeTransfer( + spendingKey.account, + toAddress, + ZcashSdk.MINERS_FEE, + "first mainnet tx from the SDK" + ), + spendingKey ) return true } 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 c7c866f0..1edf516a 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 @@ -106,7 +106,15 @@ class TestWallet( memo: String = "", amount: Zatoshi = Zatoshi(500L) ): TestWallet { - synchronizer.sendToAddress(spendingKey, amount, address, memo) + synchronizer.createProposedTransactions( + synchronizer.proposeTransfer( + spendingKey.account, + address, + amount, + memo + ), + spendingKey + ) return this } @@ -124,7 +132,12 @@ class TestWallet( Twig.debug { "FOUND utxo balance of total: $walletBalance" } if (walletBalance.value > 0L) { - synchronizer.shieldFunds(spendingKey) + synchronizer.proposeShielding(spendingKey.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 631c8d98..e5f80350 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 @@ -88,15 +88,17 @@ internal class FakeRustBackend( override suspend fun proposeShielding( account: Int, - memo: ByteArray? - ): ProposalUnsafe { + shieldingThreshold: Long, + memo: ByteArray?, + transparentReceiver: String? + ): ProposalUnsafe? { TODO("Not yet implemented") } - override suspend fun createProposedTransaction( + override suspend fun createProposedTransactions( proposal: ProposalUnsafe, unifiedSpendingKey: ByteArray - ): ByteArray { + ): List { TODO("Not yet implemented") } 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 8670dc8c..1da81a75 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/SdkSynchronizer.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/SdkSynchronizer.kt @@ -42,8 +42,10 @@ import cash.z.ecc.android.sdk.internal.transaction.TransactionEncoderImpl 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.Proposal 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.Zatoshi import cash.z.ecc.android.sdk.model.ZcashNetwork @@ -66,6 +68,7 @@ import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn @@ -405,6 +408,7 @@ class SdkSynchronizer private constructor( lastScanTime = now SYNCED } + is Stopped -> STOPPED is Disconnected -> DISCONNECTED is Syncing, Initialized -> SYNCING @@ -555,6 +559,58 @@ class SdkSynchronizer private constructor( account ) + @Throws(TransactionEncoderException::class) + override suspend fun proposeTransfer( + account: Account, + recipient: String, + amount: Zatoshi, + memo: String + ): Proposal = txManager.proposeTransfer(account, recipient, amount, memo) + + @Throws(TransactionEncoderException::class) + override suspend fun proposeShielding( + account: Account, + shieldingThreshold: Zatoshi, + memo: String, + transparentReceiver: String? + ): Proposal? = txManager.proposeShielding(account, shieldingThreshold, memo, transparentReceiver) + + @Throws(TransactionEncoderException::class) + override suspend fun createProposedTransactions( + proposal: Proposal, + usk: UnifiedSpendingKey + ): Flow { + // Internally, this logic submits and checks every incoming transaction, and once [Failure] or + // [NotAttempted] submission result occurs, it returns [NotAttempted] for the rest of them + var anySubmissionFailed = false + return txManager.createProposedTransactions(proposal, usk) + .asFlow() + .map { transaction -> + if (anySubmissionFailed) { + TransactionSubmitResult.NotAttempted(transaction.txId) + } else { + val submission = txManager.submit(transaction) + when (submission) { + is TransactionSubmitResult.Success -> { + // Expected state + } + is TransactionSubmitResult.Failure, + is TransactionSubmitResult.NotAttempted -> { + anySubmissionFailed = true + } + } + submission + } + } + } + + @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, TransactionSubmitException::class) override suspend fun sendToAddress( usk: UnifiedSpendingKey, @@ -571,13 +627,23 @@ class SdkSynchronizer private constructor( usk.account ) - if (txManager.submit(encodedTx)) { - return storage.findMatchingTransactionId(encodedTx.txId.byteArray)!! - } else { - throw TransactionSubmitException() + when (txManager.submit(encodedTx)) { + is TransactionSubmitResult.Success -> { + return storage.findMatchingTransactionId(encodedTx.txId.byteArray)!! + } + else -> { + throw TransactionSubmitException() + } } } + @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, TransactionSubmitException::class) override suspend fun shieldFunds( usk: UnifiedSpendingKey, @@ -596,10 +662,13 @@ class SdkSynchronizer private constructor( usk.account ) - if (txManager.submit(encodedTx)) { - return storage.findMatchingTransactionId(encodedTx.txId.byteArray)!! - } else { - throw TransactionSubmitException() + when (txManager.submit(encodedTx)) { + is TransactionSubmitResult.Success -> { + return storage.findMatchingTransactionId(encodedTx.txId.byteArray)!! + } + else -> { + throw TransactionSubmitException() + } } } 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 71018141..4bc740f7 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 @@ -15,8 +15,10 @@ 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.BlockHeight import cash.z.ecc.android.sdk.model.PercentDecimal +import cash.z.ecc.android.sdk.model.Proposal 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 @@ -167,6 +169,64 @@ interface Synchronizer { */ suspend fun getTransparentAddress(account: Account): String + /** + * Creates a proposal for transferring funds to the given recipient. + * + * @param account the account from which to transfer funds. + * @param recipient the recipient's address. + * @param amount the amount of zatoshi to send. + * @param memo the optional memo to include as part of the proposal's transactions. + * + * @return the proposal or an exception + */ + suspend fun proposeTransfer( + account: Account, + recipient: String, + amount: Zatoshi, + memo: String = "" + ): Proposal + + /** + * Creates a proposal for shielding any transparent funds received by the given account. + * + * @param account the account for which to shield funds. + * @param shieldingThreshold the minimum transparent balance required before a + * proposal will be created. + * @param memo the optional memo to include as part of the proposal's transactions. + * @param transparentReceiver a specific transparent receiver within the account that + * should be the source of transparent funds. Default is + * null which will select whichever of the account's + * transparent receivers has funds to shield. + * + * @return the proposal, or null if the transparent balance that would be shielded is + * zero or below `shieldingThreshold`. + * + * @throws Exception if `transparentReceiver` is null and there are transparent funds + * in more than one of the account's transparent receivers. + */ + suspend fun proposeShielding( + account: Account, + shieldingThreshold: Zatoshi, + memo: String = ZcashSdk.DEFAULT_SHIELD_FUNDS_MEMO_PREFIX, + transparentReceiver: String? = null + ): Proposal? + + /** + * Creates the transactions in the given proposal. + * + * @param proposal the proposal for which to create transactions. + * @param usk the unified spending key associated with the account for which the + * proposal was created. + * + * @return a flow of result objects for the transactions that were created as part of + * the proposal, indicating whether they were submitted to the network or if + * an error occurred. + */ + suspend fun createProposedTransactions( + proposal: Proposal, + usk: UnifiedSpendingKey + ): Flow + /** * Sends zatoshi. * @@ -180,6 +240,13 @@ interface Synchronizer { * 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, @@ -187,6 +254,13 @@ interface Synchronizer { memo: String = "" ): Long + @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 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 3c6a7fdd..42861be8 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 @@ -26,21 +26,23 @@ internal interface TypesafeBackend { @Suppress("LongParameterList") suspend fun proposeTransfer( - usk: UnifiedSpendingKey, + account: Account, to: String, value: Long, memo: ByteArray? = byteArrayOf() ): Proposal suspend fun proposeShielding( - usk: UnifiedSpendingKey, - memo: ByteArray? = byteArrayOf() - ): Proposal + account: Account, + shieldingThreshold: Long, + memo: ByteArray? = byteArrayOf(), + transparentReceiver: String? = null + ): Proposal? - suspend fun createProposedTransaction( + suspend fun createProposedTransactions( proposal: Proposal, usk: UnifiedSpendingKey - ): FirstClassByteArray + ): List 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 1dbad3d4..1eb811f3 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 @@ -37,14 +37,14 @@ internal class TypesafeBackendImpl(private val backend: Backend) : TypesafeBacke @Suppress("LongParameterList") override suspend fun proposeTransfer( - usk: UnifiedSpendingKey, + account: Account, to: String, value: Long, memo: ByteArray? ): Proposal = Proposal.fromUnsafe( backend.proposeTransfer( - usk.account.value, + account.value, to, value, memo @@ -52,26 +52,30 @@ internal class TypesafeBackendImpl(private val backend: Backend) : TypesafeBacke ) override suspend fun proposeShielding( - usk: UnifiedSpendingKey, - memo: ByteArray? - ): Proposal = - Proposal.fromUnsafe( - backend.proposeShielding( - usk.account.value, - memo + account: Account, + shieldingThreshold: Long, + memo: ByteArray?, + transparentReceiver: String? + ): Proposal? = + backend.proposeShielding( + account.value, + shieldingThreshold, + memo, + transparentReceiver + )?.let { + Proposal.fromUnsafe( + it ) - ) + } - override suspend fun createProposedTransaction( + override suspend fun createProposedTransactions( proposal: Proposal, usk: UnifiedSpendingKey - ): FirstClassByteArray = - FirstClassByteArray( - backend.createProposedTransaction( - proposal.toUnsafe(), - usk.copyBytes() - ) - ) + ): List = + backend.createProposedTransactions( + proposal.toUnsafe(), + usk.copyBytes() + ).map { FirstClassByteArray(it) } override suspend fun getCurrentAddress(account: Account): String { return backend.getCurrentAddress(account.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 4671d457..fcc9b3e2 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,7 +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.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 @@ -33,6 +35,62 @@ internal interface OutboundTransactionManager { account: Account ): EncodedTransaction + /** + * Creates a proposal for transferring funds to the given recipient. + * + * @param account the account from which to transfer funds. + * @param recipient the recipient's address. + * @param amount the amount of zatoshi to send. + * @param memo the optional memo to include as part of the proposal's transactions. + * + * @return the proposal or an exception + */ + suspend fun proposeTransfer( + account: Account, + recipient: String, + amount: Zatoshi, + memo: String + ): Proposal + + /** + * Creates a proposal for shielding any transparent funds received by the given account. + * + * @param account the account for which to shield funds. + * @param shieldingThreshold the minimum transparent balance required before a + * proposal will be created. + * @param memo the optional memo to include as part of the proposal's transactions. + * @param transparentReceiver a specific transparent receiver within the account that + * should be the source of transparent funds. Default is + * null which will select whichever of the account's + * transparent receivers has funds to shield. + * + * @return the proposal, or null if the transparent balance that would be shielded is + * zero or below `shieldingThreshold`. + * + * @throws Exception if `transparentReceiver` is null and there are transparent funds + * in more than one of the account's transparent receivers. + */ + suspend fun proposeShielding( + account: Account, + shieldingThreshold: Zatoshi, + memo: String, + transparentReceiver: String? + ): Proposal? + + /** + * Creates the transactions in the given proposal. + * + * @param proposal the proposal for which to create transactions. + * @param usk the unified spending key associated with the account for which the + * proposal was created. + * + * @return the successfully encoded transactions or an exception + */ + suspend fun createProposedTransactions( + proposal: Proposal, + usk: UnifiedSpendingKey + ): List + /** * Submits the transaction represented by [encodedTransaction] to lightwalletd to broadcast to the * network and, hopefully, include in the next block. @@ -41,7 +99,7 @@ internal interface OutboundTransactionManager { * to lightwalletd. * @return true if the transaction was successfully submitted to lightwalletd. */ - suspend fun submit(encodedTransaction: EncodedTransaction): Boolean + suspend fun submit(encodedTransaction: EncodedTransaction): TransactionSubmitResult /** * 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 0eadef75..77404e56 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 @@ -1,9 +1,12 @@ package cash.z.ecc.android.sdk.internal.transaction 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.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 import co.electriccoin.lightwallet.client.LightWalletClient @@ -40,20 +43,60 @@ internal class OutboundTransactionManagerImpl( } } - override suspend fun submit(encodedTransaction: EncodedTransaction): Boolean { + override suspend fun proposeTransfer( + account: Account, + recipient: String, + amount: Zatoshi, + memo: String + ): Proposal = encoder.proposeTransfer(account, recipient, amount, memo.toByteArray()) + + override suspend fun proposeShielding( + account: Account, + shieldingThreshold: Zatoshi, + memo: String, + transparentReceiver: String? + ): Proposal? = encoder.proposeShielding(account, shieldingThreshold, memo.toByteArray(), transparentReceiver) + + override suspend fun createProposedTransactions( + proposal: Proposal, + usk: UnifiedSpendingKey + ): List = encoder.createProposedTransactions(proposal, usk) + + override suspend fun submit(encodedTransaction: EncodedTransaction): TransactionSubmitResult { return when (val response = service.submitTransaction(encodedTransaction.raw.byteArray)) { is Response.Success -> { - Twig.debug { "SUCCESS: submit transaction completed with response: ${response.result}" } - true + if (response.result.code == 0) { + Twig.info { + "SUCCESS: submit transaction completed for:" + + " ${encodedTransaction.txId.byteArray.toHexReversed()}" + } + TransactionSubmitResult.Success(encodedTransaction.txId) + } else { + Twig.error { + "FAILURE! submit transaction ${encodedTransaction.txId.byteArray.toHexReversed()} " + + "completed with response: ${response.result.code}: ${response.result.message}" + } + TransactionSubmitResult.Failure( + encodedTransaction.txId, + false, + response.result.code, + response.result.message + ) + } } is Response.Failure -> { - Twig.debug { - "FAILURE! submit transaction completed with response: ${response.code}: ${ + Twig.error { + "FAILURE! submit transaction failed with gRPC response: ${response.code}: ${ response.description }" } - false + TransactionSubmitResult.Failure( + encodedTransaction.txId, + true, + response.code, + response.description + ) } } } 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 4a5383e9..c0aa8cea 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 @@ -1,7 +1,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.BlockHeight +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 @@ -38,6 +40,61 @@ internal interface TransactionEncoder { memo: ByteArray? = byteArrayOf() ): EncodedTransaction + /** + * Creates a proposal for transferring funds to the given recipient. + * + * @param account the account from which to transfer funds. + * @param recipient the recipient's address. + * @param amount the amount of zatoshi to send. + * @param memo the optional memo to include as part of the proposal's transactions. + * + * @return the proposal or an exception + */ + suspend fun proposeTransfer( + account: Account, + recipient: String, + amount: Zatoshi, + memo: ByteArray? = byteArrayOf() + ): Proposal + + /** + * Creates a proposal for shielding any transparent funds sent to the given account. + * + * @param account the account for which to shield funds. + * @param shieldingThreshold the minimum transparent balance required before a + * proposal will be created. + * @param memo the optional memo to include as part of the proposal's transactions. + * @param transparentReceiver a specific transparent receiver within the account that + * should be the source of transparent funds. Default is + * null which will select whichever of the account's + * transparent receivers has funds to shield. + * + * @return the proposal, or null if the transparent balance that would be shielded is + * zero or below `shieldingThreshold`. + * + * @throws Exception if `transparentReceiver` is null and there are transparent funds + * in more than one of the account's transparent receivers. + */ + suspend fun proposeShielding( + account: Account, + shieldingThreshold: Zatoshi, + memo: ByteArray? = byteArrayOf(), + transparentReceiver: String? = null + ): Proposal? + + /** + * Creates the transactions in the given proposal. + * + * @param proposal the proposal to create. + * @param usk the unified spending key associated with the notes that will be spent. + * + * @return the successfully encoded transactions or an exception + */ + suspend fun createProposedTransactions( + proposal: Proposal, + usk: UnifiedSpendingKey + ): List + /** * 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. 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 652112ed..33efc2b1 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.SdkException import cash.z.ecc.android.sdk.exception.TransactionEncoderException import cash.z.ecc.android.sdk.ext.masked import cash.z.ecc.android.sdk.internal.SaplingParamTool @@ -7,8 +8,10 @@ import cash.z.ecc.android.sdk.internal.Twig 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.BlockHeight import cash.z.ecc.android.sdk.model.FirstClassByteArray +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 @@ -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, @@ -64,6 +68,77 @@ internal class TransactionEncoderImpl( ?: throw TransactionEncoderException.TransactionNotFoundException(transactionId) } + override suspend fun proposeTransfer( + account: Account, + recipient: String, + amount: Zatoshi, + memo: ByteArray? + ): Proposal { + Twig.debug { + "creating proposal to spend $amount zatoshi to" + + " ${recipient.masked()} with memo: ${memo?.decodeToString()}" + } + + return runCatching { + backend.proposeTransfer( + account, + recipient, + amount.value, + memo + ) + }.onFailure { + Twig.error(it) { "Caught exception while creating proposal." } + }.onSuccess { result -> + Twig.debug { "result of proposeTransfer: $result" } + }.getOrThrow() + } + + override suspend fun proposeShielding( + account: Account, + shieldingThreshold: Zatoshi, + memo: ByteArray?, + transparentReceiver: String? + ): Proposal? { + return runCatching { + backend.proposeShielding(account, shieldingThreshold.value, memo, transparentReceiver) + }.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) { "proposeShielding failed" } + }.onSuccess { result -> + Twig.debug { "result of proposeShielding: $result" } + }.getOrThrow() + } + + override suspend fun createProposedTransactions( + proposal: Proposal, + usk: UnifiedSpendingKey + ): List { + Twig.debug { + "creating transactions for proposal" + } + + val transactionIds = + runCatching { + saplingParamTool.ensureParams(saplingParamTool.properties.paramsDirectory) + Twig.debug { "params exist! attempting to send..." } + backend.createProposedTransactions(proposal, usk) + }.onFailure { + Twig.error(it) { "Caught exception while creating transaction." } + }.onSuccess { result -> + Twig.debug { "result of createProposedTransactions: $result" } + }.getOrThrow() + + val txs = + transactionIds.map { transactionId -> + repository.findEncodedTransactionByTxId(transactionId) + ?: throw TransactionEncoderException.TransactionNotFoundException(transactionId) + } + + return txs + } + /** * 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. @@ -133,48 +208,53 @@ internal class TransactionEncoderImpl( " ${toAddress.masked()} with memo: ${memo?.decodeToString()}" } - @Suppress("TooGenericExceptionCaught") - return try { + return runCatching { saplingParamTool.ensureParams(saplingParamTool.properties.paramsDirectory) Twig.debug { "params exist! attempting to send..." } - // TODO [#1359]: Expose the proposal in a way that enables querying its fee. - // TODO [#1359]: https://github.com/Electric-Coin-Company/zcash-android-wallet-sdk/issues/1359 val proposal = backend.proposeTransfer( - usk, + usk.account, toAddress, amount.value, memo ) - backend.createProposedTransaction(proposal, usk) - } catch (t: Throwable) { - Twig.debug(t) { "Caught exception while creating transaction." } - throw t - }.also { result -> + 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? = byteArrayOf() ): FirstClassByteArray { - @Suppress("TooGenericExceptionCaught") - return try { + return runCatching { saplingParamTool.ensureParams(saplingParamTool.properties.paramsDirectory) Twig.debug { "params exist! attempting to shield..." } - // TODO [#1359]: Expose the proposal in a way that enables querying its fee. - // TODO [#1359]: https://github.com/Electric-Coin-Company/zcash-android-wallet-sdk/issues/1359 - val proposal = backend.proposeShielding(usk, memo) - backend.createProposedTransaction(proposal, usk) - } catch (t: Throwable) { + 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.debug(t) { "Shield failed" } - throw t - }.also { result -> + 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/Proposal.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/Proposal.kt index fc068f9b..0906dafb 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/Proposal.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/Proposal.kt @@ -20,7 +20,7 @@ class Proposal( // Check for type errors eagerly, to ensure that the caller won't // encounter these errors later. - typed.feeRequired() + typed.totalFeeRequired() return typed } @@ -34,9 +34,21 @@ class Proposal( } /** - * Returns the fee required by this proposal. + * Returns the number of transactions that this proposal will create. + * + * This is equal to the number of `TransactionSubmitResult`s that will be returned + * from `Synchronizer.createProposedTransactions`. + * + * Proposals always create at least one transaction. */ - fun feeRequired(): Zatoshi { - return Zatoshi(inner.feeRequired()) + fun transactionCount(): Int { + return inner.transactionCount() + } + + /** + * Returns the total fee required by this proposal for its transactions. + */ + fun totalFeeRequired(): Zatoshi { + return Zatoshi(inner.totalFeeRequired()) } } diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/TransactionSubmitResult.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/TransactionSubmitResult.kt new file mode 100644 index 00000000..231ba48d --- /dev/null +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/TransactionSubmitResult.kt @@ -0,0 +1,32 @@ +package cash.z.ecc.android.sdk.model + +/** + * A result object for a transaction that was created as part of a proposal, indicating + * whether it was submitted to the network or if an error occurred. + */ +sealed class TransactionSubmitResult { + /** + * The transaction was successfully submitted to the mempool. + */ + data class Success(val txId: FirstClassByteArray) : TransactionSubmitResult() + + /** + * An error occurred while submitting the transaction. + * + * If `grpcError` is true, the transaction failed to reach the `lightwalletd` server. + * Otherwise, the transaction reached the `lightwalletd` server but failed to enter + * the mempool. + */ + data class Failure( + val txId: FirstClassByteArray, + val grpcError: Boolean, + val code: Int, + val description: String? + ) : TransactionSubmitResult() + + /** + * The transaction was created and is in the local wallet, but was not submitted to + * the network. + */ + data class NotAttempted(val txId: FirstClassByteArray) : TransactionSubmitResult() +}