diff --git a/.github/workflows/cargo-checkmate.yaml b/.github/workflows/cargo-checkmate.yaml index 41db0da864..59b1fca0a3 100644 --- a/.github/workflows/cargo-checkmate.yaml +++ b/.github/workflows/cargo-checkmate.yaml @@ -28,7 +28,7 @@ jobs: tool: cargo-checkmate - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install protoc run: sudo apt-get install protobuf-compiler diff --git a/.github/workflows/ci-nightly.yaml b/.github/workflows/ci-nightly.yaml index c8fccdce08..8a89519837 100644 --- a/.github/workflows/ci-nightly.yaml +++ b/.github/workflows/ci-nightly.yaml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Reject trailing whitespace run: ./utils/trailing-whitespace.sh reject @@ -28,26 +28,41 @@ jobs: create-cache-key: uses: zingolabs/zingo-mobile/.github/workflows/create-cache-key.yaml@dev - build-android: + android-build: strategy: matrix: arch: [ x86_64, x86, arm64-v8a, armeabi-v7a ] fail-fast: false - uses: zingolabs/zingo-mobile/.github/workflows/build.yaml@dev + uses: zingolabs/zingo-mobile/.github/workflows/android-build.yaml@dev needs: create-cache-key with: cache-key: ${{ needs.create-cache-key.outputs.cache-key }} arch: ${{ matrix.arch }} - integration-test-android: + android-ubuntu-integration-test-actions: strategy: matrix: abi: [ x86_64, x86, arm64-v8a, armeabi-v7a ] fail-fast: false - uses: zingolabs/zingo-mobile/.github/workflows/integration-test.yaml@dev - needs: [ create-timestamp, create-cache-key, build-android ] + uses: zingolabs/zingo-mobile/.github/workflows/android-ubuntu-integration-test-actions.yaml@dev + needs: [create-timestamp, android-build] with: timestamp: ${{ needs.create-timestamp.outputs.timestamp }} cache-key: ${{ needs.create-cache-key.outputs.cache-key }} abi: ${{ matrix.abi }} + api-level: 29 + ios-build: + strategy: + fail-fast: false + uses: zingolabs/zingo-mobile/.github/workflows/ios-build.yaml@dev + needs: create-cache-key + with: + cache-key: ${{ needs.create-cache-key.outputs.cache-key }} + + ios-integration-test: + uses: zingolabs/zingo-mobile/.github/workflows/ios-integration-test.yaml@dev + needs: [ create-timestamp, ios-build ] + with: + timestamp: ${{ needs.create-timestamp.outputs.timestamp }} + cache-key: ${{ needs.create-cache-key.outputs.cache-key }} diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 71f5381539..3732da27aa 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -17,7 +17,7 @@ jobs: if: github.event.pull_request.draft == false steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Reject trailing whitespace run: ./utils/trailing-whitespace.sh reject @@ -30,11 +30,9 @@ jobs: RUSTFLAGS: -D warnings steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - uses: actions-rust-lang/setup-rust-toolchain@v1 - with: - toolchain: stable - name: Install protoc run: sudo apt-get install protobuf-compiler diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml index 042ba03b85..8a5318d2b7 100644 --- a/.github/workflows/coverage.yaml +++ b/.github/workflows/coverage.yaml @@ -16,7 +16,7 @@ jobs: options: --security-opt seccomp=unconfined steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Symlink lightwalletd and zcash binaries run: ln -s /usr/bin/lightwalletd /usr/bin/zcashd /usr/bin/zcash-cli ./libtonode-tests/regtest/bin/ diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 4e7ff0383e..9d2055b8f4 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -19,14 +19,15 @@ jobs: RUSTFLAGS: -D warnings steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - uses: actions-rust-lang/setup-rust-toolchain@v1 - with: - toolchain: stable - name: Install nextest - uses: taiki-e/install-action@nextest + uses: taiki-e/install-action@v2 + with: + tool: nextest + version: 0.9.78 - name: Install protoc run: sudo apt-get install protobuf-compiler @@ -38,7 +39,7 @@ jobs: run: cargo nextest archive --verbose --workspace --all-features --archive-file nextest-archive.tar.zst - name: Upload archive - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: nextest-archive path: nextest-archive.tar.zst @@ -58,7 +59,7 @@ jobs: partition: [1, 2, 3, 4, 5, 6, 7, 8] steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: create binaries dir run: mkdir -p ./test_binaries/bins @@ -70,7 +71,7 @@ jobs: run: ln -s /root/.zcash-params /github/home - name: Download archive - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: nextest-archive diff --git a/Cargo.lock b/Cargo.lock index 848ba28b56..ee48add4e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -185,20 +185,46 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +[[package]] +name = "aws-lc-rs" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f95446d919226d587817a7d21379e6eb099b97b45110a7f272a444ca5c54070" +dependencies = [ + "aws-lc-sys", + "mirai-annotations", + "paste", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234314bd569802ec87011d653d6815c6d7b9ffb969e9fee5b8b20ef860e8dce9" +dependencies = [ + "bindgen", + "cc", + "cmake", + "dunce", + "fs_extra", + "libc", + "paste", +] + [[package]] name = "axum" -version = "0.6.20" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" +checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" dependencies = [ "async-trait", "axum-core", - "bitflags 1.3.2", "bytes 1.6.0", "futures-util", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.29", + "http", + "http-body", + "http-body-util", "itoa", "matchit", "memchr", @@ -207,7 +233,7 @@ dependencies = [ "pin-project-lite", "rustversion", "serde", - "sync_wrapper", + "sync_wrapper 1.0.1", "tower", "tower-layer", "tower-service", @@ -215,17 +241,20 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.3.4" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" +checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" dependencies = [ "async-trait", "bytes 1.6.0", "futures-util", - "http 0.2.12", - "http-body 0.4.6", + "http", + "http-body", + "http-body-util", "mime", + "pin-project-lite", "rustversion", + "sync_wrapper 0.1.2", "tower-layer", "tower-service", ] @@ -281,6 +310,12 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" +[[package]] +name = "bech32" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d" + [[package]] name = "bellman" version = "0.14.0" @@ -302,11 +337,34 @@ dependencies = [ "subtle", ] +[[package]] +name = "bindgen" +version = "0.69.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" +dependencies = [ + "bitflags 2.6.0", + "cexpr", + "clang-sys", + "itertools", + "lazy_static", + "lazycell", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.68", + "which", +] + [[package]] name = "bip0039" -version = "0.10.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef0f0152ec5cf17f49a5866afaa3439816207fd4f0a224c0211ffaf5e278426" +checksum = "e68a5a99c65851e7be249f5cf510c0a136f18c9bca32139576d59bd3f577b043" dependencies = [ "hmac", "pbkdf2", @@ -316,6 +374,22 @@ dependencies = [ "zeroize", ] +[[package]] +name = "bip32" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa13fae8b6255872fd86f7faf4b41168661d7d78609f7bfe6771b85c6739a15b" +dependencies = [ + "bs58", + "hmac", + "rand_core 0.6.4", + "ripemd", + "secp256k1", + "sha2 0.10.8", + "subtle", + "zeroize", +] + [[package]] name = "bit-set" version = "0.5.3" @@ -464,6 +538,20 @@ name = "cc" version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac367972e516d45567c7eafc73d24e1c193dcf200a8d94e9db7b3d38b349572d" +dependencies = [ + "jobserver", + "libc", + "once_cell", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] [[package]] name = "cfg-if" @@ -520,6 +608,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "clap" version = "4.5.8" @@ -558,6 +657,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "cmake" +version = "0.1.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb1e43aa7fd152b1f968787f7dbcdeb306d1867ff373c69955211876c053f91a" +dependencies = [ + "cc", +] + [[package]] name = "colorchoice" version = "1.0.1" @@ -661,9 +769,11 @@ version = "0.1.0" dependencies = [ "futures-util", "hex", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.29", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", "json", "orchard", "portpicker", @@ -678,9 +788,7 @@ dependencies = [ "tower", "zcash_client_backend", "zcash_primitives", - "zingo-testutils", - "zingo-testvectors", - "zingoconfig", + "zingo-netutils", "zingolib", ] @@ -781,6 +889,12 @@ dependencies = [ "litrs", ] +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "either" version = "1.13.0" @@ -817,7 +931,7 @@ dependencies = [ [[package]] name = "equihash" version = "0.2.0" -source = "git+https://github.com/zingolabs/librustzcash.git?tag=always_require_change#ae0d477addf1034fdb5ce9d2dfbdf975e58b714c" +source = "git+https://github.com/zingolabs/librustzcash.git?tag=zcash_client_sqlite-0.11.2_plus_zingolabs_changes-1-g7ad60b5d5-2-g121371a08#121371a089f076a5ee2737809c792d905f5a4b3a" dependencies = [ "blake2b_simd", "byteorder", @@ -852,7 +966,7 @@ dependencies = [ [[package]] name = "f4jumble" version = "0.1.0" -source = "git+https://github.com/zingolabs/librustzcash.git?tag=always_require_change#ae0d477addf1034fdb5ce9d2dfbdf975e58b714c" +source = "git+https://github.com/zingolabs/librustzcash.git?tag=zcash_client_sqlite-0.11.2_plus_zingolabs_changes-1-g7ad60b5d5-2-g121371a08#121371a089f076a5ee2737809c792d905f5a4b3a" dependencies = [ "blake2b_simd", ] @@ -935,6 +1049,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "fuchsia-cprng" version = "0.1.1" @@ -1075,6 +1195,12 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + [[package]] name = "group" version = "0.13.0" @@ -1087,25 +1213,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "h2" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" -dependencies = [ - "bytes 1.6.0", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http 0.2.12", - "indexmap 2.2.6", - "slab", - "tokio", - "tokio-util", - "tracing", -] - [[package]] name = "h2" version = "0.4.5" @@ -1117,7 +1224,7 @@ dependencies = [ "fnv", "futures-core", "futures-sink", - "http 1.1.0", + "http", "indexmap 2.2.6", "slab", "tokio", @@ -1177,19 +1284,6 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -[[package]] -name = "hdwallet" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a03ba7d4c9ea41552cd4351965ff96883e629693ae85005c501bb4b9e1c48a7" -dependencies = [ - "lazy_static", - "rand_core 0.6.4", - "ring 0.16.20", - "secp256k1", - "thiserror", -] - [[package]] name = "heck" version = "0.5.0" @@ -1226,17 +1320,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "http" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" -dependencies = [ - "bytes 1.6.0", - "fnv", - "itoa", -] - [[package]] name = "http" version = "1.1.0" @@ -1248,17 +1331,6 @@ dependencies = [ "itoa", ] -[[package]] -name = "http-body" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" -dependencies = [ - "bytes 1.6.0", - "http 0.2.12", - "pin-project-lite", -] - [[package]] name = "http-body" version = "1.0.0" @@ -1266,7 +1338,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" dependencies = [ "bytes 1.6.0", - "http 1.1.0", + "http", ] [[package]] @@ -1277,8 +1349,8 @@ checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" dependencies = [ "bytes 1.6.0", "futures-util", - "http 1.1.0", - "http-body 1.0.0", + "http", + "http-body", "pin-project-lite", ] @@ -1300,30 +1372,6 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" -[[package]] -name = "hyper" -version = "0.14.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f361cde2f109281a220d4307746cdfd5ee3f410da58a70377762396775634b33" -dependencies = [ - "bytes 1.6.0", - "futures-channel", - "futures-core", - "futures-util", - "h2 0.3.26", - "http 0.2.12", - "http-body 0.4.6", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", - "want", -] - [[package]] name = "hyper" version = "1.3.1" @@ -1333,10 +1381,11 @@ dependencies = [ "bytes 1.6.0", "futures-channel", "futures-util", - "h2 0.4.5", - "http 1.1.0", - "http-body 1.0.0", + "h2", + "http", + "http-body", "httparse", + "httpdate", "itoa", "pin-project-lite", "smallvec", @@ -1346,29 +1395,34 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.23.2" +version = "0.27.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1788965e61b367cd03a62950836d5cd41560c3577d90e40e0819373194d1661c" +checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" dependencies = [ - "http 0.2.12", - "hyper 0.14.29", + "futures-util", + "http", + "hyper", + "hyper-util", "log", - "rustls 0.20.9", - "rustls-native-certs", + "rustls", + "rustls-native-certs 0.8.0", + "rustls-pki-types", "tokio", - "tokio-rustls 0.23.4", + "tokio-rustls", + "tower-service", ] [[package]] name = "hyper-timeout" -version = "0.4.1" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" +checksum = "3203a961e5c83b6f5498933e78b6b263e208c197b63e9c6c53cc82ffd3f63793" dependencies = [ - "hyper 0.14.29", + "hyper", + "hyper-util", "pin-project-lite", "tokio", - "tokio-io-timeout", + "tower-service", ] [[package]] @@ -1379,7 +1433,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes 1.6.0", "http-body-util", - "hyper 1.3.1", + "hyper", "hyper-util", "native-tls", "tokio", @@ -1396,9 +1450,9 @@ dependencies = [ "bytes 1.6.0", "futures-channel", "futures-util", - "http 1.1.0", - "http-body 1.0.0", - "hyper 1.3.1", + "http", + "http-body", + "hyper", "pin-project-lite", "socket2", "tokio", @@ -1442,9 +1496,9 @@ dependencies = [ [[package]] name = "incrementalmerkletree" -version = "0.5.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb1872810fb725b06b8c153dde9e86f3ec26747b9b60096da7a869883b549cbe" +checksum = "75346da3bd8e3d8891d02508245ed2df34447ca6637e343829f8d08986e9cde2" dependencies = [ "either", "proptest", @@ -1517,21 +1571,21 @@ dependencies = [ "either", ] -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + [[package]] name = "js-sys" version = "0.3.69" @@ -1576,15 +1630,31 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" dependencies = [ - "spin 0.9.8", + "spin", ] +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "libc" version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +[[package]] +name = "libloading" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" +dependencies = [ + "cfg-if", + "windows-targets 0.52.5", +] + [[package]] name = "libm" version = "0.2.8" @@ -1605,23 +1675,27 @@ dependencies = [ name = "libtonode-tests" version = "0.2.0" dependencies = [ + "bech32 0.11.0", + "bip0039", "hex", - "http 0.2.12", - "itertools 0.10.5", + "http", + "itertools", "json", "log", "orchard", + "proptest", "sapling-crypto", "serde_json", "shardtree", + "tempfile", "tokio", + "tracing-subscriber", "zcash_address", "zcash_client_backend", "zcash_primitives", + "zingo-netutils", "zingo-status", - "zingo-testutils", - "zingo-testvectors", - "zingoconfig", + "zingo-sync", "zingolib", ] @@ -1753,6 +1827,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "mirai-annotations" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9be0862c1b3f26a88803c4a49de6889c10e608b3ee9344e6ef5b45fb37ad3d1" + [[package]] name = "multimap" version = "0.10.0" @@ -1890,9 +1970,9 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl" -version = "0.10.64" +version = "0.10.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" +checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" dependencies = [ "bitflags 2.6.0", "cfg-if", @@ -1922,9 +2002,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.102" +version = "0.9.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" +checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" dependencies = [ "cc", "libc", @@ -1940,9 +2020,9 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "orchard" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0462569fc8b0d1b158e4d640571867a4e4319225ebee2ab6647e60c70af19ae3" +checksum = "4dc7bde644aeb980be296cd908c6650894dc8541deb56f9f5294c52ed7ca568f" dependencies = [ "aes", "bitvec", @@ -1963,6 +2043,7 @@ dependencies = [ "serde", "subtle", "tracing", + "visibility", "zcash_note_encryption", "zcash_spec", "zip32", @@ -2017,9 +2098,9 @@ dependencies = [ [[package]] name = "password-hash" -version = "0.3.2" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d791538a6dcc1e7cb7fe6f6b58aca40e7f79403c45b2bc274008b5e647af1d8" +checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" dependencies = [ "base64ct", "rand_core 0.6.4", @@ -2041,11 +2122,17 @@ dependencies = [ "subtle", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pbkdf2" -version = "0.10.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271779f35b581956db91a3e55737327a03aa051e90b1c47aeb189508533adfd7" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" dependencies = [ "digest 0.10.7", "password-hash", @@ -2202,9 +2289,9 @@ dependencies = [ [[package]] name = "prost" -version = "0.12.6" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" +checksum = "3b2ecbe40f08db5c006b5764a2645f7f3f141ce756412ac9e1dd6087e6d32995" dependencies = [ "bytes 1.6.0", "prost-derive", @@ -2212,13 +2299,13 @@ dependencies = [ [[package]] name = "prost-build" -version = "0.12.6" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4" +checksum = "f8650aabb6c35b860610e9cff5dc1af886c9e25073b7b1712a68972af4281302" dependencies = [ "bytes 1.6.0", "heck", - "itertools 0.12.1", + "itertools", "log", "multimap", "once_cell", @@ -2233,12 +2320,12 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.12.6" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" +checksum = "acf0c195eebb4af52c752bec4f52f645da98b6e92077a04110c7f349477ae5ac" dependencies = [ "anyhow", - "itertools 0.12.1", + "itertools", "proc-macro2", "quote", "syn 2.0.68", @@ -2246,9 +2333,9 @@ dependencies = [ [[package]] name = "prost-types" -version = "0.12.6" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9091c90b0a32608e984ff2fa4091273cbdd755d54935c51d520887f4a1dbd5b0" +checksum = "60caa6738c7369b940c3d49246a8d1749323674c65cb13010134f5c9bad5b519" dependencies = [ "prost", ] @@ -2480,11 +2567,11 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "h2 0.4.5", - "http 1.1.0", - "http-body 1.0.0", + "h2", + "http", + "http-body", "http-body-util", - "hyper 1.3.1", + "hyper", "hyper-tls", "hyper-util", "ipnet", @@ -2499,7 +2586,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "sync_wrapper", + "sync_wrapper 0.1.2", "system-configuration", "tokio", "tokio-native-tls", @@ -2511,21 +2598,6 @@ dependencies = [ "winreg", ] -[[package]] -name = "ring" -version = "0.16.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" -dependencies = [ - "cc", - "libc", - "once_cell", - "spin 0.5.2", - "untrusted 0.7.1", - "web-sys", - "winapi", -] - [[package]] name = "ring" version = "0.17.8" @@ -2536,8 +2608,8 @@ dependencies = [ "cfg-if", "getrandom", "libc", - "spin 0.9.8", - "untrusted 0.9.0", + "spin", + "untrusted", "windows-sys 0.52.0", ] @@ -2550,17 +2622,6 @@ dependencies = [ "digest 0.10.7", ] -[[package]] -name = "ripemd160" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eca4ecc81b7f313189bf73ce724400a07da2a6dac19588b03c8bd76a2dcc251" -dependencies = [ - "block-buffer 0.9.0", - "digest 0.9.0", - "opaque-debug", -] - [[package]] name = "rust-embed" version = "6.8.1" @@ -2601,6 +2662,12 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustix" version = "0.38.34" @@ -2616,36 +2683,42 @@ dependencies = [ [[package]] name = "rustls" -version = "0.20.9" +version = "0.23.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b80e3dec595989ea8510028f30c408a4630db12c9cbb8de34203b89d6577e99" +checksum = "f2dabaac7466917e566adb06783a81ca48944c6898a1b08b9374106dd671f4c8" dependencies = [ + "aws-lc-rs", "log", - "ring 0.16.20", - "sct", - "webpki 0.22.4", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", ] [[package]] -name = "rustls" -version = "0.21.12" +name = "rustls-native-certs" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" dependencies = [ - "log", - "ring 0.17.8", - "rustls-webpki", - "sct", + "openssl-probe", + "rustls-pemfile 2.1.2", + "rustls-pki-types", + "schannel", + "security-framework", ] [[package]] name = "rustls-native-certs" -version = "0.6.3" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +checksum = "fcaf18a4f2be7326cd874a5fa579fae794320a0f388d365dca7e480e55f83f8a" dependencies = [ "openssl-probe", - "rustls-pemfile 1.0.4", + "rustls-pemfile 2.1.2", + "rustls-pki-types", "schannel", "security-framework", ] @@ -2671,18 +2744,20 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" +checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" [[package]] name = "rustls-webpki" -version = "0.101.7" +version = "0.102.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" dependencies = [ - "ring 0.17.8", - "untrusted 0.9.0", + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", ] [[package]] @@ -2743,9 +2818,9 @@ dependencies = [ [[package]] name = "sapling-crypto" -version = "0.1.3" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02f4270033afcb0c74c5c7d59c73cfd1040367f67f224fe7ed9a919ae618f1b7" +checksum = "15e379398fffad84e49f9a45a05635fc004f66086e65942dbf4eb95332c26d2a" dependencies = [ "aes", "bellman", @@ -2788,21 +2863,11 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "sct" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring 0.17.8", - "untrusted 0.9.0", -] - [[package]] name = "secp256k1" -version = "0.26.0" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4124a35fe33ae14259c490fd70fa199a32b9ce9502f2ee6bc4f81ec06fa65894" +checksum = "25996b82292a7a57ed3508f052cfff8640d38d32018784acd714758b43da9c8f" dependencies = [ "secp256k1-sys", ] @@ -2949,9 +3014,9 @@ dependencies = [ [[package]] name = "shardtree" -version = "0.3.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cdd24424ce0b381646737fedddc33c4dcf7dcd2d545056b53f7982097bef5" +checksum = "78222845cd8bbe5eb95687407648ff17693a35de5e8abaa39a4681fb21e033f9" dependencies = [ "bitflags 2.6.0", "either", @@ -2969,6 +3034,12 @@ dependencies = [ "regex", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signal-hook-registry" version = "1.4.2" @@ -3003,12 +3074,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "spin" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" - [[package]] name = "spin" version = "0.9.8" @@ -3035,9 +3100,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "subtle" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] name = "syn" @@ -3067,6 +3132,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "sync_wrapper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" + [[package]] name = "system-configuration" version = "0.5.1" @@ -3171,9 +3242,9 @@ dependencies = [ [[package]] name = "thread-id" -version = "4.2.1" +version = "4.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0ec81c46e9eb50deaa257be2f148adf052d1fb7701cfd55ccfab2525280b70b" +checksum = "cfe8f25bbdd100db7e1d34acf7fd2dc59c4bf8f7483f505eaa7d4f12f76cc0ea" dependencies = [ "libc", "winapi", @@ -3242,16 +3313,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "tokio-io-timeout" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" -dependencies = [ - "pin-project-lite", - "tokio", -] - [[package]] name = "tokio-macros" version = "2.3.0" @@ -3275,22 +3336,12 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.23.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" -dependencies = [ - "rustls 0.20.9", - "tokio", - "webpki 0.22.4", -] - -[[package]] -name = "tokio-rustls" -version = "0.24.1" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" dependencies = [ - "rustls 0.21.12", + "rustls", + "rustls-pki-types", "tokio", ] @@ -3320,41 +3371,43 @@ dependencies = [ [[package]] name = "tonic" -version = "0.10.2" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d560933a0de61cf715926b9cac824d4c883c2c43142f787595e48280c40a1d0e" +checksum = "c6f6ba989e4b2c58ae83d862d3a3e27690b6e3ae630d0deb59f3697f32aa88ad" dependencies = [ "async-stream", "async-trait", "axum", - "base64 0.21.7", + "base64 0.22.1", "bytes 1.6.0", - "h2 0.3.26", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.29", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", "hyper-timeout", + "hyper-util", "percent-encoding", "pin-project", "prost", - "rustls 0.21.12", - "rustls-native-certs", - "rustls-pemfile 1.0.4", + "rustls-native-certs 0.7.3", + "rustls-pemfile 2.1.2", + "socket2", "tokio", - "tokio-rustls 0.24.1", + "tokio-rustls", "tokio-stream", "tower", "tower-layer", "tower-service", "tracing", - "webpki-roots 0.25.4", + "webpki-roots 0.26.5", ] [[package]] name = "tonic-build" -version = "0.10.2" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d021fc044c18582b9a2408cd0dd05b1596e3ecdb5c4df822bb0183545683889" +checksum = "fe4ee8877250136bd7e3d2331632810a4df4ea5e004656990d8d66d2f5ee8a67" dependencies = [ "prettyplease", "proc-macro2", @@ -3550,12 +3603,6 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" -[[package]] -name = "untrusted" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" - [[package]] name = "untrusted" version = "0.9.0" @@ -3597,6 +3644,17 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "visibility" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d674d135b4a8c1d7e813e2f8d1c9a58308aee4a680323066025e53132218bd91" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.68", +] + [[package]] name = "wait-timeout" version = "0.2.0" @@ -3708,40 +3766,20 @@ dependencies = [ ] [[package]] -name = "webpki" -version = "0.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" -dependencies = [ - "ring 0.16.20", - "untrusted 0.7.1", -] - -[[package]] -name = "webpki" -version = "0.22.4" +name = "webpki-roots" +version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" -dependencies = [ - "ring 0.17.8", - "untrusted 0.9.0", -] +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" [[package]] name = "webpki-roots" -version = "0.21.1" +version = "0.26.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aabe153544e473b775453675851ecc86863d2a81d786d741f6b76778f2a48940" +checksum = "0bd24728e5af82c6c4ec1b66ac4844bdf8156257fccda846ec58b42cd0cdbe6a" dependencies = [ - "webpki 0.21.4", + "rustls-pki-types", ] -[[package]] -name = "webpki-roots" -version = "0.25.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" - [[package]] name = "which" version = "4.4.2" @@ -3960,31 +3998,33 @@ checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" [[package]] name = "zcash_address" -version = "0.3.2" -source = "git+https://github.com/zingolabs/librustzcash.git?tag=always_require_change#ae0d477addf1034fdb5ce9d2dfbdf975e58b714c" +version = "0.4.0" +source = "git+https://github.com/zingolabs/librustzcash.git?tag=zcash_client_sqlite-0.11.2_plus_zingolabs_changes-1-g7ad60b5d5-2-g121371a08#121371a089f076a5ee2737809c792d905f5a4b3a" dependencies = [ - "bech32", + "bech32 0.9.1", "bs58", "f4jumble", + "proptest", "zcash_encoding", "zcash_protocol", ] [[package]] name = "zcash_client_backend" -version = "0.12.1" -source = "git+https://github.com/zingolabs/librustzcash.git?tag=always_require_change#ae0d477addf1034fdb5ce9d2dfbdf975e58b714c" +version = "0.13.0" +source = "git+https://github.com/zingolabs/librustzcash.git?tag=zcash_client_sqlite-0.11.2_plus_zingolabs_changes-1-g7ad60b5d5-2-g121371a08#121371a089f076a5ee2737809c792d905f5a4b3a" dependencies = [ "base64 0.21.7", - "bech32", + "bech32 0.9.1", + "bip32", "bls12_381", "bs58", "byteorder", "crossbeam-channel", "document-features", "group", - "hdwallet", "hex", + "hyper-util", "incrementalmerkletree", "memuse", "nom", @@ -4010,12 +4050,13 @@ dependencies = [ "zcash_primitives", "zcash_protocol", "zip32", + "zip321", ] [[package]] name = "zcash_encoding" -version = "0.2.0" -source = "git+https://github.com/zingolabs/librustzcash.git?tag=always_require_change#ae0d477addf1034fdb5ce9d2dfbdf975e58b714c" +version = "0.2.1" +source = "git+https://github.com/zingolabs/librustzcash.git?tag=zcash_client_sqlite-0.11.2_plus_zingolabs_changes-1-g7ad60b5d5-2-g121371a08#121371a089f076a5ee2737809c792d905f5a4b3a" dependencies = [ "byteorder", "nonempty", @@ -4023,17 +4064,17 @@ dependencies = [ [[package]] name = "zcash_keys" -version = "0.2.0" -source = "git+https://github.com/zingolabs/librustzcash.git?tag=always_require_change#ae0d477addf1034fdb5ce9d2dfbdf975e58b714c" +version = "0.3.0" +source = "git+https://github.com/zingolabs/librustzcash.git?tag=zcash_client_sqlite-0.11.2_plus_zingolabs_changes-1-g7ad60b5d5-2-g121371a08#121371a089f076a5ee2737809c792d905f5a4b3a" dependencies = [ - "bech32", + "bech32 0.9.1", + "bip32", "blake2b_simd", "bls12_381", "bs58", "byteorder", "document-features", "group", - "hdwallet", "memuse", "nonempty", "orchard", @@ -4064,19 +4105,19 @@ dependencies = [ [[package]] name = "zcash_primitives" -version = "0.15.0" -source = "git+https://github.com/zingolabs/librustzcash.git?tag=always_require_change#ae0d477addf1034fdb5ce9d2dfbdf975e58b714c" +version = "0.16.0" +source = "git+https://github.com/zingolabs/librustzcash.git?tag=zcash_client_sqlite-0.11.2_plus_zingolabs_changes-1-g7ad60b5d5-2-g121371a08#121371a089f076a5ee2737809c792d905f5a4b3a" dependencies = [ "aes", - "bip0039", + "bip32", "blake2b_simd", + "bs58", "byteorder", "document-features", "equihash", "ff", "fpe", "group", - "hdwallet", "hex", "incrementalmerkletree", "jubjub", @@ -4102,8 +4143,8 @@ dependencies = [ [[package]] name = "zcash_proofs" -version = "0.15.0" -source = "git+https://github.com/zingolabs/librustzcash.git?tag=always_require_change#ae0d477addf1034fdb5ce9d2dfbdf975e58b714c" +version = "0.16.0" +source = "git+https://github.com/zingolabs/librustzcash.git?tag=zcash_client_sqlite-0.11.2_plus_zingolabs_changes-1-g7ad60b5d5-2-g121371a08#121371a089f076a5ee2737809c792d905f5a4b3a" dependencies = [ "bellman", "blake2b_simd", @@ -4124,8 +4165,8 @@ dependencies = [ [[package]] name = "zcash_protocol" -version = "0.1.1" -source = "git+https://github.com/zingolabs/librustzcash.git?tag=always_require_change#ae0d477addf1034fdb5ce9d2dfbdf975e58b714c" +version = "0.2.0" +source = "git+https://github.com/zingolabs/librustzcash.git?tag=zcash_client_sqlite-0.11.2_plus_zingolabs_changes-1-g7ad60b5d5-2-g121371a08#121371a089f076a5ee2737809c792d905f5a4b3a" dependencies = [ "document-features", "memuse", @@ -4165,13 +4206,12 @@ name = "zingo-cli" version = "0.2.0" dependencies = [ "clap", - "http 0.2.12", + "http", "json", "log", + "rustls", "rustyline", "shellwords", - "zingo-testutils", - "zingoconfig", "zingolib", ] @@ -4190,17 +4230,19 @@ dependencies = [ name = "zingo-netutils" version = "0.1.0" dependencies = [ - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.29", + "http", + "http-body", + "http-body-util", + "hyper", "hyper-rustls", + "hyper-util", "prost", "rustls-pemfile 1.0.4", "thiserror", - "tokio-rustls 0.23.4", + "tokio-rustls", "tonic", "tower", - "webpki-roots 0.21.1", + "webpki-roots 0.25.4", "zcash_client_backend", ] @@ -4212,47 +4254,26 @@ dependencies = [ ] [[package]] -name = "zingo-testutils" +name = "zingo-sync" version = "0.1.0" dependencies = [ - "http 0.2.12", + "crossbeam-channel", + "futures", + "getset", "incrementalmerkletree", - "json", - "log", - "nonempty", + "memuse", "orchard", - "portpicker", - "tempdir", + "rayon", + "sapling-crypto", + "shardtree", "tokio", "tonic", - "zcash_address", + "tracing", "zcash_client_backend", + "zcash_keys", + "zcash_note_encryption", "zcash_primitives", "zingo-netutils", - "zingo-status", - "zingo-testvectors", - "zingoconfig", - "zingolib", -] - -[[package]] -name = "zingo-testvectors" -version = "0.1.0" -dependencies = [ - "zcash_primitives", - "zingoconfig", -] - -[[package]] -name = "zingoconfig" -version = "0.1.0" -dependencies = [ - "dirs", - "http 0.2.12", - "log", - "log4rs", - "tempdir", - "zcash_primitives", ] [[package]] @@ -4262,26 +4283,31 @@ dependencies = [ "append-only-vec", "base58", "base64 0.13.1", + "bech32 0.11.0", + "bip0039", + "bip32", "bls12_381", + "bs58", "build_utils", "byteorder", "bytes 0.4.12", "chrono", "concat-idents", + "dirs", "enum_dispatch", "ff", "futures", "getset", "group", - "hdwallet", "hex", - "http 0.2.12", + "http", "incrementalmerkletree", "indoc", "json", "jubjub", "lazy_static", "log", + "log4rs", "nonempty", "orchard", "portpicker", @@ -4289,8 +4315,7 @@ dependencies = [ "prost", "rand 0.8.5", "reqwest", - "ring 0.17.8", - "ripemd160", + "ring", "rust-embed", "sapling-crypto", "secp256k1", @@ -4300,6 +4325,7 @@ dependencies = [ "sha2 0.9.9", "shardtree", "subtle", + "tempdir", "tempfile", "test-case", "thiserror", @@ -4316,8 +4342,7 @@ dependencies = [ "zingo-memo", "zingo-netutils", "zingo-status", - "zingo-testvectors", - "zingoconfig", + "zingo-sync", "zip32", ] @@ -4331,3 +4356,15 @@ dependencies = [ "memuse", "subtle", ] + +[[package]] +name = "zip321" +version = "0.1.0" +source = "git+https://github.com/zingolabs/librustzcash.git?tag=zcash_client_sqlite-0.11.2_plus_zingolabs_changes-1-g7ad60b5d5-2-g121371a08#121371a089f076a5ee2737809c792d905f5a4b3a" +dependencies = [ + "base64 0.21.7", + "nom", + "percent-encoding", + "zcash_address", + "zcash_protocol", +] diff --git a/Cargo.toml b/Cargo.toml index 8cfb725413..082b2b0816 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,64 +4,59 @@ # This doesn't account for all dependency relations, for example, zingocli depends on zingoconfig directly (for now). [workspace] members = [ - "zingo-testutils", "libtonode-tests", "darkside-tests", "zingocli", "zingolib", - "zingoconfig", - "zingo-testvectors", "zingo-netutils", "zingo-memo", + "zingo-sync", ] resolver = "2" [workspace.dependencies] -zcash_address = { git = "https://github.com/zingolabs/librustzcash.git", tag = "always_require_change" } -zcash_client_backend = { git = "https://github.com/zingolabs/librustzcash.git", tag = "always_require_change", features = ["lightwalletd-tonic", "orchard", "transparent-inputs"] } -zcash_encoding = { git = "https://github.com/zingolabs/librustzcash.git", tag = "always_require_change" } -zcash_keys = { git = "https://github.com/zingolabs/librustzcash.git", tag = "always_require_change", features = ["orchard"] } +bip0039 = "0.11" +orchard = "0.9" +sapling-crypto = "0.2" +shardtree = "0.4" + +# annotated tag starting with LRZ base tag and ending with `git describe --dirty` +# TAG FROM `main_zingolib` BRANCH OF LIBRUSTZCASH FORK! +zcash_address = { git = "https://github.com/zingolabs/librustzcash.git", tag = "zcash_client_sqlite-0.11.2_plus_zingolabs_changes-1-g7ad60b5d5-2-g121371a08" } +zcash_client_backend = { git = "https://github.com/zingolabs/librustzcash.git", tag = "zcash_client_sqlite-0.11.2_plus_zingolabs_changes-1-g7ad60b5d5-2-g121371a08" , features = ["lightwalletd-tonic", "orchard", "transparent-inputs"] } +zcash_encoding = { git = "https://github.com/zingolabs/librustzcash.git", tag = "zcash_client_sqlite-0.11.2_plus_zingolabs_changes-1-g7ad60b5d5-2-g121371a08" } +zcash_keys = { git = "https://github.com/zingolabs/librustzcash.git", tag = "zcash_client_sqlite-0.11.2_plus_zingolabs_changes-1-g7ad60b5d5-2-g121371a08" , features = ["transparent-inputs", "sapling", "orchard" ] } zcash_note_encryption = "0.4" -zcash_primitives = { git = "https://github.com/zingolabs/librustzcash.git", tag = "always_require_change" } -zcash_proofs = { git = "https://github.com/zingolabs/librustzcash.git", tag = "always_require_change" } -zcash_protocol = { git = "https://github.com/zingolabs/librustzcash.git", tag = "always_require_change" } -sapling-crypto = "0.1.2" -orchard = "0.8" +zcash_primitives = { git = "https://github.com/zingolabs/librustzcash.git", tag = "zcash_client_sqlite-0.11.2_plus_zingolabs_changes-1-g7ad60b5d5-2-g121371a08" } +zcash_proofs = { git = "https://github.com/zingolabs/librustzcash.git", tag = "zcash_client_sqlite-0.11.2_plus_zingolabs_changes-1-g7ad60b5d5-2-g121371a08" } +zcash_protocol = { git = "https://github.com/zingolabs/librustzcash.git", tag = "zcash_client_sqlite-0.11.2_plus_zingolabs_changes-1-g7ad60b5d5-2-g121371a08" } zip32 = "0.1" -clap = "4.4" -tempdir = "0.3" -portpicker = "0.1" -incrementalmerkletree = { version = "0.5" } -futures = "0.3.15" -shardtree = "0.3" -build_utils = { path = "./build_utils" } -http = "0.2.4" -hyper = { version = "0.14", features = ["full"] } -hyper-rustls = { version = "0.23", features = ["http2"] } -http-body = "0.4.4" -tonic = {version = "0.10.0", features = ["tls", "tls-roots", "tls-webpki-roots"]} -prost = "0.12.0" -tower = { version = "0.4" } -hex = "0.4" -tokio-rustls = "0.23" -webpki-roots = "0.25" -thiserror = "1.0.59" -nonempty = "0.7" append-only-vec = { git = "https://github.com/zancas/append-only-vec.git", branch = "add_debug_impl" } base58 = "0.1.0" base64 = "0.13.0" bls12_381 = "0.8" +build_utils = { path = "./build_utils" } byteorder = "1" bytes = "0.4" -concat-idents = "1.1.3" chrono = "0.4" +clap = "4.4" +concat-idents = "1.1.3" dirs = "5.0" +enum_dispatch = "0.3.13" ff = "0.13" +futures = "0.3.15" futures-util = "0.3.28" getset = "0.1.2" group = "0.13" -hdwallet = "0.4.1" +hex = "0.4" +http = "1.1.0" +http-body-util = "0.1.2" +http-body = "1.0.0" +hyper-util = "0.1.5" +hyper = { version = "1.3.1", features = ["full"] } +hyper-rustls = { version = "0.27", features = ["http2"] } +incrementalmerkletree = { version = "0.6.0" } indoc = "2.0.1" itertools = "0.10.5" json = "0.12.4" @@ -69,27 +64,43 @@ jubjub = "0.10.0" lazy_static = "1.4.0" log = "0.4.19" log4rs = "1.1.1" +memuse = "0.2.1" +nonempty = "0.7" +portpicker = "0.1" proptest = "1.4.0" +prost = "0.13.2" rand = "0.8.5" reqwest = "0.12" ring = "0.17.0" -ripemd160 = "0.9.1" rust-embed = "6.3.0" +rustls = { version = "0.23.13", features = ["ring"] } rustls-pemfile = "1.0.0" rustyline = "11.0.0" -secp256k1 = "=0.26.0" +secp256k1 = "=0.27.0" secrecy = "0.8.0" serde = "1.0.201" serde_json = "1.0.107" sha2 = "0.9.5" shellwords = "1.1.0" -subtle = "2.4.1" +subtle = "~2.5.0" +tempdir = "0.3" tempfile = "3.3.0" test-case = "3.3.1" +thiserror = "1.0.59" tokio = "1.28.2" -tonic-build = "0.10" +tokio-rustls = "0.26" +tonic = {version = "0.12.2", features = ["tls", "tls-roots", "tls-webpki-roots"]} +tonic-build = "0.12" +tower = { version = "0.4" } +tracing = "0.1.40" tracing-subscriber = "0.3.15" -enum_dispatch = "0.3.13" +webpki-roots = "0.25" + +# Parallel processing +crossbeam-channel = "0.5" +rayon = "1.5" +bip32 = { version = "0.5", default-features = false } +bs58 = "0.5" [profile.release] debug = false diff --git a/darkside-tests/Cargo.toml b/darkside-tests/Cargo.toml index f7c6d76614..528b0318fa 100644 --- a/darkside-tests/Cargo.toml +++ b/darkside-tests/Cargo.toml @@ -8,14 +8,14 @@ edition = "2021" chain_generic_tests = [] [dependencies] -zingolib = { path = "../zingolib", features = ["darkside_tests"] } -zingo-testutils = { path = "../zingo-testutils" } -zingo-testvectors = { path = "../zingo-testvectors" } -zingoconfig = { path = "../zingoconfig" } +zingolib = { path = "../zingolib", features = ["darkside_tests", "testvectors"] } +zingo-netutils = { path = "../zingo-netutils" } tokio = { workspace = true, features = ["full"] } json = { workspace = true } http = { workspace = true } +http-body-util = { workspace = true } hyper = { workspace = true } +hyper-util = { workspace = true } tonic = { workspace = true } prost = { workspace = true } tower = { workspace = true } diff --git a/darkside-tests/proptest-regressions/chain_generics.txt b/darkside-tests/proptest-regressions/chain_generics.txt new file mode 100644 index 0000000000..7c6ef1313e --- /dev/null +++ b/darkside-tests/proptest-regressions/chain_generics.txt @@ -0,0 +1,9 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc eef3deec08ec4d278ea80acf3351dba273deacb3d9544ebf173c6ce8d5d3f79d # shrinks to value = 0 +cc 58a549bf09db2b84ee36488c0bda1295ca5068fa7ec753dae3ca0b955bedd613 # shrinks to value = 0 +cc e2b75ab014d34f914e99dd8b15e75371a3da8e4d14896d8d65dc7199990de081 # shrinks to value = 34 diff --git a/darkside-tests/src/chain_generics.rs b/darkside-tests/src/chain_generics.rs index e9b52c091b..4c3ea3173a 100644 --- a/darkside-tests/src/chain_generics.rs +++ b/darkside-tests/src/chain_generics.rs @@ -6,7 +6,7 @@ use zcash_client_backend::PoolType::Transparent; use zcash_client_backend::ShieldedProtocol::Orchard; use zcash_client_backend::ShieldedProtocol::Sapling; -use zingo_testutils::chain_generics::fixtures::send_value_to_pool; +use zingolib::testutils::chain_generics::fixtures::send_value_to_pool; use crate::utils::scenarios::DarksideEnvironment; @@ -31,14 +31,14 @@ proptest! { }); } } -pub(crate) mod impl_conduct_chain_for_darkside_environment { +pub(crate) mod conduct_chain { //! known issues include //! - transparent sends do not work //! - txids are regenerated randomly. zingo can optionally accept_server_txid - //! these tests cannot portray the full range of network weather. + //! - these tests cannot portray the full range of network weather. - use zingo_testutils::chain_generics::conduct_chain::ConductChain; use zingolib::lightclient::LightClient; + use zingolib::testutils::chain_generics::conduct_chain::ConductChain; use zingolib::wallet::WalletBase; use crate::constants::ABANDON_TO_DARKSIDE_SAP_10_000_000_ZAT; @@ -67,19 +67,9 @@ pub(crate) mod impl_conduct_chain_for_darkside_environment { .unwrap() } - async fn create_client(&mut self) -> LightClient { - let mut zingo_config = self - .client_builder - .make_unique_data_dir_and_load_config(self.regtest_network); - zingo_config.accept_server_txids = true; - LightClient::create_from_wallet_base_async( - WalletBase::FreshEntropy, - &zingo_config, - 0, - false, - ) - .await - .unwrap() + fn zingo_config(&mut self) -> zingolib::config::ZingoConfig { + self.client_builder + .make_unique_data_dir_and_load_config(self.regtest_network) } async fn bump_chain(&mut self) { diff --git a/darkside-tests/src/lib.rs b/darkside-tests/src/lib.rs index 2dd5a18264..11117b9da4 100644 --- a/darkside-tests/src/lib.rs +++ b/darkside-tests/src/lib.rs @@ -4,5 +4,5 @@ pub mod darkside_types { tonic::include_proto!("cash.z.wallet.sdk.rpc"); } -#[cfg(test)] //TODO: Evaluate necessity. +#[cfg(test)] pub mod chain_generics; diff --git a/darkside-tests/src/utils.rs b/darkside-tests/src/utils.rs index 4495f834f5..b850bb4560 100644 --- a/darkside-tests/src/utils.rs +++ b/darkside-tests/src/utils.rs @@ -1,6 +1,5 @@ use http::Uri; -use http_body::combinators::UnsyncBoxBody; -use hyper::client::HttpConnector; +use hyper_util::client::legacy::connect::HttpConnector; use orchard::{note_encryption::OrchardDomain, tree::MerkleHashOrchard}; use sapling_crypto::note_encryption::SaplingDomain; use std::{ @@ -14,12 +13,11 @@ use std::{ }; use tempdir; use tokio::time::sleep; -use tonic::Status; -use tower::{util::BoxCloneService, ServiceExt}; +use tower::ServiceExt; use zcash_primitives::consensus::BranchId; use zcash_primitives::{merkle_tree::read_commitment_tree, transaction::Transaction}; -use zingo_testutils::{ - self, +use zingo_netutils::UnderlyingService; +use zingolib::testutils::{ incrementalmerkletree::frontier::CommitmentTree, paths::{get_bin_dir, get_cargo_manifest_dir}, regtest::launch_lightwalletd, @@ -39,13 +37,6 @@ use super::{ constants, darkside_types::{RawTransaction, TreeState}, }; - -type UnderlyingService = BoxCloneService< - http::Request>, - http::Response, - hyper::Error, ->; - macro_rules! define_darkside_connector_methods( ($($name:ident (&$self:ident $(,$param:ident: $param_type:ty)*$(,)?) -> $return:ty {$param_packing:expr}),*) => {$( #[allow(unused)] @@ -76,7 +67,7 @@ impl DarksideConnector { let mut http_connector = HttpConnector::new(); http_connector.enforce_http(false); let connector = tower::ServiceBuilder::new().service(http_connector); - let client = Box::new(hyper::Client::builder().http2_only(true).build(connector)); + let client = zingo_netutils::client::client_from_connector(connector, true); let uri = uri.clone(); let svc = tower::ServiceBuilder::new() //Here, we take all the pieces of our uri, and add in the path from the Requests's uri @@ -270,7 +261,9 @@ impl Drop for DarksideHandler { .is_err() { // if regular kill doesn't work, kill it harder - let _ = self.lightwalletd_handle.kill(); + self.lightwalletd_handle + .kill() + .expect("command couldn't be killed"); } } } @@ -397,7 +390,7 @@ pub async fn init_darksidewalletd( set_port: Option, ) -> Result<(DarksideHandler, DarksideConnector), String> { let handler = DarksideHandler::new(set_port); - let server_id = zingoconfig::construct_lightwalletd_uri(Some(format!( + let server_id = zingolib::config::construct_lightwalletd_uri(Some(format!( "http://127.0.0.1:{}", handler.grpc_port ))); @@ -499,10 +492,10 @@ pub mod scenarios { }; use zcash_client_backend::{PoolType, ShieldedProtocol}; use zcash_primitives::consensus::{BlockHeight, BranchId}; - use zingo_testutils::scenarios::setup::ClientBuilder; - use zingo_testvectors::seeds::HOSPITAL_MUSEUM_SEED; - use zingoconfig::RegtestNetwork; + use zingolib::config::RegtestNetwork; use zingolib::lightclient::LightClient; + use zingolib::testutils::scenarios::setup::ClientBuilder; + use zingolib::testvectors::seeds::HOSPITAL_MUSEUM_SEED; use super::{ init_darksidewalletd, update_tree_states_for_transaction, write_raw_transaction, @@ -564,7 +557,7 @@ pub mod scenarios { self.faucet = Some( self.client_builder .build_client( - zingo_testvectors::seeds::DARKSIDE_SEED.to_string(), + zingolib::testvectors::seeds::DARKSIDE_SEED.to_string(), 0, true, self.regtest_network, @@ -679,7 +672,7 @@ pub mod scenarios { DarksideSender::IndexedClient(n) => self.get_lightclient(n), DarksideSender::ExternalClient(lc) => lc, }; - zingo_testutils::lightclient::from_inputs::quick_send( + zingolib::testutils::lightclient::from_inputs::quick_send( lightclient, vec![(receiver_address, value, None)], ) diff --git a/darkside-tests/tests/advanced_reorg_tests.rs b/darkside-tests/tests/advanced_reorg_tests.rs index 3b8ccf850f..5e215fbf74 100644 --- a/darkside-tests/tests/advanced_reorg_tests.rs +++ b/darkside-tests/tests/advanced_reorg_tests.rs @@ -12,19 +12,19 @@ use darkside_tests::{ use tokio::time::sleep; use zcash_primitives::consensus::BlockHeight; -use zingo_testutils::{ +use zingolib::lightclient::PoolBalances; +use zingolib::testutils::{ lightclient::from_inputs, paths::get_cargo_manifest_dir, scenarios::setup::ClientBuilder, }; -use zingoconfig::RegtestNetwork; -use zingolib::lightclient::PoolBalances; use zingolib::wallet::data::summaries::ValueTransferKind; +use zingolib::{config::RegtestNetwork, wallet::data::summaries::SentValueTransfer}; #[ignore] #[tokio::test] async fn reorg_changes_incoming_tx_height() { let darkside_handler = DarksideHandler::new(None); - let server_id = zingoconfig::construct_lightwalletd_uri(Some(format!( + let server_id = zingolib::config::construct_lightwalletd_uri(Some(format!( "http://127.0.0.1:{}", darkside_handler.grpc_port ))); @@ -180,7 +180,7 @@ async fn prepare_after_tx_height_change_reorg(uri: http::Uri) -> Result<(), Stri async fn reorg_changes_incoming_tx_index() { let darkside_handler = DarksideHandler::new(None); - let server_id = zingoconfig::construct_lightwalletd_uri(Some(format!( + let server_id = zingolib::config::construct_lightwalletd_uri(Some(format!( "http://127.0.0.1:{}", darkside_handler.grpc_port ))); @@ -335,7 +335,7 @@ async fn prepare_after_tx_index_change_reorg(uri: http::Uri) -> Result<(), Strin async fn reorg_expires_incoming_tx() { let darkside_handler = DarksideHandler::new(None); - let server_id = zingoconfig::construct_lightwalletd_uri(Some(format!( + let server_id = zingolib::config::construct_lightwalletd_uri(Some(format!( "http://127.0.0.1:{}", darkside_handler.grpc_port ))); @@ -513,7 +513,7 @@ async fn prepare_expires_incoming_tx_after_reorg(uri: http::Uri) -> Result<(), S async fn reorg_changes_outgoing_tx_height() { let darkside_handler = DarksideHandler::new(None); - let server_id = zingoconfig::construct_lightwalletd_uri(Some(format!( + let server_id = zingolib::config::construct_lightwalletd_uri(Some(format!( "http://127.0.0.1:{}", darkside_handler.grpc_port ))); @@ -605,7 +605,7 @@ async fn reorg_changes_outgoing_tx_height() { .await .iter() .find_map(|v| match v.kind() { - ValueTransferKind::Sent => { + ValueTransferKind::Sent(SentValueTransfer::Send) => { if let Some(addr) = v.recipient_address() { if addr == recipient_string && v.value() == 100_000 { Some(v.blockheight()) @@ -753,7 +753,7 @@ async fn prepare_changes_outgoing_tx_height_before_reorg(uri: http::Uri) -> Resu async fn reorg_expires_outgoing_tx_height() { let darkside_handler = DarksideHandler::new(None); - let server_id = zingoconfig::construct_lightwalletd_uri(Some(format!( + let server_id = zingolib::config::construct_lightwalletd_uri(Some(format!( "http://127.0.0.1:{}", darkside_handler.grpc_port ))); @@ -831,29 +831,25 @@ async fn reorg_expires_outgoing_tx_height() { println!("{:?}", light_client.value_transfers().await); - assert_eq!( - light_client - .value_transfers() - .await - .iter() - .find_map(|v| match v.kind() { - ValueTransferKind::Sent => { - if let Some(addr) = v.recipient_address() { - if addr == recipient_string && v.value() == 100_000 { - Some(v.blockheight()) - } else { - None - } + let send_height = light_client + .value_transfers() + .await + .iter() + .find_map(|v| match v.kind() { + ValueTransferKind::Sent(SentValueTransfer::Send) => { + if let Some(addr) = v.recipient_address() { + if addr == recipient_string && v.value() == 100_000 { + Some(v.blockheight()) } else { None } - } - _ => { + } else { None } - }), - Some(BlockHeight::from(sent_tx_height as u32)) - ); + } + _ => None, + }); + assert_eq!(send_height, Some(BlockHeight::from(sent_tx_height as u32))); // // Create reorg @@ -934,7 +930,7 @@ async fn reorg_expires_outgoing_tx_height() { async fn reorg_changes_outgoing_tx_index() { let darkside_handler = DarksideHandler::new(None); - let server_id = zingoconfig::construct_lightwalletd_uri(Some(format!( + let server_id = zingolib::config::construct_lightwalletd_uri(Some(format!( "http://127.0.0.1:{}", darkside_handler.grpc_port ))); @@ -1024,7 +1020,7 @@ async fn reorg_changes_outgoing_tx_index() { .await .iter() .find_map(|v| match v.kind() { - ValueTransferKind::Sent => { + ValueTransferKind::Sent(SentValueTransfer::Send) => { if let Some(addr) = v.recipient_address() { if addr == recipient_string && v.value() == 100_000 { Some(v.blockheight()) diff --git a/darkside-tests/tests/darkside_connector.rs b/darkside-tests/tests/darkside_connector.rs index cc447b45f3..53f2a0f657 100644 --- a/darkside-tests/tests/darkside_connector.rs +++ b/darkside-tests/tests/darkside_connector.rs @@ -3,17 +3,11 @@ use darkside_tests::darkside_types::{ DarksideEmptyBlocks, DarksideHeight, DarksideMetaState, Empty, RawTransaction, TreeState, }; -use http_body::combinators::UnsyncBoxBody; -use hyper::{client::HttpConnector, Uri}; +use hyper::Uri; +use hyper_util::client::legacy::connect::HttpConnector; use std::sync::Arc; -use tonic::Status; -use tower::{util::BoxCloneService, ServiceExt}; - -type UnderlyingService = BoxCloneService< - http::Request>, - http::Response, - hyper::Error, ->; +use tower::ServiceExt; +use zingo_netutils::UnderlyingService; macro_rules! define_darkside_connector_methods( ($($name:ident (&$self:ident $(,$param:ident: $param_type:ty)*$(,)?) -> $return:ty {$param_packing:expr}),*) => {$( @@ -49,7 +43,7 @@ impl DarksideConnector { let mut http_connector = HttpConnector::new(); http_connector.enforce_http(false); let connector = tower::ServiceBuilder::new().service(http_connector); - let client = Box::new(hyper::Client::builder().http2_only(true).build(connector)); + let client = zingo_netutils::client::client_from_connector(connector, true); let uri = uri.clone(); let svc = tower::ServiceBuilder::new() //Here, we take all the pieces of our uri, and add in the path from the Requests's uri diff --git a/darkside-tests/tests/network_interruption_tests.rs b/darkside-tests/tests/network_interruption_tests.rs index d6753934bf..8349dfe93b 100644 --- a/darkside-tests/tests/network_interruption_tests.rs +++ b/darkside-tests/tests/network_interruption_tests.rs @@ -17,13 +17,15 @@ use darkside_tests::{ }; use tokio::time::sleep; use zcash_client_backend::{PoolType, ShieldedProtocol}; -use zingo_testutils::{ - get_base_address_macro, scenarios::setup::ClientBuilder, start_proxy_and_connect_lightclient, -}; -use zingoconfig::RegtestNetwork; +use zingolib::config::RegtestNetwork; +use zingolib::get_base_address_macro; +use zingolib::testutils::{scenarios::setup::ClientBuilder, start_proxy_and_connect_lightclient}; use zingolib::{ lightclient::PoolBalances, - wallet::transaction_record::{SendType, TransactionKind}, + wallet::{ + data::summaries::TransactionSummaryInterface as _, + transaction_record::{SendType, TransactionKind}, + }, }; #[ignore] @@ -31,7 +33,7 @@ use zingolib::{ async fn interrupt_initial_tree_fetch() { let darkside_handler = DarksideHandler::new(None); - let server_id = zingoconfig::construct_lightwalletd_uri(Some(format!( + let server_id = zingolib::config::construct_lightwalletd_uri(Some(format!( "http://127.0.0.1:{}", darkside_handler.grpc_port ))); diff --git a/darkside-tests/tests/tests.rs b/darkside-tests/tests/tests.rs index 880cef9d3c..c97b16dc5b 100644 --- a/darkside-tests/tests/tests.rs +++ b/darkside-tests/tests/tests.rs @@ -2,18 +2,17 @@ use darkside_tests::utils::{ prepare_darksidewalletd, update_tree_states_for_transaction, DarksideConnector, DarksideHandler, }; use tokio::time::sleep; -use zingo_testutils::{ - get_base_address_macro, lightclient::from_inputs, scenarios::setup::ClientBuilder, -}; -use zingo_testvectors::seeds::DARKSIDE_SEED; -use zingoconfig::RegtestNetwork; +use zingolib::config::RegtestNetwork; +use zingolib::get_base_address_macro; use zingolib::lightclient::PoolBalances; +use zingolib::testutils::{lightclient::from_inputs, scenarios::setup::ClientBuilder}; +use zingolib::testvectors::seeds::DARKSIDE_SEED; #[tokio::test] async fn simple_sync() { let darkside_handler = DarksideHandler::new(None); - let server_id = zingoconfig::construct_lightwalletd_uri(Some(format!( + let server_id = zingolib::config::construct_lightwalletd_uri(Some(format!( "http://127.0.0.1:{}", darkside_handler.grpc_port ))); @@ -52,7 +51,7 @@ async fn simple_sync() { async fn reorg_away_receipt() { let darkside_handler = DarksideHandler::new(None); - let server_id = zingoconfig::construct_lightwalletd_uri(Some(format!( + let server_id = zingolib::config::construct_lightwalletd_uri(Some(format!( "http://127.0.0.1:{}", darkside_handler.grpc_port ))); @@ -104,7 +103,7 @@ async fn reorg_away_receipt() { async fn sent_transaction_reorged_into_mempool() { let darkside_handler = DarksideHandler::new(None); - let server_id = zingoconfig::construct_lightwalletd_uri(Some(format!( + let server_id = zingolib::config::construct_lightwalletd_uri(Some(format!( "http://127.0.0.1:{}", darkside_handler.grpc_port ))); @@ -120,7 +119,7 @@ async fn sent_transaction_reorged_into_mempool() { .await; let recipient = client_manager .build_client( - zingo_testvectors::seeds::HOSPITAL_MUSEUM_SEED.to_string(), + zingolib::testvectors::seeds::HOSPITAL_MUSEUM_SEED.to_string(), 1, true, regtest_network, @@ -197,9 +196,10 @@ async fn sent_transaction_reorged_into_mempool() { serde_json::to_string_pretty(&light_client.do_balance().await).unwrap() ); dbg!("Sender post-reorg: {}", light_client.list_outputs().await); - let loaded_client = zingo_testutils::lightclient::new_client_from_save_buffer(&light_client) - .await - .unwrap(); + let loaded_client = + zingolib::testutils::lightclient::new_client_from_save_buffer(&light_client) + .await + .unwrap(); loaded_client.do_sync(false).await.unwrap(); dbg!("Sender post-load: {}", loaded_client.list_outputs().await); assert_eq!( diff --git a/libtonode-tests/Cargo.toml b/libtonode-tests/Cargo.toml index 7c1805c106..cf77142d37 100644 --- a/libtonode-tests/Cargo.toml +++ b/libtonode-tests/Cargo.toml @@ -5,19 +5,20 @@ edition = "2021" [features] chain_generic_tests = [] +ci = ["zingolib/ci"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -zingolib = { path = "../zingolib", features = ["deprecations"] } +zingolib = { path = "../zingolib", features = ["deprecations", "test-elevation", "sync"] } zingo-status = { path = "../zingo-status" } -zingo-testutils = { path = "../zingo-testutils" } -zingo-testvectors = { path = "../zingo-testvectors" } -zingoconfig = { path = "../zingoconfig" } +zingo-netutils = { path = "../zingo-netutils" } +zingo-sync = { path = "../zingo-sync" } +bip0039.workspace = true zcash_primitives = { workspace = true } orchard = { workspace = true } sapling-crypto = { workspace = true } -zcash_address = { workspace = true } +zcash_address = { workspace = true, features = ["test-dependencies"] } zcash_client_backend = { workspace = true } shardtree = { workspace = true } @@ -28,3 +29,7 @@ hex = { workspace = true } itertools = { workspace = true } serde_json = { workspace = true } http.workspace = true +tempfile.workspace = true +tracing-subscriber.workspace = true +proptest.workspace = true +bech32 = "0.11.0" diff --git a/libtonode-tests/tests/chain_generics.rs b/libtonode-tests/tests/chain_generics.rs index e80bc0f076..270a51c2c2 100644 --- a/libtonode-tests/tests/chain_generics.rs +++ b/libtonode-tests/tests/chain_generics.rs @@ -4,9 +4,9 @@ mod chain_generics { use zcash_client_backend::ShieldedProtocol::Orchard; use zcash_client_backend::ShieldedProtocol::Sapling; - use zingo_testutils::chain_generics::fixtures; + use zingolib::testutils::chain_generics::fixtures; - use environment::LibtonodeEnvironment; + use conduct_chain::LibtonodeEnvironment; #[tokio::test] async fn generate_a_range_of_value_transfers() { fixtures::create_various_value_transfers::().await; @@ -192,16 +192,138 @@ mod chain_generics { async fn simpool_change_50_000_orchard_to_orchard() { fixtures::shpool_to_pool::(Orchard, Shielded(Orchard), 50_000).await; } - mod environment { + #[tokio::test] + async fn simpool_insufficient_1_sapling_to_transparent() { + fixtures::shpool_to_pool_insufficient_error::( + Sapling, + Transparent, + 1, + ) + .await; + } + #[tokio::test] + async fn simpool_insufficient_1_sapling_to_sapling() { + fixtures::shpool_to_pool_insufficient_error::( + Sapling, + Shielded(Sapling), + 1, + ) + .await; + } + #[tokio::test] + async fn simpool_insufficient_1_sapling_to_orchard() { + fixtures::shpool_to_pool_insufficient_error::( + Sapling, + Shielded(Orchard), + 1, + ) + .await; + } + #[tokio::test] + async fn simpool_insufficient_1_orchard_to_transparent() { + fixtures::shpool_to_pool_insufficient_error::( + Orchard, + Transparent, + 1, + ) + .await; + } + #[tokio::test] + async fn simpool_insufficient_1_orchard_to_sapling() { + fixtures::shpool_to_pool_insufficient_error::( + Orchard, + Shielded(Sapling), + 1, + ) + .await; + } + #[tokio::test] + async fn simpool_insufficient_1_orchard_to_orchard() { + fixtures::shpool_to_pool_insufficient_error::( + Orchard, + Shielded(Orchard), + 1, + ) + .await + } + #[tokio::test] + async fn simpool_insufficient_10_000_sapling_to_transparent() { + fixtures::shpool_to_pool_insufficient_error::( + Sapling, + Transparent, + 10_000, + ) + .await; + } + #[tokio::test] + async fn simpool_insufficient_10_000_sapling_to_sapling() { + fixtures::shpool_to_pool_insufficient_error::( + Sapling, + Shielded(Sapling), + 10_000, + ) + .await; + } + #[tokio::test] + async fn simpool_insufficient_10_000_sapling_to_orchard() { + fixtures::shpool_to_pool_insufficient_error::( + Sapling, + Shielded(Orchard), + 10_000, + ) + .await; + } + #[tokio::test] + async fn simpool_insufficient_10_000_orchard_to_transparent() { + fixtures::shpool_to_pool_insufficient_error::( + Orchard, + Transparent, + 10_000, + ) + .await; + } + #[tokio::test] + async fn simpool_insufficient_10_000_orchard_to_sapling() { + fixtures::shpool_to_pool_insufficient_error::( + Orchard, + Shielded(Sapling), + 10_000, + ) + .await; + } + #[tokio::test] + async fn simpool_insufficient_10_000_orchard_to_orchard() { + fixtures::shpool_to_pool_insufficient_error::( + Orchard, + Shielded(Orchard), + 10_000, + ) + .await; + } + #[tokio::test] + async fn simpool_no_fund_1_000_000_to_transparent() { + fixtures::to_pool_unfunded_error::(Transparent, 1_000_000).await; + } + #[tokio::test] + async fn simpool_no_fund_1_000_000_to_sapling() { + fixtures::to_pool_unfunded_error::(Shielded(Sapling), 1_000_000) + .await; + } + #[tokio::test] + async fn simpool_no_fund_1_000_000_to_orchard() { + fixtures::to_pool_unfunded_error::(Shielded(Orchard), 1_000_000) + .await; + } + mod conduct_chain { use zcash_client_backend::PoolType; use zcash_client_backend::ShieldedProtocol::Sapling; - use zingo_testutils::chain_generics::conduct_chain::ConductChain; - use zingo_testutils::scenarios::setup::ScenarioBuilder; - use zingoconfig::RegtestNetwork; + use zingolib::config::RegtestNetwork; use zingolib::lightclient::LightClient; - use zingolib::wallet::WalletBase; + use zingolib::testutils::chain_generics::conduct_chain::ConductChain; + use zingolib::testutils::scenarios::setup::ScenarioBuilder; + pub(crate) struct LibtonodeEnvironment { regtest_network: RegtestNetwork, scenario_builder: ScenarioBuilder, @@ -232,19 +354,10 @@ mod chain_generics { .await } - async fn create_client(&mut self) -> LightClient { - let zingo_config = self - .scenario_builder + fn zingo_config(&mut self) -> zingolib::config::ZingoConfig { + self.scenario_builder .client_builder - .make_unique_data_dir_and_load_config(self.regtest_network); - LightClient::create_from_wallet_base_async( - WalletBase::FreshEntropy, - &zingo_config, - 0, - false, - ) - .await - .unwrap() + .make_unique_data_dir_and_load_config(self.regtest_network) } async fn bump_chain(&mut self) { diff --git a/libtonode-tests/tests/concrete.rs b/libtonode-tests/tests/concrete.rs index 9cae0789d6..93677ead9d 100644 --- a/libtonode-tests/tests/concrete.rs +++ b/libtonode-tests/tests/concrete.rs @@ -6,39 +6,27 @@ use orchard::tree::MerkleHashOrchard; use sapling_crypto::note_encryption::SaplingDomain; use shardtree::store::memory::MemoryShardStore; use shardtree::ShardTree; -use std::{fs::File, path::Path, time::Duration}; +use std::{path::Path, time::Duration}; use zcash_address::unified::Fvk; use zcash_client_backend::encoding::encode_payment_address; use zcash_primitives::transaction::components::amount::NonNegativeAmount; -use zcash_primitives::zip339::Mnemonic; -use zcash_primitives::{ - consensus::{BlockHeight, Parameters}, - transaction::fees::zip317::MINIMUM_FEE, -}; -use zingo_testutils::lightclient::from_inputs; -use zingo_testutils::{ - self, build_fvk_client, check_client_balances, get_base_address_macro, get_otd, - increase_height_and_wait_for_client, paths::get_cargo_manifest_dir, scenarios, validate_otds, -}; -use zingolib::lightclient::propose::ProposeSendError; +use zcash_primitives::{consensus::BlockHeight, transaction::fees::zip317::MINIMUM_FEE}; +use zingolib::testutils::lightclient::from_inputs; +use zingolib::testutils::{build_fvk_client, increase_height_and_wait_for_client, scenarios}; use zingolib::utils::conversion::address_from_str; +use zingolib::wallet::data::summaries::TransactionSummaryInterface; +use zingolib::wallet::keys::unified::UnifiedKeyStore; +use zingolib::wallet::propose::ProposeSendError; +use zingolib::{check_client_balances, get_base_address_macro, get_otd, validate_otds}; -use zingo_testvectors::{ - block_rewards, - seeds::{CHIMNEY_BETTER_SEED, HOSPITAL_MUSEUM_SEED}, - BASE_HEIGHT, -}; -use zingoconfig::{ChainType, RegtestNetwork, ZingoConfig, MAX_REORG}; +use zingolib::config::{ChainType, RegtestNetwork, MAX_REORG}; +use zingolib::testvectors::{block_rewards, seeds::HOSPITAL_MUSEUM_SEED, BASE_HEIGHT}; use zingolib::{ lightclient::{LightClient, PoolBalances}, utils, wallet::{ data::{COMMITMENT_TREE_LEVELS, MAX_SHARD_LEVEL}, - keys::{ - extended_transparent::ExtendedPrivKey, - unified::{Capability, WalletCapability}, - }, - LightWallet, + keys::unified::WalletCapability, }, }; @@ -74,23 +62,26 @@ fn check_view_capability_bounds( balance: &PoolBalances, watch_wc: &WalletCapability, fvks: &[&Fvk], - ovk: &Fvk, - svk: &Fvk, - tvk: &Fvk, + orchard_fvk: &Fvk, + sapling_fvk: &Fvk, + transparent_fvk: &Fvk, sent_o_value: Option, sent_s_value: Option, sent_t_value: Option, notes: &JsonValue, ) { + let UnifiedKeyStore::View(ufvk) = watch_wc.unified_key_store() else { + panic!("should be viewing key!") + }; //Orchard - if !fvks.contains(&ovk) { - assert!(!watch_wc.orchard.can_view()); + if !fvks.contains(&orchard_fvk) { + assert!(ufvk.orchard().is_none()); assert_eq!(balance.orchard_balance, None); assert_eq!(balance.verified_orchard_balance, None); assert_eq!(balance.unverified_orchard_balance, None); assert_eq!(notes["unspent_orchard_notes"].members().count(), 0); } else { - assert!(watch_wc.orchard.can_view()); + assert!(ufvk.orchard().is_some()); assert_eq!(balance.orchard_balance, sent_o_value); assert_eq!(balance.verified_orchard_balance, sent_o_value); assert_eq!(balance.unverified_orchard_balance, Some(0)); @@ -99,40 +90,50 @@ fn check_view_capability_bounds( assert!((1..=2).contains(&orchard_notes_count)); } //Sapling - if !fvks.contains(&svk) { - assert!(!watch_wc.sapling.can_view()); + if !fvks.contains(&sapling_fvk) { + assert!(ufvk.sapling().is_none()); assert_eq!(balance.sapling_balance, None); assert_eq!(balance.verified_sapling_balance, None); assert_eq!(balance.unverified_sapling_balance, None); assert_eq!(notes["unspent_sapling_notes"].members().count(), 0); } else { - assert!(watch_wc.sapling.can_view()); + assert!(ufvk.sapling().is_some()); assert_eq!(balance.sapling_balance, sent_s_value); assert_eq!(balance.verified_sapling_balance, sent_s_value); assert_eq!(balance.unverified_sapling_balance, Some(0)); assert_eq!(notes["unspent_sapling_notes"].members().count(), 1); } - if !fvks.contains(&tvk) { - assert!(!watch_wc.transparent.can_view()); + if !fvks.contains(&transparent_fvk) { + assert!(ufvk.transparent().is_none()); assert_eq!(balance.transparent_balance, None); assert_eq!(notes["utxos"].members().count(), 0); } else { - assert!(watch_wc.transparent.can_view()); + assert!(ufvk.transparent().is_some()); assert_eq!(balance.transparent_balance, sent_t_value); assert_eq!(notes["utxos"].members().count(), 1); } } mod fast { - use zcash_address::unified::Encoding; - use zcash_client_backend::{PoolType, ShieldedProtocol}; - use zcash_primitives::transaction::components::amount::NonNegativeAmount; + use bip0039::Mnemonic; + use zcash_address::{AddressKind, ZcashAddress}; + use zcash_client_backend::{ + zip321::{Payment, TransactionRequest}, + PoolType, ShieldedProtocol, + }; + use zcash_primitives::transaction::{components::amount::NonNegativeAmount, TxId}; use zingo_status::confirmation_status::ConfirmationStatus; - use zingo_testutils::lightclient::from_inputs; - use zingoconfig::ZENNIES_FOR_ZINGO_REGTEST_ADDRESS; + use zingolib::wallet::notes::OutputInterface as _; + use zingolib::{ + config::ZENNIES_FOR_ZINGO_REGTEST_ADDRESS, wallet::data::summaries::SentValueTransfer, + }; use zingolib::{ - utils::conversion::txid_from_hex_encoded_str, - wallet::{data::summaries::ValueTransferKind, notes::ShieldedNoteInterface, WalletBase}, + testutils::lightclient::from_inputs, wallet::data::summaries::SelfSendValueTransfer, + }; + + use zingolib::{ + utils::conversion::txid_from_hex_encoded_str, wallet::data::summaries::ValueTransferKind, + wallet::notes::ShieldedNoteInterface, }; use super::*; @@ -144,11 +145,7 @@ mod fast { recipient .propose_send_all( - address_from_str( - &get_base_address_macro!(&recipient, "unified"), - &recipient.config().chain, - ) - .unwrap(), + address_from_str(&get_base_address_macro!(&recipient, "unified")).unwrap(), true, None, ) @@ -162,17 +159,88 @@ mod fast { let value_transfers = &recipient.value_transfers().await; - assert!(value_transfers - .iter() - .any(|vt| vt.kind() == ValueTransferKind::SendToSelf)); - assert!(value_transfers - .iter() - .any(|vt| vt.kind() == ValueTransferKind::Sent - && vt.recipient_address() == Some(ZENNIES_FOR_ZINGO_REGTEST_ADDRESS))); + dbg!(value_transfers); + + assert!(value_transfers.iter().any(|vt| vt.kind() + == ValueTransferKind::Sent(SentValueTransfer::SendToSelf( + SelfSendValueTransfer::Basic + )))); + assert!(value_transfers.iter().any(|vt| vt.kind() + == ValueTransferKind::Sent(SentValueTransfer::Send) + && vt.recipient_address() == Some(ZENNIES_FOR_ZINGO_REGTEST_ADDRESS))); + } + + pub mod tex { + use super::*; + fn first_taddr_to_tex(client: &LightClient) -> ZcashAddress { + let taddr = ZcashAddress::try_from_encoded( + &client + .wallet + .get_first_address(PoolType::Transparent) + .unwrap(), + ) + .unwrap(); + + let AddressKind::P2pkh(taddr_bytes) = taddr.kind() else { + panic!() + }; + let tex_string = + utils::interpret_taddr_as_tex_addr(*taddr_bytes, &client.config().chain); + // let tex_string = utils::interpret_taddr_as_tex_addr(*taddr_bytes); + + ZcashAddress::try_from_encoded(&tex_string).unwrap() + } + #[tokio::test] + async fn send_to_tex() { + let (ref _regtest_manager, _cph, ref faucet, sender, _txid) = + scenarios::orchard_funded_recipient(5_000_000).await; + + let tex_addr_from_first = first_taddr_to_tex(&faucet); + let payment = vec![Payment::without_memo( + tex_addr_from_first.clone(), + NonNegativeAmount::from_u64(100_000).unwrap(), + )]; + + let transaction_request = TransactionRequest::new(payment).unwrap(); + + let proposal = sender.propose_send(transaction_request).await.unwrap(); + assert_eq!(proposal.steps().len(), 2usize); + let sent_txids_according_to_broadcast = sender + .complete_and_broadcast_stored_proposal() + .await + .unwrap(); + let txids = sender + .wallet + .transactions() + .read() + .await + .transaction_records_by_id + .keys() + .cloned() + .collect::>(); + dbg!(&txids); + dbg!(sent_txids_according_to_broadcast); + assert_eq!( + sender + .wallet + .transactions() + .read() + .await + .transaction_records_by_id + .len(), + 3usize + ); + let val_tranfers = dbg!(sender.value_transfers().await); + // This fails, as we don't scan sends to tex correctly yet + assert_eq!( + val_tranfers.0[2].recipient_address().unwrap(), + tex_addr_from_first.encode() + ); + } } #[tokio::test] - async fn targetted_rescan() { + async fn targeted_rescan() { let (regtest_manager, _cph, _faucet, recipient, txid) = scenarios::orchard_funded_recipient(100_000).await; @@ -227,7 +295,7 @@ mod fast { .find(|tx| tx.value() == 20_000) .unwrap() .status(), - ConfirmationStatus::Pending(BlockHeight::from_u32(5)) // FIXME: mempool blockheight is at chain hieght instead of chain height + 1 + ConfirmationStatus::Mempool(BlockHeight::from_u32(6)) ); increase_height_and_wait_for_client(®test_manager, &recipient, 1) @@ -272,8 +340,8 @@ mod fast { ); assert_eq!(preshield_utxos[0].value, postshield_utxos[0].value); assert_eq!(preshield_utxos[0].script, postshield_utxos[0].script); - assert!(preshield_utxos[0].pending_spent.is_none()); - assert!(postshield_utxos[0].pending_spent.is_some()); + assert!(preshield_utxos[0].spending_tx_status().is_none()); + assert!(postshield_utxos[0].spending_tx_status().is_some()); } // TODO: zip317 - check reorg buffer offset is still accounted for in zip317 sends, fix or delete this test @@ -298,47 +366,6 @@ mod fast { // ); // } - #[tokio::test] - async fn load_and_parse_different_wallet_versions() { - let regtest_network = RegtestNetwork::all_upgrades_active(); - let (_sap_wallet, _sap_path, sap_dir) = - zingo_testutils::get_wallet_nym("sap_only").unwrap(); - let (_loaded_wallet, _) = - zingo_testutils::load_wallet(sap_dir, ChainType::Regtest(regtest_network)).await; - } - - #[tokio::test] - async fn list_transactions_include_foreign() { - let wallet_nym = format!( - "{}/tests/data/wallets/missing_data_test/zingo-wallet.dat", - get_cargo_manifest_dir().to_string_lossy() - ); - let wallet_path = Path::new(&wallet_nym); - let wallet_dir = wallet_path.parent().unwrap(); - let (wallet, config) = - zingo_testutils::load_wallet(wallet_dir.to_path_buf(), ChainType::Mainnet).await; - let client = LightClient::create_from_wallet_async(wallet, config) - .await - .unwrap(); - let transactions = client.do_list_transactions().await[0].clone(); - //env_logger::init(); - let expected_consumer_ui_note = r#"{ - "amount": 100000, - "memo": "Enviado desde YWallet, Enviado desde YWallet", - "block_height": 2060028, - "pending": false, - "datetime": 1682127442, - "position": 0, - "txid": "d93fbb42a101ac148b4e610eea1fe519c0131b17d49af53f29b5e35a778145cb", - "zec_price": null, - "address": "u1n5zgv8c9px4hfmq7cr9f9t0av6q9nj5dwca9w0z9jxegut65gxs2y4qnx7ppng6k2hyt0asyycqrywalzyasxu2302xt4spfqnkh25nevr3h9exc3clh9tfpr5hyhc9dwee50l0cxm7ajun5xs9ycqhlw8rd39jql8z5zlv9hw4q8azcgpv04dez5547geuvyh8pfzezpw52cg2qknm" -}"#; - assert_eq!( - expected_consumer_ui_note, - json::stringify_pretty(transactions, 2) - ); - } - #[tokio::test] async fn zcashd_sapling_commitment_tree() { // TODO: Make this test assert something, what is this a test of? @@ -402,77 +429,6 @@ mod fast { //dbg!(std::process::Command::new("grpcurl").args(["-plaintext", "127.0.0.1:9067"])); } - #[tokio::test] - async fn unspent_notes_are_not_saved() { - let regtest_network = RegtestNetwork::all_upgrades_active(); - let (regtest_manager, _cph, faucet, recipient) = scenarios::faucet_recipient( - PoolType::Shielded(ShieldedProtocol::Sapling), - regtest_network, - ) - .await; - zingo_testutils::increase_height_and_wait_for_client(®test_manager, &faucet, 1) - .await - .unwrap(); - - check_client_balances!(faucet, o: 0 s: 2_500_000_000u64 t: 0u64); - from_inputs::quick_send( - &faucet, - vec![( - get_base_address_macro!(recipient, "unified").as_str(), - 5_000, - Some("this note never makes it to the wallet! or chain"), - )], - ) - .await - .unwrap(); - - assert_eq!( - faucet.do_list_notes(true).await["unspent_orchard_notes"].len(), - 1 - ); - // Create a new client using the faucet's wallet - - // Create zingo config - let mut wallet_location = regtest_manager.zingo_datadir; - wallet_location.pop(); - wallet_location.push("zingo_client_1"); - let zingo_config = ZingoConfig::build(zingoconfig::ChainType::Regtest(regtest_network)) - .set_wallet_dir(wallet_location.clone()) - .create(); - wallet_location.push("zingo-wallet.dat"); - let read_buffer = File::open(wallet_location.clone()).unwrap(); - - // Create wallet from faucet zingo-wallet.dat - let faucet_wallet = - zingolib::wallet::LightWallet::read_internal(read_buffer, &zingo_config) - .await - .unwrap(); - - // Create client based on config and wallet of faucet - let faucet_copy = - LightClient::create_from_wallet_async(faucet_wallet, zingo_config.clone()) - .await - .unwrap(); - assert_eq!( - &faucet_copy.do_seed_phrase().await.unwrap(), - &faucet.do_seed_phrase().await.unwrap() - ); // Sanity check identity - assert_eq!( - faucet.do_list_notes(true).await["unspent_orchard_notes"].len(), - 1 - ); - assert_eq!( - faucet_copy.do_list_notes(true).await["unspent_orchard_notes"].len(), - 0 - ); - let mut faucet_transactions = faucet.do_list_transactions().await; - faucet_transactions.pop(); - faucet_transactions.pop(); - let mut faucet_copy_transactions = faucet_copy.do_list_transactions().await; - faucet_copy_transactions.pop(); - assert_eq!(faucet_transactions, faucet_copy_transactions); - } - #[tokio::test] async fn diversified_addresses_receive_funds_in_best_pool() { let (regtest_manager, _cph, faucet, recipient) = @@ -488,7 +444,7 @@ mod fast { from_inputs::quick_send(&faucet, address_5000_nonememo_tuples) .await .unwrap(); - zingo_testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 1) + zingolib::testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 1) .await .unwrap(); let balance_b = recipient.do_balance().await; @@ -514,7 +470,7 @@ mod fast { async fn diversification_deterministic_and_coherent() { let (_regtest_manager, _cph, mut client_builder, regtest_network) = scenarios::custom_clients_default().await; - let seed_phrase = zcash_primitives::zip339::Mnemonic::from_entropy([1; 32]) + let seed_phrase = Mnemonic::::from_entropy([1; 32]) .unwrap() .to_string(); let recipient1 = client_builder @@ -603,92 +559,6 @@ mod fast { ); } - #[tokio::test] - async fn reload_wallet_from_buffer() { - // We test that the LightWallet can be read from v28 .dat file - // A testnet wallet initiated with - // --seed "chimney better bulb horror rebuild whisper improve intact letter giraffe brave rib appear bulk aim burst snap salt hill sad merge tennis phrase raise" - // --birthday 0 - // --nosync - // with 3 addresses containing all receivers. - let data = include_bytes!("zingo-wallet-v28.dat"); - - let config = zingoconfig::ZingoConfig::build(ChainType::Testnet).create(); - let mid_wallet = LightWallet::read_internal(&data[..], &config) - .await - .map_err(|e| format!("Cannot deserialize LightWallet version 28 file: {}", e)) - .unwrap(); - - let mid_client = LightClient::create_from_wallet_async(mid_wallet, config.clone()) - .await - .unwrap(); - let mid_buffer = mid_client.export_save_buffer_async().await.unwrap(); - let wallet = LightWallet::read_internal(&mid_buffer[..], &config) - .await - .map_err(|e| format!("Cannot deserialize rebuffered LightWallet: {}", e)) - .unwrap(); - let expected_mnemonic = ( - Mnemonic::from_phrase(CHIMNEY_BETTER_SEED.to_string()).unwrap(), - 0, - ); - assert_eq!(wallet.mnemonic(), Some(&expected_mnemonic)); - - let expected_wc = - WalletCapability::new_from_phrase(&config, &expected_mnemonic.0, expected_mnemonic.1) - .unwrap(); - let wc = wallet.wallet_capability(); - - let Capability::Spend(orchard_sk) = &wc.orchard else { - panic!("Expected Orchard Spending Key"); - }; - assert_eq!( - orchard_sk.to_bytes(), - orchard::keys::SpendingKey::try_from(&expected_wc) - .unwrap() - .to_bytes() - ); - - let Capability::Spend(sapling_sk) = &wc.sapling else { - panic!("Expected Sapling Spending Key"); - }; - assert_eq!( - sapling_sk, - &zcash_client_backend::keys::sapling::ExtendedSpendingKey::try_from(&expected_wc) - .unwrap() - ); - - let Capability::Spend(transparent_sk) = &wc.transparent else { - panic!("Expected transparent extended private key"); - }; - assert_eq!( - transparent_sk, - &ExtendedPrivKey::try_from(&expected_wc).unwrap() - ); - - assert_eq!(wc.addresses().len(), 3); - for addr in wc.addresses().iter() { - assert!(addr.orchard().is_some()); - assert!(addr.sapling().is_some()); - assert!(addr.transparent().is_some()); - } - - let ufvk = wc.ufvk().unwrap(); - let ufvk_string = ufvk.encode(&config.chain.network_type()); - let ufvk_base = WalletBase::Ufvk(ufvk_string.clone()); - let view_wallet = - LightWallet::new(config.clone(), ufvk_base, wallet.get_birthday().await).unwrap(); - let v_wc = view_wallet.wallet_capability(); - let vv = v_wc.ufvk().unwrap(); - let vv_string = vv.encode(&config.chain.network_type()); - assert_eq!(ufvk_string, vv_string); - - let client = LightClient::create_from_wallet_async(wallet, config) - .await - .unwrap(); - let balance = client.do_balance().await; - assert_eq!(balance.orchard_balance, Some(10342837)); - } - #[tokio::test] async fn sync_all_epochs_from_sapling() { let regtest_network = RegtestNetwork::new(1, 1, 3, 5, 7, 9); @@ -795,36 +665,33 @@ mod fast { .first() .unwrap() .value(), - NonNegativeAmount::const_from_u64( - (zingo_testvectors::block_rewards::CANOPY * 4) - expected_fee - ) + NonNegativeAmount::const_from_u64((block_rewards::CANOPY * 4) - expected_fee) ) } } mod slow { + use bip0039::Mnemonic; use orchard::note_encryption::OrchardDomain; use zcash_client_backend::{PoolType, ShieldedProtocol}; use zcash_primitives::{ consensus::NetworkConstants, memo::Memo, transaction::fees::zip317::MARGINAL_FEE, }; use zingo_status::confirmation_status::ConfirmationStatus; - use zingo_testutils::{ + use zingolib::testutils::{ assert_transaction_summary_equality, assert_transaction_summary_exists, lightclient::{from_inputs, get_fees_paid_by_client}, }; - use zingo_testvectors::TEST_TXID; + use zingolib::testvectors::TEST_TXID; use zingolib::{ - lightclient::{ - propose::ProposeSendError::Proposal, send::send_with_proposal::QuickSendError, - }, + lightclient::send::send_with_proposal::QuickSendError, wallet::{ data::{ - summaries::{OrchardNoteSummary, SpendStatus, TransactionSummaryBuilder}, + summaries::{OrchardNoteSummary, SpendSummary, TransactionSummaryBuilder}, OutgoingTxData, }, notes::OutputInterface, transaction_record::{SendType, TransactionKind}, - tx_map_and_maybe_trees::TxMapAndMaybeTreesTraitError, + tx_map::TxMapTraitError, }, }; @@ -847,7 +714,7 @@ mod slow { .await .unwrap(); - zingo_testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 5) + zingolib::testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 5) .await .unwrap(); let _sent_transaction_id = from_inputs::quick_send( @@ -856,7 +723,7 @@ mod slow { ) .await .unwrap(); - zingo_testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 5) + zingolib::testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 5) .await .unwrap(); @@ -872,7 +739,7 @@ mod slow { } #[tokio::test] async fn zero_value_change() { - // 2. Send an incoming transaction to fill the wallet + // 1. Send an incoming transaction to fill the wallet let value = 100_000; let (regtest_manager, _cph, faucet, recipient, _txid) = scenarios::orchard_funded_recipient(value).await; @@ -891,7 +758,7 @@ mod slow { .first() .to_string(); - zingo_testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 5) + zingolib::testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 5) .await .unwrap(); @@ -974,7 +841,7 @@ mod slow { .contains(&position)); // 4. Mine the sent transaction - zingo_testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 1) + zingolib::testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 1) .await .unwrap(); @@ -1017,7 +884,7 @@ mod slow { ); // 5. Mine 50 blocks, witness should still be there - zingo_testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 50) + zingolib::testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 50) .await .unwrap(); let position = recipient @@ -1047,7 +914,7 @@ mod slow { .contains(&position)); // 5. Mine 100 blocks, witness should now disappear - zingo_testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 50) + zingolib::testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 50) .await .unwrap(); let position = recipient @@ -1088,40 +955,7 @@ mod slow { .unwrap() .contains(&position)); } - #[tokio::test] - async fn verify_old_wallet_uses_server_height_in_send() { - // An earlier version of zingolib used the _wallet's_ 'height' when - // constructing transactions. This worked well enough when the - // client completed sync prior to sending, but when we introduced - // interrupting send, it made it immediately obvious that this was - // the wrong height to use! The correct height is the - // "mempool height" which is the server_height + 1 - let (regtest_manager, _cph, faucet, recipient) = - scenarios::faucet_recipient_default().await; - // Ensure that the client has confirmed spendable funds - zingo_testutils::increase_height_and_wait_for_client(®test_manager, &faucet, 5) - .await - .unwrap(); - - // Without sync push server forward 2 blocks - zingo_testutils::increase_server_height(®test_manager, 2).await; - let client_wallet_height = faucet.do_wallet_last_scanned_height().await; - // Verify that wallet is still back at 6. - assert_eq!(client_wallet_height.as_fixed_point_u64(0).unwrap(), 8); - - // Interrupt generating send - from_inputs::quick_send( - &faucet, - vec![( - &get_base_address_macro!(recipient, "unified"), - 10_000, - Some("Interrupting sync!!"), - )], - ) - .await - .unwrap(); - } #[tokio::test] async fn test_scanning_in_watch_only_mode() { // # Scenario: @@ -1144,7 +978,7 @@ mod slow { let original_recipient = client_builder .build_client(HOSPITAL_MUSEUM_SEED.to_string(), 0, false, regtest_network) .await; - let zingo_config = zingoconfig::load_clientconfig( + let zingo_config = zingolib::config::load_clientconfig( client_builder.server_id, Some(client_builder.zingo_datadir), ChainType::Regtest(regtest_network), @@ -1163,14 +997,14 @@ mod slow { (recipient_unified.as_str(), 3_000u64, None), ]; // 1. fill wallet with a coinbase transaction by syncing faucet with 1-block increase - zingo_testutils::increase_height_and_wait_for_client(®test_manager, &faucet, 1) + zingolib::testutils::increase_height_and_wait_for_client(®test_manager, &faucet, 1) .await .unwrap(); // 2. send a transaction containing all types of outputs from_inputs::quick_send(&faucet, addr_amount_memos) .await .unwrap(); - zingo_testutils::increase_height_and_wait_for_client( + zingolib::testutils::increase_height_and_wait_for_client( ®test_manager, &original_recipient, 1, @@ -1192,7 +1026,7 @@ mod slow { // Extract viewing keys let wallet_capability = original_recipient.wallet.wallet_capability().clone(); let [o_fvk, s_fvk, t_fvk] = - zingo_testutils::build_fvks_from_wallet_capability(&wallet_capability); + zingolib::testutils::build_fvks_from_wallet_capability(&wallet_capability); let fvks_sets = [ vec![&o_fvk], vec![&s_fvk], @@ -1233,12 +1067,12 @@ mod slow { assert!(matches!( from_inputs::quick_send( &watch_client, - vec![(zingo_testvectors::EXT_TADDR, 1000, None)] + vec![(zingolib::testvectors::EXT_TADDR, 1000, None)] ) .await, Err(QuickSendError::ProposeSend(ProposeSendError::Proposal( zcash_client_backend::data_api::error::Error::DataSource( - TxMapAndMaybeTreesTraitError::NoSpendCapability + TxMapTraitError::NoSpendCapability ) ))) )); @@ -1257,7 +1091,7 @@ mod slow { .await .unwrap(); - zingo_testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 1) + zingolib::testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 1) .await .unwrap(); recipient.do_sync(true).await.unwrap(); @@ -1272,7 +1106,7 @@ mod slow { let sent_value = 20_000; let sent_transaction_error = from_inputs::quick_send( &recipient, - vec![(zingo_testvectors::EXT_TADDR, sent_value, None)], + vec![(zingolib::testvectors::EXT_TADDR, sent_value, None)], ) .await .unwrap_err(); @@ -1302,11 +1136,11 @@ mod slow { ) .await .unwrap(); - zingo_testutils::increase_height_and_wait_for_client(regtest_manager, recipient, 1) + zingolib::testutils::increase_height_and_wait_for_client(regtest_manager, recipient, 1) .await .unwrap(); recipient.quick_shield().await.unwrap(); - zingo_testutils::increase_height_and_wait_for_client(regtest_manager, recipient, 1) + zingolib::testutils::increase_height_and_wait_for_client(regtest_manager, recipient, 1) .await .unwrap(); println!( @@ -1343,7 +1177,7 @@ mod slow { ) .await .unwrap(); - zingo_testutils::increase_height_and_wait_for_client(®test_manager, &faucet, 1) + zingolib::testutils::increase_height_and_wait_for_client(®test_manager, &faucet, 1) .await .unwrap(); let list = faucet.do_list_transactions().await; @@ -1393,7 +1227,7 @@ mod slow { .fee(None) .orchard_notes(vec![OrchardNoteSummary::from_parts( recipient_initial_funds, - SpendStatus::Spent( + SpendSummary::Spent( utils::conversion::txid_from_hex_encoded_str(TEST_TXID).unwrap(), ), Some(0), @@ -1417,7 +1251,7 @@ mod slow { ) .await .unwrap(); - zingo_testutils::increase_height_and_wait_for_client(regtest_manager, &recipient, 1) + zingolib::testutils::increase_height_and_wait_for_client(regtest_manager, &recipient, 1) .await .unwrap(); let summary_external_sapling = TransactionSummaryBuilder::new() @@ -1431,7 +1265,7 @@ mod slow { .fee(Some(20_000)) .orchard_notes(vec![OrchardNoteSummary::from_parts( 99_960_000, - SpendStatus::PendingSpent( + SpendSummary::TransmittedSpent( utils::conversion::txid_from_hex_encoded_str(TEST_TXID).unwrap(), ), Some(0), @@ -1452,7 +1286,8 @@ mod slow { let first_send_to_transparent = 20_000; let summary_external_transparent = TransactionSummaryBuilder::new() .blockheight(BlockHeight::from_u32(7)) - .status(ConfirmationStatus::Pending(BlockHeight::from_u32(7))) + // We're not monitoring the mempool for this test + .status(ConfirmationStatus::Transmitted(BlockHeight::from_u32(7))) .datetime(0) .txid(utils::conversion::txid_from_hex_encoded_str(TEST_TXID).unwrap()) .value(first_send_to_transparent) @@ -1461,7 +1296,7 @@ mod slow { .fee(Some(15_000)) .orchard_notes(vec![OrchardNoteSummary::from_parts( 99_925_000, - SpendStatus::Unspent, + SpendSummary::Unspent, Some(0), None, )]) @@ -1518,7 +1353,7 @@ mod slow { Some(0) ); - zingo_testutils::increase_height_and_wait_for_client(regtest_manager, &faucet, 1) + zingolib::testutils::increase_height_and_wait_for_client(regtest_manager, &faucet, 1) .await .unwrap(); @@ -1534,7 +1369,7 @@ mod slow { .fee(None) .orchard_notes(vec![OrchardNoteSummary::from_parts( recipient_second_funding, - SpendStatus::Spent( + SpendSummary::Spent( utils::conversion::txid_from_hex_encoded_str(TEST_TXID).unwrap(), ), Some(0), @@ -1555,7 +1390,7 @@ mod slow { ) .await .unwrap(); - zingo_testutils::increase_height_and_wait_for_client(regtest_manager, &recipient, 1) + zingolib::testutils::increase_height_and_wait_for_client(regtest_manager, &recipient, 1) .await .unwrap(); @@ -1572,7 +1407,7 @@ mod slow { .fee(Some(15_000)) .orchard_notes(vec![OrchardNoteSummary::from_parts( 965_000, - SpendStatus::Spent( + SpendSummary::Spent( utils::conversion::txid_from_hex_encoded_str(TEST_TXID).unwrap(), ), Some(0), @@ -1612,7 +1447,7 @@ mod slow { .fee(Some(20_000)) .orchard_notes(vec![OrchardNoteSummary::from_parts( 99_885_000, - SpendStatus::Unspent, + SpendSummary::Unspent, Some(0), None, )]) @@ -1636,7 +1471,7 @@ mod slow { ) .await .unwrap(); - zingo_testutils::increase_height_and_wait_for_client(regtest_manager, &recipient, 1) + zingolib::testutils::increase_height_and_wait_for_client(regtest_manager, &recipient, 1) .await .unwrap(); @@ -1653,7 +1488,7 @@ mod slow { .fee(Some(15_000)) .orchard_notes(vec![OrchardNoteSummary::from_parts( 930_000, - SpendStatus::Unspent, + SpendSummary::Unspent, Some(0), None, )]) @@ -1677,7 +1512,7 @@ mod slow { ) .await .unwrap(); - zingo_testutils::increase_height_and_wait_for_client(regtest_manager, &recipient, 1) + zingolib::testutils::increase_height_and_wait_for_client(regtest_manager, &recipient, 1) .await .unwrap(); @@ -1735,7 +1570,7 @@ mod slow { .unwrap(); let orch_change = block_rewards::CANOPY - (faucet_to_recipient_amount + u64::from(MINIMUM_FEE)); - zingo_testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 1) + zingolib::testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 1) .await .unwrap(); faucet.do_sync(true).await.unwrap(); @@ -1764,7 +1599,7 @@ mod slow { ) .await .unwrap(); - zingo_testutils::increase_height_and_wait_for_client(®test_manager, &faucet, 1) + zingolib::testutils::increase_height_and_wait_for_client(®test_manager, &faucet, 1) .await .unwrap(); recipient.do_sync(true).await.unwrap(); @@ -1804,7 +1639,7 @@ mod slow { ) .await .unwrap(); - zingo_testutils::increase_height_and_wait_for_client(®test_manager, &faucet, 1) + zingolib::testutils::increase_height_and_wait_for_client(®test_manager, &faucet, 1) .await .unwrap(); let balance = faucet.do_balance().await; @@ -1877,7 +1712,7 @@ mod slow { ) .await .unwrap(); - zingo_testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 1) + zingolib::testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 1) .await .unwrap(); let recipient_taddr = get_base_address_macro!(recipient, "transparent"); @@ -1891,7 +1726,7 @@ mod slow { ) .await .unwrap(); - zingo_testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 1) + zingolib::testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 1) .await .unwrap(); from_inputs::quick_send( @@ -1923,7 +1758,7 @@ mod slow { ) .await .unwrap(); - zingo_testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 1) + zingolib::testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 1) .await .unwrap(); println!( @@ -1958,13 +1793,13 @@ mod slow { scenarios::faucet_recipient_default().await; // Give the faucet a block reward - zingo_testutils::increase_height_and_wait_for_client(®test_manager, &faucet, 1) + zingolib::testutils::increase_height_and_wait_for_client(®test_manager, &faucet, 1) .await .unwrap(); let value = 100_000; // Send some sapling value to the recipient - let txid = zingo_testutils::send_value_between_clients_and_sync( + let txid = zingolib::testutils::send_value_between_clients_and_sync( ®test_manager, &faucet, &recipient, @@ -1985,7 +1820,7 @@ mod slow { .first() .to_string(); - zingo_testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 1) + zingolib::testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 1) .await .unwrap(); // 5. Check the transaction list to make sure we got all transactions @@ -2023,7 +1858,7 @@ mod slow { .unwrap() .first() .to_string(); - zingo_testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 1) + zingolib::testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 1) .await .unwrap(); @@ -2100,7 +1935,6 @@ mod slow { sent_transaction_id ); assert!(notes["pending_sapling_notes"][0]["spent"].is_null()); - assert!(notes["pending_sapling_notes"][0]["spent_at_height"].is_null()); // Check transaction list let list = recipient.do_list_transactions().await; @@ -2135,7 +1969,7 @@ mod slow { ); // 6. Mine the sent transaction - zingo_testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 1) + zingolib::testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 1) .await .unwrap(); @@ -2191,7 +2025,7 @@ mod slow { ) .await .unwrap(); - zingo_testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 1) + zingolib::testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 1) .await .unwrap(); check_client_balances!(recipient, o: for_orchard s: for_sapling t: 0 ); @@ -2206,7 +2040,7 @@ mod slow { ) .await .unwrap(); - zingo_testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 1) + zingolib::testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 1) .await .unwrap(); let remaining_orchard = for_orchard - (6 * fee); @@ -2239,7 +2073,7 @@ mod slow { ) .await .unwrap(); - zingo_testutils::increase_height_and_wait_for_client(regtest_manager, recipient, 10) + zingolib::testutils::increase_height_and_wait_for_client(regtest_manager, recipient, 10) .await .unwrap(); from_inputs::quick_send( @@ -2248,7 +2082,7 @@ mod slow { ) .await .unwrap(); - zingo_testutils::increase_height_and_wait_for_client(regtest_manager, recipient, 10) + zingolib::testutils::increase_height_and_wait_for_client(regtest_manager, recipient, 10) .await .unwrap(); faucet.do_sync(false).await.unwrap(); @@ -2294,9 +2128,13 @@ mod slow { .unwrap() .first(), ); - zingo_testutils::increase_height_and_wait_for_client(®test_manager, &faucet, 1) - .await - .unwrap(); + zingolib::testutils::increase_height_and_wait_for_client( + ®test_manager, + &faucet, + 1, + ) + .await + .unwrap(); } let nom_txid = &txids[0]; @@ -2331,7 +2169,7 @@ mod slow { .first(); // TODO: This chain height bump should be unnecessary. I think removing // this increase_height call reveals a bug! - zingo_testutils::increase_height_and_wait_for_client(®test_manager, &faucet, 1) + zingolib::testutils::increase_height_and_wait_for_client(®test_manager, &faucet, 1) .await .unwrap(); let external_send_txid_no_memo_ref = &external_send_txid_no_memo; @@ -2353,7 +2191,7 @@ mod slow { ) .await .unwrap(); - zingo_testutils::increase_height_and_wait_for_client(regtest_manager, recipient, 1) + zingolib::testutils::increase_height_and_wait_for_client(regtest_manager, recipient, 1) .await .unwrap(); let pre_rescan_transactions = recipient.do_list_transactions().await; @@ -2385,7 +2223,7 @@ mod slow { ) .await .unwrap(); - zingo_testutils::increase_height_and_wait_for_client(regtest_manager, recipient, 1) + zingolib::testutils::increase_height_and_wait_for_client(regtest_manager, recipient, 1) .await .unwrap(); let pre_rescan_transactions = recipient.do_list_transactions().await; @@ -2413,7 +2251,7 @@ mod slow { // * sends back to the original sender's UA let (regtest_manager, _cph, faucet, recipient) = scenarios::faucet_recipient_default().await; - zingo_testutils::increase_height_and_wait_for_client(®test_manager, &faucet, 5) + zingolib::testutils::increase_height_and_wait_for_client(®test_manager, &faucet, 5) .await .unwrap(); @@ -2437,7 +2275,7 @@ mod slow { .await .unwrap(); - zingo_testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 5) + zingolib::testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 5) .await .unwrap(); // We know that the largest single note that 2 received from 1 was 30_000, for 2 to send @@ -2495,7 +2333,7 @@ mod slow { /* assert_eq!(non_change_note_values.iter().sum::(), 10000u64); - zingo_testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 5) + zingolib::testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 5) .await .unwrap(); let client_2_post_transaction_notes = recipient.do_list_notes(false).await; @@ -2550,7 +2388,7 @@ mod slow { ); // Put some transactions unrelated to the recipient (faucet->faucet) on-chain, to get some clutter for _ in 0..5 { - zingo_testutils::send_value_between_clients_and_sync( + zingolib::testutils::send_value_between_clients_and_sync( ®test_manager, &faucet, &faucet, @@ -2563,7 +2401,7 @@ mod slow { let sent_to_self = 10; // Send recipient->recipient, to make tree equality check at the end simpler - zingo_testutils::send_value_between_clients_and_sync( + zingolib::testutils::send_value_between_clients_and_sync( ®test_manager, &recipient, &recipient, @@ -2721,7 +2559,7 @@ mod slow { assert_eq!(transactions_before.pretty(2), transactions_after.pretty(2)); // 6. Mine 10 blocks, the pending transaction should still be there. - zingo_testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 10) + zingolib::testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 10) .await .unwrap(); assert_eq!(recipient.wallet.last_synced_height().await, 21); @@ -2746,7 +2584,7 @@ mod slow { assert_eq!(transactions.len(), 3); // 7. Mine 100 blocks, so the mempool expires - zingo_testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 100) + zingolib::testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 100) .await .unwrap(); assert_eq!(recipient.wallet.last_synced_height().await, 121); @@ -2798,7 +2636,7 @@ mod slow { server_orchard_shardtree .insert_frontier_nodes( server_orchard_front.unwrap(), - zingo_testutils::incrementalmerkletree::Retention::Marked, + zingolib::testutils::incrementalmerkletree::Retention::Marked, ) .unwrap(); assert_eq!( @@ -2825,7 +2663,7 @@ mod slow { assert_eq!(bal.verified_orchard_balance.unwrap(), value); // 3. Mine 10 blocks - zingo_testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 10) + zingolib::testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 10) .await .unwrap(); let bal = recipient.do_balance().await; @@ -2857,7 +2695,7 @@ mod slow { assert_eq!(bal.unverified_orchard_balance.unwrap(), new_bal); // 5. Mine the pending block, making the funds verified and spendable. - zingo_testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 10) + zingolib::testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 10) .await .unwrap(); @@ -2867,173 +2705,6 @@ mod slow { assert_eq!(bal.verified_orchard_balance.unwrap(), new_bal); assert_eq!(bal.unverified_orchard_balance.unwrap(), 0); } - #[tokio::test] - async fn load_old_wallet_at_reorged_height() { - let regtest_network = RegtestNetwork::all_upgrades_active(); - let (ref regtest_manager, cph, ref faucet) = scenarios::faucet( - PoolType::Shielded(ShieldedProtocol::Orchard), - regtest_network, - ) - .await; - println!("Shutting down initial zcd/lwd unneeded processes"); - drop(cph); - - let zcd_datadir = ®test_manager.zcashd_data_dir; - let zingo_datadir = ®test_manager.zingo_datadir; - // This test is the unique consumer of: - // zingo-testutils/old_wallet_reorg_test_wallet - let cached_data_dir = get_cargo_manifest_dir() - .parent() - .unwrap() - .join("zingo-testvectors") - .join("old_wallet_reorg_test_wallet"); - let zcd_source = cached_data_dir - .join("zcashd") - .join(".") - .to_string_lossy() - .to_string(); - let zcd_dest = zcd_datadir.to_string_lossy().to_string(); - std::process::Command::new("rm") - .arg("-r") - .arg(&zcd_dest) - .output() - .expect("directory rm failed"); - std::fs::DirBuilder::new() - .create(&zcd_dest) - .expect("Dir recreate failed"); - std::process::Command::new("cp") - .arg("-r") - .arg(zcd_source) - .arg(zcd_dest) - .output() - .expect("directory copy failed"); - let zingo_source = cached_data_dir - .join("zingo-wallet.dat") - .to_string_lossy() - .to_string(); - let zingo_dest = zingo_datadir.to_string_lossy().to_string(); - std::process::Command::new("cp") - .arg("-f") - .arg(zingo_source) - .arg(&zingo_dest) - .output() - .expect("wallet copy failed"); - let _cph = regtest_manager.launch(false).unwrap(); - println!("loading wallet"); - let (wallet, conf) = - zingo_testutils::load_wallet(zingo_dest.into(), ChainType::Regtest(regtest_network)) - .await; - println!("setting uri"); - *conf.lightwalletd_uri.write().unwrap() = faucet.get_server_uri(); - println!("creating lightclient"); - let recipient = LightClient::create_from_wallet_async(wallet, conf) - .await - .unwrap(); - println!( - "pre-sync transactions: {}", - recipient.do_list_transactions().await.pretty(2) - ); - let expected_pre_sync_transactions = r#"[ - { - "block_height": 3, - "pending": false, - "datetime": 1692212261, - "position": 0, - "txid": "7a9d41caca143013ebd2f710e4dad04f0eb9f0ae98b42af0f58f25c61a9d439e", - "amount": 100000, - "zec_price": null, - "address": "uregtest1wdukkmv5p5n824e8ytnc3m6m77v9vwwl7hcpj0wangf6z23f9x0fnaen625dxgn8cgp67vzw6swuar6uwp3nqywfvvkuqrhdjffxjfg644uthqazrtxhrgwac0a6ujzgwp8y9cwthjeayq8r0q6786yugzzyt9vevxn7peujlw8kp3vf6d8p4fvvpd8qd5p7xt2uagelmtf3vl6w3u8", - "memo": null - }, - { - "block_height": 8, - "pending": false, - "datetime": 1692212266, - "position": 0, - "txid": "122f8ab8dc5483e36256a4fbd7ff8d60eb7196670716a6690f9215f1c2a4d841", - "amount": 50000, - "zec_price": null, - "address": "uregtest1wdukkmv5p5n824e8ytnc3m6m77v9vwwl7hcpj0wangf6z23f9x0fnaen625dxgn8cgp67vzw6swuar6uwp3nqywfvvkuqrhdjffxjfg644uthqazrtxhrgwac0a6ujzgwp8y9cwthjeayq8r0q6786yugzzyt9vevxn7peujlw8kp3vf6d8p4fvvpd8qd5p7xt2uagelmtf3vl6w3u8", - "memo": null - }, - { - "block_height": 9, - "pending": false, - "datetime": 1692212299, - "position": 0, - "txid": "0a014017add7dc9eb57ada3e70f905c9dce610ef055e135b03f4907dd5dc99a4", - "amount": 30000, - "zec_price": null, - "address": "uregtest1wdukkmv5p5n824e8ytnc3m6m77v9vwwl7hcpj0wangf6z23f9x0fnaen625dxgn8cgp67vzw6swuar6uwp3nqywfvvkuqrhdjffxjfg644uthqazrtxhrgwac0a6ujzgwp8y9cwthjeayq8r0q6786yugzzyt9vevxn7peujlw8kp3vf6d8p4fvvpd8qd5p7xt2uagelmtf3vl6w3u8", - "memo": null - } -]"#; - assert_eq!( - expected_pre_sync_transactions, - recipient.do_list_transactions().await.pretty(2) - ); - recipient.do_sync(false).await.unwrap(); - let expected_post_sync_transactions = r#"[ - { - "block_height": 3, - "pending": false, - "datetime": 1692212261, - "position": 0, - "txid": "7a9d41caca143013ebd2f710e4dad04f0eb9f0ae98b42af0f58f25c61a9d439e", - "amount": 100000, - "zec_price": null, - "address": "uregtest1wdukkmv5p5n824e8ytnc3m6m77v9vwwl7hcpj0wangf6z23f9x0fnaen625dxgn8cgp67vzw6swuar6uwp3nqywfvvkuqrhdjffxjfg644uthqazrtxhrgwac0a6ujzgwp8y9cwthjeayq8r0q6786yugzzyt9vevxn7peujlw8kp3vf6d8p4fvvpd8qd5p7xt2uagelmtf3vl6w3u8", - "memo": null - }, - { - "block_height": 8, - "pending": false, - "datetime": 1692212266, - "position": 0, - "txid": "122f8ab8dc5483e36256a4fbd7ff8d60eb7196670716a6690f9215f1c2a4d841", - "amount": 50000, - "zec_price": null, - "address": "uregtest1wdukkmv5p5n824e8ytnc3m6m77v9vwwl7hcpj0wangf6z23f9x0fnaen625dxgn8cgp67vzw6swuar6uwp3nqywfvvkuqrhdjffxjfg644uthqazrtxhrgwac0a6ujzgwp8y9cwthjeayq8r0q6786yugzzyt9vevxn7peujlw8kp3vf6d8p4fvvpd8qd5p7xt2uagelmtf3vl6w3u8", - "memo": null - } -]"#; - assert_eq!( - expected_post_sync_transactions, - recipient.do_list_transactions().await.pretty(2) - ); - let expected_post_sync_balance = PoolBalances { - sapling_balance: Some(0), - verified_sapling_balance: Some(0), - spendable_sapling_balance: Some(0), - unverified_sapling_balance: Some(0), - orchard_balance: Some(150000), - verified_orchard_balance: Some(150000), - spendable_orchard_balance: Some(150000), - unverified_orchard_balance: Some(0), - transparent_balance: Some(0), - }; - assert_eq!(expected_post_sync_balance, recipient.do_balance().await); - let missing_output_index = from_inputs::quick_send( - &recipient, - vec![(&get_base_address_macro!(faucet, "unified"), 14000, None)], - ) - .await; - if let Err(QuickSendError::ProposeSend(Proposal( - zcash_client_backend::data_api::error::Error::DataSource(zingolib::wallet::tx_map_and_maybe_trees::TxMapAndMaybeTreesTraitError::InputSource( - zingolib::wallet::transaction_records_by_id::trait_inputsource::InputSourceError::MissingOutputIndexes(output_error) - )), - ))) = missing_output_index { - let txid1 = utils::conversion::txid_from_hex_encoded_str("122f8ab8dc5483e36256a4fbd7ff8d60eb7196670716a6690f9215f1c2a4d841").unwrap(); - let txid2 = utils::conversion::txid_from_hex_encoded_str("7a9d41caca143013ebd2f710e4dad04f0eb9f0ae98b42af0f58f25c61a9d439e").unwrap(); - let expected_txids = vec![txid1, txid2]; - // in case the txids are in reverse order - let missing_index_txids: Vec = output_error.into_iter().map(|(txid, _)| txid).collect(); - if missing_index_txids != expected_txids { - let expected_txids = vec![txid2, txid1]; - assert!(missing_index_txids == expected_txids, "{:?}\n\n{:?}", missing_index_txids, expected_txids); - } - }; - } /// An arbitrary number of diversified addresses may be generated /// from a seed. If the wallet is subsequently lost-or-destroyed /// wallet-regeneration-from-seed (sprouting) doesn't regenerate @@ -3045,7 +2716,7 @@ mod slow { scenarios::custom_clients_default().await; let faucet = client_builder.build_faucet(false, regtest_network).await; faucet.do_sync(false).await.unwrap(); - let seed_phrase_of_recipient1 = zcash_primitives::zip339::Mnemonic::from_entropy([1; 32]) + let seed_phrase_of_recipient1 = Mnemonic::::from_entropy([1; 32]) .unwrap() .to_string(); let recipient1 = client_builder @@ -3083,7 +2754,7 @@ mod slow { ) .await .unwrap(); - zingo_testutils::increase_height_and_wait_for_client(®test_manager, &faucet, 1) + zingolib::testutils::increase_height_and_wait_for_client(®test_manager, &faucet, 1) .await .unwrap(); recipient1.do_sync(true).await.unwrap(); @@ -3149,7 +2820,7 @@ mod slow { .await .unwrap(); let sender_balance = faucet.do_balance().await; - zingo_testutils::increase_height_and_wait_for_client(®test_manager, &faucet, 1) + zingolib::testutils::increase_height_and_wait_for_client(®test_manager, &faucet, 1) .await .unwrap(); @@ -3177,12 +2848,12 @@ mod slow { let pmc_sapling = get_base_address_macro!(pool_migration_client, "sapling"); let pmc_unified = get_base_address_macro!(pool_migration_client, "unified"); // Ensure that the client has confirmed spendable funds - zingo_testutils::increase_height_and_wait_for_client(®test_manager, &faucet, 3) + zingolib::testutils::increase_height_and_wait_for_client(®test_manager, &faucet, 3) .await .unwrap(); macro_rules! bump_and_check_pmc { (o: $o:tt s: $s:tt t: $t:tt) => { - zingo_testutils::increase_height_and_wait_for_client(®test_manager, &pool_migration_client, 1).await.unwrap(); + zingolib::testutils::increase_height_and_wait_for_client(®test_manager, &pool_migration_client, 1).await.unwrap(); check_client_balances!(pool_migration_client, o:$o s:$s t:$t); }; } @@ -3223,12 +2894,16 @@ mod slow { let pmc_sapling = get_base_address_macro!(client, "sapling"); let pmc_unified = get_base_address_macro!(client, "unified"); // Ensure that the client has confirmed spendable funds - zingo_testutils::increase_height_and_wait_for_client(®test_manager, &sapling_faucet, 1) - .await - .unwrap(); + zingolib::testutils::increase_height_and_wait_for_client( + ®test_manager, + &sapling_faucet, + 1, + ) + .await + .unwrap(); macro_rules! bump_and_check { (o: $o:tt s: $s:tt t: $t:tt) => { - zingo_testutils::increase_height_and_wait_for_client(®test_manager, &client, 1).await.unwrap(); + zingolib::testutils::increase_height_and_wait_for_client(®test_manager, &client, 1).await.unwrap(); check_client_balances!(client, o:$o s:$s t:$t); }; } @@ -3380,13 +3055,6 @@ mod slow { Ok(_) => panic!(), Err(QuickSendError::ProposeSend(proposesenderror)) => match proposesenderror { ProposeSendError::Proposal(insufficient) => match insufficient { - zcash_client_backend::data_api::error::Error::DataSource(_) => panic!(), - zcash_client_backend::data_api::error::Error::CommitmentTree(_) => panic!(), - zcash_client_backend::data_api::error::Error::NoteSelection(_) => panic!(), - zcash_client_backend::data_api::error::Error::Proposal(_) => panic!(), - zcash_client_backend::data_api::error::Error::ProposalNotSupported => panic!(), - zcash_client_backend::data_api::error::Error::KeyNotRecognized => panic!(), - zcash_client_backend::data_api::error::Error::BalanceError(_) => panic!(), zcash_client_backend::data_api::error::Error::InsufficientFunds { available, required, @@ -3394,20 +3062,7 @@ mod slow { assert_eq!(available, NonNegativeAmount::from_u64(20_000).unwrap()); assert_eq!(required, NonNegativeAmount::from_u64(25_001).unwrap()); } - zcash_client_backend::data_api::error::Error::ScanRequired => panic!(), - zcash_client_backend::data_api::error::Error::Builder(_) => panic!(), - zcash_client_backend::data_api::error::Error::MemoForbidden => panic!(), - zcash_client_backend::data_api::error::Error::UnsupportedChangeType(_) => { - panic!() - } - zcash_client_backend::data_api::error::Error::NoSupportedReceivers(_) => { - panic!() - } - zcash_client_backend::data_api::error::Error::NoSpendingKey(_) => panic!(), - zcash_client_backend::data_api::error::Error::NoteMismatch(_) => panic!(), - zcash_client_backend::data_api::error::Error::AddressNotRecognized(_) => { - panic!() - } + _ => panic!(), }, ProposeSendError::TransactionRequestFailed(_) => panic!(), ProposeSendError::ZeroValueSendAll => panic!(), @@ -3531,7 +3186,7 @@ mod slow { async fn factor_do_shield_to_call_do_send() { let (regtest_manager, __cph, faucet, recipient) = scenarios::faucet_recipient_default().await; - zingo_testutils::increase_height_and_wait_for_client(®test_manager, &faucet, 2) + zingolib::testutils::increase_height_and_wait_for_client(®test_manager, &faucet, 2) .await .unwrap(); from_inputs::quick_send( @@ -3563,7 +3218,7 @@ mod slow { .await .unwrap(); - zingo_testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 5) + zingolib::testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 5) .await .unwrap(); @@ -3592,7 +3247,7 @@ mod slow { .await .unwrap(); - zingo_testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 2) + zingolib::testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 2) .await .unwrap(); let recipient_balance = recipient.do_balance().await; @@ -3628,9 +3283,10 @@ mod slow { 65_000 ); - let loaded_client = zingo_testutils::lightclient::new_client_from_save_buffer(&recipient) - .await - .unwrap(); + let loaded_client = + zingolib::testutils::lightclient::new_client_from_save_buffer(&recipient) + .await + .unwrap(); let loaded_balance = loaded_client.do_balance().await; assert_eq!(loaded_balance.unverified_orchard_balance, Some(0),); check_client_balances!(loaded_client, o: 100_000 s: 0 t: 0 ); @@ -3641,7 +3297,7 @@ mod slow { let (regtest_manager, _cph, faucet, recipient) = scenarios::faucet_recipient_default().await; let base_uaddress = get_base_address_macro!(recipient, "unified"); - zingo_testutils::increase_height_and_wait_for_client(®test_manager, &faucet, 2) + zingolib::testutils::increase_height_and_wait_for_client(®test_manager, &faucet, 2) .await .unwrap(); println!( @@ -3672,7 +3328,7 @@ mod slow { let (regtest_manager, _cph, faucet, recipient, _txid) = scenarios::orchard_funded_recipient(100_000).await; - zingo_testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 1) + zingolib::testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 1) .await .unwrap(); @@ -3693,7 +3349,7 @@ mod slow { .to_string(); // Validate transaction - zingo_testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 1) + zingolib::testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 1) .await .unwrap(); @@ -3721,11 +3377,12 @@ mod slow { assert_eq!(orchard_note.value(), 0); } #[tokio::test] + #[ignore = "test does not correspond to real-world case"] async fn aborted_resync() { let (regtest_manager, _cph, faucet, recipient, _txid) = scenarios::orchard_funded_recipient(500_000).await; - zingo_testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 15) + zingolib::testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 15) .await .unwrap(); @@ -3745,7 +3402,7 @@ mod slow { .first() .to_string(); - zingo_testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 5) + zingolib::testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 5) .await .unwrap(); @@ -3788,7 +3445,7 @@ mod slow { // 5. Now, we'll manually remove some of the blocks in the wallet, pretending that the sync was aborted in the middle. // We'll remove the top 20 blocks, so now the wallet only has the first 3 blocks - recipient.wallet.blocks.write().await.drain(0..20); + recipient.wallet.last_100_blocks.write().await.drain(0..20); assert_eq!(recipient.wallet.last_synced_height().await, 5); // 6. Do a sync again @@ -3865,7 +3522,7 @@ mod slow { let (regtest_manager, _cph, faucet, recipient) = scenarios::faucet_recipient_default().await; for i in 1..4 { - let _ = faucet.do_sync(false).await; + faucet.do_sync(false).await.unwrap(); from_inputs::quick_send( &faucet, vec![(&get_base_address_macro!(recipient, "sapling"), 10_100, None)], @@ -3874,8 +3531,8 @@ mod slow { .unwrap(); let chainwait: u32 = 6; let amount: u64 = u64::from(chainwait * i); - zingo_testutils::increase_server_height(®test_manager, chainwait).await; - let _ = recipient.do_sync(false).await; + zingolib::testutils::increase_server_height(®test_manager, chainwait).await; + recipient.do_sync(false).await.unwrap(); from_inputs::quick_send( &recipient, vec![(&get_base_address_macro!(recipient, "unified"), amount, None)], @@ -3883,7 +3540,7 @@ mod slow { .await .unwrap(); } - zingo_testutils::increase_server_height(®test_manager, 1).await; + zingolib::testutils::increase_server_height(®test_manager, 1).await; let _synciiyur = recipient.do_sync(false).await; // let summ_sim = recipient.list_value_transfers().await; @@ -3893,7 +3550,7 @@ mod slow { dbg!("finished basic sync. restarting for interrupted data"); let timeout = 28; let race_condition = - zingo_testutils::interrupts::sync_with_timeout_millis(&recipient, timeout).await; + zingolib::testutils::interrupts::sync_with_timeout_millis(&recipient, timeout).await; match race_condition { Ok(_) => { println!("synced in less than {} millis ", timeout); @@ -3923,153 +3580,13 @@ mod slow { assert_eq!(bala_sim, bala_syn); } - async fn load_wallet_from_data_and_assert( - data: &[u8], - expected_balance: u64, - num_addresses: usize, - ) { - let config = zingoconfig::ZingoConfig::build(ChainType::Testnet) - .set_lightwalletd_uri( - ("https://zcash.mysideoftheweb.com:19067") - .parse::() - .unwrap(), - ) - .create(); - let wallet = LightWallet::read_internal(data, &config) - .await - .map_err(|e| format!("Cannot deserialize LightWallet file!: {}", e)) - .unwrap(); - - let expected_mnemonic = ( - Mnemonic::from_phrase(CHIMNEY_BETTER_SEED.to_string()).unwrap(), - 0, - ); - assert_eq!(wallet.mnemonic(), Some(&expected_mnemonic)); - - let expected_wc = - WalletCapability::new_from_phrase(&config, &expected_mnemonic.0, expected_mnemonic.1) - .unwrap(); - let wc = wallet.wallet_capability(); - - // We don't want the WalletCapability to impl. `Eq` (because it stores secret keys) - // so we have to compare each component instead - - // Compare Orchard - let Capability::Spend(orchard_sk) = &wc.orchard else { - panic!("Expected Orchard Spending Key"); - }; - assert_eq!( - orchard_sk.to_bytes(), - orchard::keys::SpendingKey::try_from(&expected_wc) - .unwrap() - .to_bytes() - ); - - // Compare Sapling - let Capability::Spend(sapling_sk) = &wc.sapling else { - panic!("Expected Sapling Spending Key"); - }; - assert_eq!( - sapling_sk, - &zcash_client_backend::keys::sapling::ExtendedSpendingKey::try_from(&expected_wc) - .unwrap() - ); - - // Compare transparent - let Capability::Spend(transparent_sk) = &wc.transparent else { - panic!("Expected transparent extended private key"); - }; - assert_eq!( - transparent_sk, - &ExtendedPrivKey::try_from(&expected_wc).unwrap() - ); - - assert_eq!(wc.addresses().len(), num_addresses); - for addr in wc.addresses().iter() { - assert!(addr.orchard().is_some()); - assert!(addr.sapling().is_some()); - assert!(addr.transparent().is_some()); - } - - let client = LightClient::create_from_wallet_async(wallet, config) - .await - .unwrap(); - let balance = client.do_balance().await; - assert_eq!(balance.orchard_balance, Some(expected_balance)); - if expected_balance > 0 { - let _ = from_inputs::quick_send( - &client, - vec![(&get_base_address_macro!(client, "sapling"), 11011, None)], - ) - .await - .unwrap(); - let _ = client.do_sync(true).await.unwrap(); - let _ = from_inputs::quick_send( - &client, - vec![(&get_base_address_macro!(client, "transparent"), 28000, None)], - ) - .await - .unwrap(); - } - } - - #[tokio::test] - async fn load_wallet_from_v26_dat_file() { - // We test that the LightWallet can be read from v26 .dat file - // Changes in version 27: - // - The wallet does not have to have a mnemonic. - // Absence of mnemonic is represented by an empty byte vector in v27. - // v26 serialized wallet is always loaded with `Some(mnemonic)`. - // - The wallet capabilities can be restricted from spending to view-only or none. - // We introduce `Capability` type represent different capability types in v27. - // v26 serialized wallet is always loaded with `Capability::Spend(sk)`. - - // A testnet wallet initiated with - // --seed "chimney better bulb horror rebuild whisper improve intact letter giraffe brave rib appear bulk aim burst snap salt hill sad merge tennis phrase raise" - // with 3 addresses containing all receivers. - // including orchard and sapling transactions - let data = include_bytes!("zingo-wallet-v26.dat"); - - load_wallet_from_data_and_assert(data, 0, 3).await; - } - - #[ignore = "flakey test"] - #[tokio::test] - async fn load_wallet_from_v26_2_dat_file() { - // We test that the LightWallet can be read from v26 .dat file - // Changes in version 27: - // - The wallet does not have to have a mnemonic. - // Absence of mnemonic is represented by an empty byte vector in v27. - // v26 serialized wallet is always loaded with `Some(mnemonic)`. - // - The wallet capabilities can be restricted from spending to view-only or none. - // We introduce `Capability` type represent different capability types in v27. - // v26 serialized wallet is always loaded with `Capability::Spend(sk)`. - - // A testnet wallet initiated with - // --seed "chimney better bulb horror rebuild whisper improve intact letter giraffe brave rib appear bulk aim burst snap salt hill sad merge tennis phrase raise" - // with 3 addresses containing all receivers. - // including orchard and sapling transactions - let data = include_bytes!("zingo-wallet-v26-2.dat"); - - load_wallet_from_data_and_assert(data, 10177826, 1).await; - } - - #[ignore = "flakey test"] - #[tokio::test] - async fn load_wallet_from_v28_dat_file() { - // We test that the LightWallet can be read from v28 .dat file - // --seed "chimney better bulb horror rebuild whisper improve intact letter giraffe brave rib appear bulk aim burst snap salt hill sad merge tennis phrase raise" - // with 3 addresses containing all receivers. - let data = include_bytes!("zingo-wallet-v28.dat"); - - load_wallet_from_data_and_assert(data, 10342837, 3).await; - } } mod basic_transactions { use std::cmp; - use zingo_testutils::{get_base_address_macro, lightclient::from_inputs, scenarios}; + use zingolib::get_base_address_macro; + use zingolib::testutils::{lightclient::from_inputs, scenarios}; #[tokio::test] async fn send_and_sync_with_multiple_notes_no_panic() { @@ -4079,7 +3596,7 @@ mod basic_transactions { let recipient_addr_ua = get_base_address_macro!(recipient, "unified"); let faucet_addr_ua = get_base_address_macro!(faucet, "unified"); - zingo_testutils::generate_n_blocks_return_new_height(®test_manager, 2) + zingolib::testutils::generate_n_blocks_return_new_height(®test_manager, 2) .await .unwrap(); @@ -4092,7 +3609,7 @@ mod basic_transactions { .unwrap(); } - zingo_testutils::generate_n_blocks_return_new_height(®test_manager, 1) + zingolib::testutils::generate_n_blocks_return_new_height(®test_manager, 1) .await .unwrap(); @@ -4103,7 +3620,7 @@ mod basic_transactions { .await .unwrap(); - zingo_testutils::generate_n_blocks_return_new_height(®test_manager, 1) + zingolib::testutils::generate_n_blocks_return_new_height(®test_manager, 1) .await .unwrap(); @@ -4155,7 +3672,7 @@ mod basic_transactions { .first() .to_string(); - zingo_testutils::generate_n_blocks_return_new_height(®test_manager, 1) + zingolib::testutils::generate_n_blocks_return_new_height(®test_manager, 1) .await .unwrap(); @@ -4164,23 +3681,23 @@ mod basic_transactions { println!( "Transaction Inputs:\n{:?}", - zingo_testutils::tx_inputs(&faucet, txid1.as_str()).await + zingolib::testutils::tx_inputs(&faucet, txid1.as_str()).await ); println!( "Transaction Outputs:\n{:?}", - zingo_testutils::tx_outputs(&recipient, txid1.as_str()).await + zingolib::testutils::tx_outputs(&recipient, txid1.as_str()).await ); println!( "Transaction Change:\n{:?}", - zingo_testutils::tx_outputs(&faucet, txid1.as_str()).await + zingolib::testutils::tx_outputs(&faucet, txid1.as_str()).await ); let tx_actions_txid1 = - zingo_testutils::tx_actions(&faucet, Some(&recipient), txid1.as_str()).await; + zingolib::testutils::tx_actions(&faucet, Some(&recipient), txid1.as_str()).await; println!("Transaction Actions:\n{:?}", tx_actions_txid1); let calculated_fee_txid1 = - zingo_testutils::total_tx_value(&faucet, txid1.as_str()).await - 40_000; + zingolib::testutils::total_tx_value(&faucet, txid1.as_str()).await - 40_000; println!("Fee Paid: {}", calculated_fee_txid1); let expected_fee_txid1 = 5000 @@ -4196,23 +3713,23 @@ mod basic_transactions { println!( "Transaction Inputs:\n{:?}", - zingo_testutils::tx_inputs(&faucet, txid2.as_str()).await + zingolib::testutils::tx_inputs(&faucet, txid2.as_str()).await ); println!( "Transaction Outputs:\n{:?}", - zingo_testutils::tx_outputs(&recipient, txid2.as_str()).await + zingolib::testutils::tx_outputs(&recipient, txid2.as_str()).await ); println!( "Transaction Change:\n{:?}", - zingo_testutils::tx_outputs(&faucet, txid2.as_str()).await + zingolib::testutils::tx_outputs(&faucet, txid2.as_str()).await ); let tx_actions_txid2 = - zingo_testutils::tx_actions(&faucet, Some(&recipient), txid2.as_str()).await; + zingolib::testutils::tx_actions(&faucet, Some(&recipient), txid2.as_str()).await; println!("Transaction Actions:\n{:?}", tx_actions_txid2); let calculated_fee_txid2 = - zingo_testutils::total_tx_value(&faucet, txid2.as_str()).await - 40_000; + zingolib::testutils::total_tx_value(&faucet, txid2.as_str()).await - 40_000; println!("Fee Paid: {}", calculated_fee_txid2); let expected_fee_txid2 = 5000 @@ -4228,23 +3745,23 @@ mod basic_transactions { println!( "Transaction Inputs:\n{:?}", - zingo_testutils::tx_inputs(&faucet, txid3.as_str()).await + zingolib::testutils::tx_inputs(&faucet, txid3.as_str()).await ); println!( "Transaction Outputs:\n{:?}", - zingo_testutils::tx_outputs(&recipient, txid3.as_str()).await + zingolib::testutils::tx_outputs(&recipient, txid3.as_str()).await ); println!( "Transaction Change:\n{:?}", - zingo_testutils::tx_outputs(&faucet, txid3.as_str()).await + zingolib::testutils::tx_outputs(&faucet, txid3.as_str()).await ); let tx_actions_txid3 = - zingo_testutils::tx_actions(&faucet, Some(&recipient), txid3.as_str()).await; + zingolib::testutils::tx_actions(&faucet, Some(&recipient), txid3.as_str()).await; println!("Transaction Actions:\n{:?}", tx_actions_txid3); let calculated_fee_txid3 = - zingo_testutils::total_tx_value(&faucet, txid3.as_str()).await - 40_000; + zingolib::testutils::total_tx_value(&faucet, txid3.as_str()).await - 40_000; println!("Fee Paid: {}", calculated_fee_txid3); let expected_fee_txid3 = 5000 @@ -4258,7 +3775,7 @@ mod basic_transactions { assert_eq!(calculated_fee_txid3, expected_fee_txid3 as u64); - let txid4 = zingo_testutils::lightclient::from_inputs::quick_send( + let txid4 = zingolib::testutils::lightclient::from_inputs::quick_send( &recipient, vec![( get_base_address_macro!(faucet, "transparent").as_str(), @@ -4271,7 +3788,7 @@ mod basic_transactions { .first() .to_string(); - zingo_testutils::generate_n_blocks_return_new_height(®test_manager, 1) + zingolib::testutils::generate_n_blocks_return_new_height(®test_manager, 1) .await .unwrap(); @@ -4280,23 +3797,23 @@ mod basic_transactions { println!( "Transaction Inputs:\n{:?}", - zingo_testutils::tx_inputs(&recipient, txid4.as_str()).await + zingolib::testutils::tx_inputs(&recipient, txid4.as_str()).await ); println!( "Transaction Outputs:\n{:?}", - zingo_testutils::tx_outputs(&faucet, txid4.as_str()).await + zingolib::testutils::tx_outputs(&faucet, txid4.as_str()).await ); println!( "Transaction Change:\n{:?}", - zingo_testutils::tx_outputs(&recipient, txid4.as_str()).await + zingolib::testutils::tx_outputs(&recipient, txid4.as_str()).await ); let tx_actions_txid4 = - zingo_testutils::tx_actions(&recipient, Some(&faucet), txid4.as_str()).await; + zingolib::testutils::tx_actions(&recipient, Some(&faucet), txid4.as_str()).await; println!("Transaction Actions:\n{:?}", tx_actions_txid4); let calculated_fee_txid4 = - zingo_testutils::total_tx_value(&recipient, txid4.as_str()).await - 55_000; + zingolib::testutils::total_tx_value(&recipient, txid4.as_str()).await - 55_000; println!("Fee Paid: {}", calculated_fee_txid4); let expected_fee_txid4 = 5000 @@ -4316,7 +3833,7 @@ mod basic_transactions { let (regtest_manager, _cph, faucet, recipient) = scenarios::faucet_recipient_default().await; - let txid1 = zingo_testutils::lightclient::from_inputs::quick_send( + let txid1 = zingolib::testutils::lightclient::from_inputs::quick_send( &faucet, vec![( get_base_address_macro!(recipient, "unified").as_str(), @@ -4329,7 +3846,7 @@ mod basic_transactions { .first() .to_string(); - zingo_testutils::generate_n_blocks_return_new_height(®test_manager, 1) + zingolib::testutils::generate_n_blocks_return_new_height(®test_manager, 1) .await .unwrap(); @@ -4338,22 +3855,23 @@ mod basic_transactions { println!( "Transaction Inputs:\n{:?}", - zingo_testutils::tx_inputs(&faucet, txid1.as_str()).await + zingolib::testutils::tx_inputs(&faucet, txid1.as_str()).await ); println!( "Transaction Outputs:\n{:?}", - zingo_testutils::tx_outputs(&recipient, txid1.as_str()).await + zingolib::testutils::tx_outputs(&recipient, txid1.as_str()).await ); println!( "Transaction Change:\n{:?}", - zingo_testutils::tx_outputs(&faucet, txid1.as_str()).await + zingolib::testutils::tx_outputs(&faucet, txid1.as_str()).await ); let tx_actions_txid1 = - zingo_testutils::tx_actions(&faucet, Some(&recipient), txid1.as_str()).await; + zingolib::testutils::tx_actions(&faucet, Some(&recipient), txid1.as_str()).await; println!("Transaction Actions:\n{:?}", tx_actions_txid1); - let calculated_fee_txid1 = zingo_testutils::total_tx_value(&faucet, txid1.as_str()).await; + let calculated_fee_txid1 = + zingolib::testutils::total_tx_value(&faucet, txid1.as_str()).await; println!("Fee Paid: {}", calculated_fee_txid1); let expected_fee_txid1 = 5000 @@ -4373,7 +3891,7 @@ mod basic_transactions { let (regtest_manager, _cph, faucet, recipient) = scenarios::faucet_recipient_default().await; - zingo_testutils::lightclient::from_inputs::quick_send( + zingolib::testutils::lightclient::from_inputs::quick_send( &faucet, vec![( get_base_address_macro!(recipient, "transparent").as_str(), @@ -4384,7 +3902,7 @@ mod basic_transactions { .await .unwrap(); - zingo_testutils::generate_n_blocks_return_new_height(®test_manager, 1) + zingolib::testutils::generate_n_blocks_return_new_height(®test_manager, 1) .await .unwrap(); @@ -4393,7 +3911,7 @@ mod basic_transactions { let txid1 = recipient.quick_shield().await.unwrap().first().to_string(); - zingo_testutils::generate_n_blocks_return_new_height(®test_manager, 1) + zingolib::testutils::generate_n_blocks_return_new_height(®test_manager, 1) .await .unwrap(); @@ -4402,18 +3920,19 @@ mod basic_transactions { println!( "Transaction Inputs:\n{:?}", - zingo_testutils::tx_inputs(&recipient, txid1.as_str()).await + zingolib::testutils::tx_inputs(&recipient, txid1.as_str()).await ); println!( "Transaction Outputs:\n{:?}", - zingo_testutils::tx_outputs(&recipient, txid1.as_str()).await + zingolib::testutils::tx_outputs(&recipient, txid1.as_str()).await ); - let tx_actions_txid1 = zingo_testutils::tx_actions(&recipient, None, txid1.as_str()).await; + let tx_actions_txid1 = + zingolib::testutils::tx_actions(&recipient, None, txid1.as_str()).await; println!("Transaction Actions:\n{:?}", tx_actions_txid1); let calculated_fee_txid1 = - zingo_testutils::total_tx_value(&recipient, txid1.as_str()).await; + zingolib::testutils::total_tx_value(&recipient, txid1.as_str()).await; println!("Fee Paid: {}", calculated_fee_txid1); let expected_fee_txid1 = 5000 @@ -4427,7 +3946,7 @@ mod basic_transactions { assert_eq!(calculated_fee_txid1, expected_fee_txid1 as u64); - zingo_testutils::lightclient::from_inputs::quick_send( + zingolib::testutils::lightclient::from_inputs::quick_send( &faucet, vec![( get_base_address_macro!(recipient, "transparent").as_str(), @@ -4438,7 +3957,7 @@ mod basic_transactions { .await .unwrap(); - zingo_testutils::generate_n_blocks_return_new_height(®test_manager, 1) + zingolib::testutils::generate_n_blocks_return_new_height(®test_manager, 1) .await .unwrap(); @@ -4449,7 +3968,7 @@ mod basic_transactions { #[ignore = "flake"] #[tokio::test] async fn proxy_server_worky() { - zingo_testutils::check_proxy_server_works().await + zingolib::testutils::check_proxy_server_works().await } // FIXME: does not assert dust was included in the proposal @@ -4496,6 +4015,7 @@ async fn audit_anyp_outputs() { assert_eq!(lapo.len(), 1); } mod send_all { + use super::*; #[tokio::test] async fn toggle_zennies_for_zingo() { @@ -4518,11 +4038,8 @@ mod send_all { increase_height_and_wait_for_client(®test_manager, &recipient, 1) .await .unwrap(); - let external_uaddress = address_from_str( - &get_base_address_macro!(faucet, "unified"), - &faucet.config().chain, - ) - .unwrap(); + let external_uaddress = + address_from_str(&get_base_address_macro!(faucet, "unified")).unwrap(); let expected_balance = NonNegativeAmount::from_u64(initial_funds - zennies_magnitude - expected_fee).unwrap(); assert_eq!( @@ -4582,11 +4099,7 @@ mod send_all { recipient .propose_send_all( - address_from_str( - &get_base_address_macro!(faucet, "sapling"), - &recipient.config().chain, - ) - .unwrap(), + address_from_str(&get_base_address_macro!(faucet, "sapling")).unwrap(), false, None, ) @@ -4624,11 +4137,7 @@ mod send_all { let proposal_error = recipient .propose_send_all( - address_from_str( - &get_base_address_macro!(faucet, "sapling"), - &recipient.config().chain, - ) - .unwrap(), + address_from_str(&get_base_address_macro!(faucet, "sapling")).unwrap(), false, None, ) @@ -4655,11 +4164,7 @@ mod send_all { let proposal_error = recipient .propose_send_all( - address_from_str( - &get_base_address_macro!(faucet, "unified"), - &recipient.config().chain, - ) - .unwrap(), + address_from_str(&get_base_address_macro!(faucet, "unified")).unwrap(), false, None, ) diff --git a/libtonode-tests/tests/data/wallets/v26/202302_release/mainnet/zingo-wallet.dat b/libtonode-tests/tests/data/wallets/v26/202302_release/mainnet/zingo-wallet.dat deleted file mode 100644 index 0d360b81d1..0000000000 Binary files a/libtonode-tests/tests/data/wallets/v26/202302_release/mainnet/zingo-wallet.dat and /dev/null differ diff --git a/libtonode-tests/tests/data/wallets/v26/202302_release/regtest/taddr_only/zingo-wallet.dat b/libtonode-tests/tests/data/wallets/v26/202302_release/regtest/taddr_only/zingo-wallet.dat deleted file mode 100644 index 67758acfd8..0000000000 Binary files a/libtonode-tests/tests/data/wallets/v26/202302_release/regtest/taddr_only/zingo-wallet.dat and /dev/null differ diff --git a/libtonode-tests/tests/shield_transparent.rs b/libtonode-tests/tests/shield_transparent.rs index d776266da5..502bf71c1a 100644 --- a/libtonode-tests/tests/shield_transparent.rs +++ b/libtonode-tests/tests/shield_transparent.rs @@ -1,6 +1,5 @@ -use zingo_testutils::{ - get_base_address_macro, lightclient::from_inputs, scenarios::faucet_recipient_default, -}; +use zingolib::get_base_address_macro; +use zingolib::testutils::{lightclient::from_inputs, scenarios::faucet_recipient_default}; #[tokio::test] #[ignore] @@ -33,7 +32,7 @@ async fn shield_transparent() { serde_json::to_string_pretty(&faucet.do_balance().await).unwrap(), serde_json::to_string_pretty(&recipient.do_balance().await).unwrap(), ); - zingo_testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 1) + zingolib::testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 1) .await .unwrap(); @@ -54,7 +53,7 @@ async fn shield_transparent() { .complete_and_broadcast_stored_proposal() .await .unwrap(); - zingo_testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 1) + zingolib::testutils::increase_height_and_wait_for_client(®test_manager, &recipient, 1) .await .unwrap(); diff --git a/libtonode-tests/tests/sync.rs b/libtonode-tests/tests/sync.rs new file mode 100644 index 0000000000..c7197614c0 --- /dev/null +++ b/libtonode-tests/tests/sync.rs @@ -0,0 +1,68 @@ +use tempfile::TempDir; +use zingo_netutils::GrpcConnector; +use zingo_sync::sync::sync; +use zingolib::{ + config::{construct_lightwalletd_uri, load_clientconfig, DEFAULT_LIGHTWALLETD_SERVER}, + lightclient::LightClient, + testutils::scenarios, + testvectors::seeds::HOSPITAL_MUSEUM_SEED, + wallet::WalletBase, +}; + +#[ignore = "too slow, and flakey"] +#[tokio::test] +async fn sync_mainnet_test() { + tracing_subscriber::fmt().init(); + + let uri = construct_lightwalletd_uri(Some(DEFAULT_LIGHTWALLETD_SERVER.to_string())); + let temp_dir = TempDir::new().unwrap(); + let temp_path = temp_dir.path().to_path_buf(); + let config = load_clientconfig( + uri.clone(), + Some(temp_path), + zingolib::config::ChainType::Mainnet, + true, + ) + .unwrap(); + let mut lightclient = LightClient::create_from_wallet_base_async( + WalletBase::from_string(HOSPITAL_MUSEUM_SEED.to_string()), + &config, + 2_611_700, + true, + ) + .await + .unwrap(); + + let client = GrpcConnector::new(uri).get_client().await.unwrap(); + + sync(client, &config.chain, &mut lightclient.wallet) + .await + .unwrap(); + + dbg!(lightclient.wallet.wallet_blocks()); + dbg!(lightclient.wallet.nullifier_map()); + dbg!(lightclient.wallet.sync_state()); +} + +#[tokio::test] +async fn sync_test() { + tracing_subscriber::fmt().init(); + + let (_regtest_manager, _cph, _faucet, mut recipient, _txid) = + scenarios::orchard_funded_recipient(5_000_000).await; + let uri = recipient.config().lightwalletd_uri.read().unwrap().clone(); + + let client = GrpcConnector::new(uri).get_client().await.unwrap(); + + sync( + client, + &recipient.config().chain.clone(), + &mut recipient.wallet, + ) + .await + .unwrap(); + + dbg!(recipient.wallet.wallet_blocks()); + dbg!(recipient.wallet.nullifier_map()); + dbg!(recipient.wallet.sync_state()); +} diff --git a/libtonode-tests/tests/wallet.rs b/libtonode-tests/tests/wallet.rs new file mode 100644 index 0000000000..0ce0669e43 --- /dev/null +++ b/libtonode-tests/tests/wallet.rs @@ -0,0 +1,324 @@ +#![forbid(unsafe_code)] +mod load_wallet { + + use std::fs::File; + + use zcash_client_backend::PoolType; + use zcash_client_backend::ShieldedProtocol; + use zingolib::check_client_balances; + use zingolib::config::RegtestNetwork; + use zingolib::config::ZingoConfig; + use zingolib::get_base_address_macro; + use zingolib::lightclient::send::send_with_proposal::QuickSendError; + use zingolib::lightclient::LightClient; + use zingolib::lightclient::PoolBalances; + use zingolib::testutils::lightclient::from_inputs; + use zingolib::testutils::paths::get_cargo_manifest_dir; + use zingolib::testutils::scenarios; + use zingolib::utils; + use zingolib::wallet::disk::testing::examples; + use zingolib::wallet::propose::ProposeSendError::Proposal; + + #[tokio::test] + async fn load_old_wallet_at_reorged_height() { + let regtest_network = RegtestNetwork::all_upgrades_active(); + let (ref regtest_manager, cph, ref faucet) = scenarios::faucet( + PoolType::Shielded(ShieldedProtocol::Orchard), + regtest_network, + ) + .await; + println!("Shutting down initial zcd/lwd unneeded processes"); + drop(cph); + + let zcd_datadir = ®test_manager.zcashd_data_dir; + let zingo_datadir = ®test_manager.zingo_datadir; + // This test is the unique consumer of: + // zingolib/src/testvectors/old_wallet_reorg_test_wallet + let cached_data_dir = get_cargo_manifest_dir() + .parent() + .unwrap() + .join("zingolib/src/testvectors") + .join("old_wallet_reorg_test_wallet"); + let zcd_source = cached_data_dir + .join("zcashd") + .join(".") + .to_string_lossy() + .to_string(); + let zcd_dest = zcd_datadir.to_string_lossy().to_string(); + std::process::Command::new("rm") + .arg("-r") + .arg(&zcd_dest) + .output() + .expect("directory rm failed"); + std::fs::DirBuilder::new() + .create(&zcd_dest) + .expect("Dir recreate failed"); + std::process::Command::new("cp") + .arg("-r") + .arg(zcd_source) + .arg(zcd_dest) + .output() + .expect("directory copy failed"); + let zingo_source = cached_data_dir + .join("zingo-wallet.dat") + .to_string_lossy() + .to_string(); + let zingo_dest = zingo_datadir.to_string_lossy().to_string(); + std::process::Command::new("cp") + .arg("-f") + .arg(zingo_source) + .arg(&zingo_dest) + .output() + .expect("wallet copy failed"); + let _cph = regtest_manager.launch(false).unwrap(); + println!("loading wallet"); + + let wallet = examples::ExampleWalletNetwork::Regtest( + examples::ExampleRegtestWalletSeed::HMVASMUVWMSSVICHCARBPOCT( + examples::ExampleHMVASMUVWMSSVICHCARBPOCTVersion::V27, + ), + ) + .load_example_wallet() + .await; + + // let wallet = zingolib::testutils::load_wallet( + // zingo_dest.into(), + // ChainType::Regtest(regtest_network), + // ) + // .await; + println!("setting uri"); + *wallet + .transaction_context + .config + .lightwalletd_uri + .write() + .unwrap() = faucet.get_server_uri(); + println!("creating lightclient"); + let recipient = LightClient::create_from_wallet_async(wallet).await.unwrap(); + println!( + "pre-sync transactions: {}", + recipient.do_list_transactions().await.pretty(2) + ); + let expected_pre_sync_transactions = r#"[ + { + "block_height": 3, + "pending": false, + "datetime": 1692212261, + "position": 0, + "txid": "7a9d41caca143013ebd2f710e4dad04f0eb9f0ae98b42af0f58f25c61a9d439e", + "amount": 100000, + "zec_price": null, + "address": "uregtest1wdukkmv5p5n824e8ytnc3m6m77v9vwwl7hcpj0wangf6z23f9x0fnaen625dxgn8cgp67vzw6swuar6uwp3nqywfvvkuqrhdjffxjfg644uthqazrtxhrgwac0a6ujzgwp8y9cwthjeayq8r0q6786yugzzyt9vevxn7peujlw8kp3vf6d8p4fvvpd8qd5p7xt2uagelmtf3vl6w3u8", + "memo": null + }, + { + "block_height": 8, + "pending": false, + "datetime": 1692212266, + "position": 0, + "txid": "122f8ab8dc5483e36256a4fbd7ff8d60eb7196670716a6690f9215f1c2a4d841", + "amount": 50000, + "zec_price": null, + "address": "uregtest1wdukkmv5p5n824e8ytnc3m6m77v9vwwl7hcpj0wangf6z23f9x0fnaen625dxgn8cgp67vzw6swuar6uwp3nqywfvvkuqrhdjffxjfg644uthqazrtxhrgwac0a6ujzgwp8y9cwthjeayq8r0q6786yugzzyt9vevxn7peujlw8kp3vf6d8p4fvvpd8qd5p7xt2uagelmtf3vl6w3u8", + "memo": null + }, + { + "block_height": 9, + "pending": false, + "datetime": 1692212299, + "position": 0, + "txid": "0a014017add7dc9eb57ada3e70f905c9dce610ef055e135b03f4907dd5dc99a4", + "amount": 30000, + "zec_price": null, + "address": "uregtest1wdukkmv5p5n824e8ytnc3m6m77v9vwwl7hcpj0wangf6z23f9x0fnaen625dxgn8cgp67vzw6swuar6uwp3nqywfvvkuqrhdjffxjfg644uthqazrtxhrgwac0a6ujzgwp8y9cwthjeayq8r0q6786yugzzyt9vevxn7peujlw8kp3vf6d8p4fvvpd8qd5p7xt2uagelmtf3vl6w3u8", + "memo": null + } +]"#; + assert_eq!( + expected_pre_sync_transactions, + recipient.do_list_transactions().await.pretty(2) + ); + recipient.do_sync(false).await.unwrap(); + let expected_post_sync_transactions = r#"[ + { + "block_height": 3, + "pending": false, + "datetime": 1692212261, + "position": 0, + "txid": "7a9d41caca143013ebd2f710e4dad04f0eb9f0ae98b42af0f58f25c61a9d439e", + "amount": 100000, + "zec_price": null, + "address": "uregtest1wdukkmv5p5n824e8ytnc3m6m77v9vwwl7hcpj0wangf6z23f9x0fnaen625dxgn8cgp67vzw6swuar6uwp3nqywfvvkuqrhdjffxjfg644uthqazrtxhrgwac0a6ujzgwp8y9cwthjeayq8r0q6786yugzzyt9vevxn7peujlw8kp3vf6d8p4fvvpd8qd5p7xt2uagelmtf3vl6w3u8", + "memo": null + }, + { + "block_height": 8, + "pending": false, + "datetime": 1692212266, + "position": 0, + "txid": "122f8ab8dc5483e36256a4fbd7ff8d60eb7196670716a6690f9215f1c2a4d841", + "amount": 50000, + "zec_price": null, + "address": "uregtest1wdukkmv5p5n824e8ytnc3m6m77v9vwwl7hcpj0wangf6z23f9x0fnaen625dxgn8cgp67vzw6swuar6uwp3nqywfvvkuqrhdjffxjfg644uthqazrtxhrgwac0a6ujzgwp8y9cwthjeayq8r0q6786yugzzyt9vevxn7peujlw8kp3vf6d8p4fvvpd8qd5p7xt2uagelmtf3vl6w3u8", + "memo": null + } +]"#; + assert_eq!( + expected_post_sync_transactions, + recipient.do_list_transactions().await.pretty(2) + ); + let expected_post_sync_balance = PoolBalances { + sapling_balance: Some(0), + verified_sapling_balance: Some(0), + spendable_sapling_balance: Some(0), + unverified_sapling_balance: Some(0), + orchard_balance: Some(150000), + verified_orchard_balance: Some(150000), + spendable_orchard_balance: Some(150000), + unverified_orchard_balance: Some(0), + transparent_balance: Some(0), + }; + assert_eq!(expected_post_sync_balance, recipient.do_balance().await); + let missing_output_index = from_inputs::quick_send( + &recipient, + vec![(&get_base_address_macro!(faucet, "unified"), 14000, None)], + ) + .await; + if let Err(QuickSendError::ProposeSend(Proposal( + zcash_client_backend::data_api::error::Error::DataSource(zingolib::wallet::tx_map::TxMapTraitError::InputSource( + zingolib::wallet::transaction_records_by_id::trait_inputsource::InputSourceError::MissingOutputIndexes(output_error) + )), + ))) = missing_output_index { + let txid1 = utils::conversion::txid_from_hex_encoded_str("122f8ab8dc5483e36256a4fbd7ff8d60eb7196670716a6690f9215f1c2a4d841").unwrap(); + let txid2 = utils::conversion::txid_from_hex_encoded_str("7a9d41caca143013ebd2f710e4dad04f0eb9f0ae98b42af0f58f25c61a9d439e").unwrap(); + let expected_txids = vec![txid1, txid2]; + // in case the txids are in reverse order + let missing_index_txids: Vec = output_error.into_iter().map(|(txid, _)| txid).collect(); + if missing_index_txids != expected_txids { + let expected_txids = vec![txid2, txid1]; + assert!(missing_index_txids == expected_txids, "{:?}\n\n{:?}", missing_index_txids, expected_txids); + } + }; + } + + #[tokio::test] + async fn pending_notes_are_not_saved() { + let regtest_network = RegtestNetwork::all_upgrades_active(); + let (regtest_manager, _cph, faucet, recipient) = scenarios::faucet_recipient( + PoolType::Shielded(ShieldedProtocol::Sapling), + regtest_network, + ) + .await; + zingolib::testutils::increase_height_and_wait_for_client(®test_manager, &faucet, 1) + .await + .unwrap(); + + check_client_balances!(faucet, o: 0 s: 2_500_000_000u64 t: 0u64); + let pending_txid = *from_inputs::quick_send( + &faucet, + vec![( + get_base_address_macro!(recipient, "unified").as_str(), + 5_000, + Some("this note never makes it to the wallet! or chain"), + )], + ) + .await + .unwrap() + .first(); + + assert!(!faucet + .transaction_summaries() + .await + .iter() + .find(|transaction_summary| transaction_summary.txid() == pending_txid) + .unwrap() + .status() + .is_confirmed()); + + assert_eq!( + faucet.do_list_notes(true).await["unspent_orchard_notes"].len(), + 1 + ); + // Create a new client using the faucet's wallet + + // Create zingo config + let mut wallet_location = regtest_manager.zingo_datadir; + wallet_location.pop(); + wallet_location.push("zingo_client_1"); + let zingo_config = + ZingoConfig::build(zingolib::config::ChainType::Regtest(regtest_network)) + .set_wallet_dir(wallet_location.clone()) + .create(); + wallet_location.push("zingo-wallet.dat"); + let read_buffer = File::open(wallet_location.clone()).unwrap(); + + // Create wallet from faucet zingo-wallet.dat + let faucet_wallet = + zingolib::wallet::LightWallet::read_internal(read_buffer, &zingo_config) + .await + .unwrap(); + + // Create client based on config and wallet of faucet + let faucet_copy = LightClient::create_from_wallet_async(faucet_wallet) + .await + .unwrap(); + assert_eq!( + &faucet_copy.do_seed_phrase().await.unwrap(), + &faucet.do_seed_phrase().await.unwrap() + ); // Sanity check identity + assert_eq!( + faucet.do_list_notes(true).await["unspent_orchard_notes"].len(), + 1 + ); + assert_eq!( + faucet_copy.do_list_notes(true).await["unspent_orchard_notes"].len(), + 0 + ); + assert!(!faucet_copy + .transaction_summaries() + .await + .iter() + .any(|transaction_summary| transaction_summary.txid() == pending_txid)); + let mut faucet_transactions = faucet.do_list_transactions().await; + faucet_transactions.pop(); + faucet_transactions.pop(); + let mut faucet_copy_transactions = faucet_copy.do_list_transactions().await; + faucet_copy_transactions.pop(); + assert_eq!(faucet_transactions, faucet_copy_transactions); + } + + #[tokio::test] + async fn verify_old_wallet_uses_server_height_in_send() { + // An earlier version of zingolib used the _wallet's_ 'height' when + // constructing transactions. This worked well enough when the + // client completed sync prior to sending, but when we introduced + // interrupting send, it made it immediately obvious that this was + // the wrong height to use! The correct height is the + // "mempool height" which is the server_height + 1 + let (regtest_manager, _cph, faucet, recipient) = + scenarios::faucet_recipient_default().await; + // Ensure that the client has confirmed spendable funds + zingolib::testutils::increase_height_and_wait_for_client(®test_manager, &faucet, 5) + .await + .unwrap(); + + // Without sync push server forward 2 blocks + zingolib::testutils::increase_server_height(®test_manager, 2).await; + let client_wallet_height = faucet.do_wallet_last_scanned_height().await; + + // Verify that wallet is still back at 6. + assert_eq!(client_wallet_height.as_fixed_point_u64(0).unwrap(), 8); + + // Interrupt generating send + from_inputs::quick_send( + &faucet, + vec![( + &get_base_address_macro!(recipient, "unified"), + 10_000, + Some("Interrupting sync!!"), + )], + ) + .await + .unwrap(); + } +} diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 2b9b1f1767..08d4d36c08 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "stable" +channel = "1.81" components = [ "clippy", "rustfmt" ] diff --git a/zingo-memo/src/lib.rs b/zingo-memo/src/lib.rs index 776a89c8c3..b7bf6cec76 100644 --- a/zingo-memo/src/lib.rs +++ b/zingo-memo/src/lib.rs @@ -11,10 +11,13 @@ use zcash_address::unified::{Address, Container, Encoding, Receiver}; use zcash_client_backend::address::UnifiedAddress; use zcash_encoding::{CompactSize, Vector}; -/// A parsed memo. Currently there is only one version of this protocol, -/// which is a list of UAs. The main use-case for this is to record the -/// UAs sent from, as the blockchain only records the pool-specific receiver -/// corresponding to the key we sent with. +/// A parsed memo. +/// The main use-case for this is to record the UAs that a foreign recipient provided, +/// as the blockchain only records the pool-specific receiver corresponding to the key we sent with. +/// We also record the index of any ephemeral addresses sent to. On rescan, this tells us: +/// * this transaction is the first step of a multistep proposal that is sending +/// to a TEX address in the second step +/// * what ephemeral address we need to derive in order to sync the second step #[non_exhaustive] #[derive(Debug)] pub enum ParsedMemo { @@ -23,12 +26,20 @@ pub enum ParsedMemo { /// The list of unified addresses uas: Vec, }, + /// the memo including unified addresses and ephemeral indexes + Version1 { + /// the list of unified addresses + uas: Vec, + /// The ephemeral address indexes + ephemeral_address_indexes: Vec, + }, } /// Packs a list of UAs into a memo. The UA only memo is version 0 of the protocol /// Note that a UA's raw representation is 1 byte for length, +21 for a T-receiver, /// +44 for a Sapling receiver, and +44 for an Orchard receiver. This totals a maximum /// of 110 bytes per UA, and attempting to write more than 510 bytes will cause an error. +#[deprecated(note = "prefer version 1")] pub fn create_wallet_internal_memo_version_0(uas: &[UnifiedAddress]) -> io::Result<[u8; 511]> { let mut uas_bytes_vec = Vec::new(); CompactSize::write(&mut uas_bytes_vec, 0usize)?; @@ -47,6 +58,38 @@ pub fn create_wallet_internal_memo_version_0(uas: &[UnifiedAddress]) -> io::Resu } } +/// Packs a list of UAs and/or ephemeral address indexes. into a memo. +/// Note that a UA's raw representation is 1 byte for length, +21 for a T-receiver, +/// +44 for a Sapling receiver, and +44 for an Orchard receiver. This totals a maximum +/// of 110 bytes per UA, and attempting to write more than 510 bytes will cause an error. +/// Ephemeral address indexes are CompactSize encoded, so for most use cases will only be +/// one byte. +pub fn create_wallet_internal_memo_version_1( + uas: &[UnifiedAddress], + ephemeral_address_indexes: &[u32], +) -> io::Result<[u8; 511]> { + let mut memo_bytes_vec = Vec::new(); + CompactSize::write(&mut memo_bytes_vec, 1usize)?; + Vector::write(&mut memo_bytes_vec, uas, |w, ua| { + write_unified_address_to_raw_encoding(ua, w) + })?; + Vector::write( + &mut memo_bytes_vec, + ephemeral_address_indexes, + |w, ea_index| CompactSize::write(w, *ea_index as usize), + )?; + let mut memo_bytes = [0u8; 511]; + if memo_bytes_vec.len() > 511 { + Err(io::Error::new( + io::ErrorKind::InvalidData, + "Too many addresses to fit in memo field", + )) + } else { + memo_bytes[..memo_bytes_vec.len()].copy_from_slice(memo_bytes_vec.as_slice()); + Ok(memo_bytes) + } +} + /// Attempts to parse the 511 bytes of a version_0 zingo memo pub fn parse_zingo_memo(memo: [u8; 511]) -> io::Result { let mut reader: &[u8] = &memo; @@ -54,6 +97,10 @@ pub fn parse_zingo_memo(memo: [u8; 511]) -> io::Result { 0 => Ok(ParsedMemo::Version0 { uas: Vector::read(&mut reader, |r| read_unified_address_from_raw_encoding(r))?, }), + 1 => Ok(ParsedMemo::Version1 { + uas: Vector::read(&mut reader, |r| read_unified_address_from_raw_encoding(r))?, + ephemeral_address_indexes: Vector::read(&mut reader, |r| CompactSize::read_t(r))?, + }), _ => Err(io::Error::new( io::ErrorKind::InvalidData, "Received memo from a future version of this protocol.\n\ diff --git a/zingo-netutils/Cargo.toml b/zingo-netutils/Cargo.toml index 9b7829639f..16617a0cad 100644 --- a/zingo-netutils/Cargo.toml +++ b/zingo-netutils/Cargo.toml @@ -5,18 +5,20 @@ authors = ["zingo@zingolabs.org"] edition = "2021" [dependencies] +http-body-util.workspace = true +http-body.workspace = true http.workspace = true -tokio-rustls.workspace = true -zcash_client_backend.workspace = true -tower.workspace = true hyper-rustls.workspace = true -webpki-roots = "0.21.0" +hyper-util.workspace = true hyper.workspace = true -http-body.workspace = true -tonic.workspace = true prost.workspace = true +rustls-pemfile.workspace = true thiserror.workspace = true -rustls-pemfile = { workspace = true } +tokio-rustls.workspace = true +tonic.workspace = true +tower.workspace = true +webpki-roots = "0.25" +zcash_client_backend.workspace = true [features] test-features = [] diff --git a/zingo-netutils/src/lib.rs b/zingo-netutils/src/lib.rs index 8d2484156e..557454213d 100644 --- a/zingo-netutils/src/lib.rs +++ b/zingo-netutils/src/lib.rs @@ -5,21 +5,24 @@ #![warn(missing_docs)] use std::sync::Arc; -use tower::ServiceExt; +use client::client_from_connector; use http::{uri::PathAndQuery, Uri}; -use http_body::combinators::UnsyncBoxBody; -use hyper::client::HttpConnector; +use http_body_util::combinators::UnsyncBoxBody; +use hyper_util::client::legacy::connect::HttpConnector; use thiserror::Error; +use tokio_rustls::rustls::pki_types::{Der, TrustAnchor}; use tokio_rustls::rustls::{ClientConfig, RootCertStore}; use tonic::Status; use tower::util::BoxCloneService; +use tower::ServiceExt; use zcash_client_backend::proto::service::compact_tx_streamer_client::CompactTxStreamerClient; -type UnderlyingService = BoxCloneService< +/// ? +pub type UnderlyingService = BoxCloneService< http::Request>, - http::Response, - hyper::Error, + http::Response, + hyper_util::client::legacy::Error, >; #[allow(missing_docs)] // error types document themselves @@ -33,6 +36,25 @@ pub enum GetClientError { InvalidPathAndQuery, } +/// ? +pub mod client { + use http_body::Body; + use hyper_util::client::legacy::{connect::Connect, Client}; + /// a utility used in multiple places + pub fn client_from_connector(connector: C, http2_only: bool) -> Box> + where + C: Connect + Clone, + B: Body + Send, + B::Data: Send, + { + Box::new( + Client::builder(hyper_util::rt::TokioExecutor::new()) + .http2_only(http2_only) + .build(connector), + ) + } +} + /// The connector, containing the URI to connect to. /// This type is mostly an interface to the get_client method, /// the proto-generated CompactTxStreamerClient type is the main @@ -71,27 +93,26 @@ impl GrpcConnector { .ok_or(GetClientError::InvalidAuthority)? .clone(); if uri.scheme_str() == Some("https") { - let mut roots = RootCertStore::empty(); - roots.add_server_trust_anchors(webpki_roots::TLS_SERVER_ROOTS.0.iter().map( - |anchor_ref| { - tokio_rustls::rustls::OwnedTrustAnchor::from_subject_spki_name_constraints( - anchor_ref.subject, - anchor_ref.spki, - anchor_ref.name_constraints, - ) - }, - )); + let mut root_store = RootCertStore::empty(); + //webpki uses a different struct for TrustAnchor + root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().map(|anchor_ref| { + TrustAnchor { + subject: Der::from_slice(anchor_ref.subject), + subject_public_key_info: Der::from_slice(anchor_ref.spki), + name_constraints: anchor_ref.name_constraints.map(Der::from_slice), + } + })); #[cfg(test)] - add_test_cert_to_roots(&mut roots); + add_test_cert_to_roots(&mut root_store); - let tls = ClientConfig::builder() - .with_safe_defaults() - .with_root_certificates(roots) + let config = ClientConfig::builder() + .with_root_certificates(root_store) .with_no_client_auth(); + let connector = tower::ServiceBuilder::new() .layer_fn(move |s| { - let tls = tls.clone(); + let tls = config.clone(); hyper_rustls::HttpsConnectorBuilder::new() .with_tls_config(tls) @@ -100,7 +121,7 @@ impl GrpcConnector { .wrap_connector(s) }) .service(http_connector); - let client = Box::new(hyper::Client::builder().build(connector)); + let client = client_from_connector(connector, false); let svc = tower::ServiceBuilder::new() //Here, we take all the pieces of our uri, and add in the path from the Requests's uri .map_request(move |mut request: http::Request| { @@ -126,7 +147,7 @@ impl GrpcConnector { Ok(CompactTxStreamerClient::new(svc.boxed_clone())) } else { let connector = tower::ServiceBuilder::new().service(http_connector); - let client = Box::new(hyper::Client::builder().http2_only(true).build(connector)); + let client = client_from_connector(connector, true); let svc = tower::ServiceBuilder::new() //Here, we take all the pieces of our uri, and add in the path from the Requests's uri .map_request(move |mut request: http::Request| { @@ -157,9 +178,14 @@ impl GrpcConnector { #[cfg(test)] fn add_test_cert_to_roots(roots: &mut RootCertStore) { + use tonic::transport::CertificateDer; + const TEST_PEMFILE_PATH: &str = "test-data/localhost.pem"; let fd = std::fs::File::open(TEST_PEMFILE_PATH).unwrap(); let mut buf = std::io::BufReader::new(&fd); - let certs = rustls_pemfile::certs(&mut buf).unwrap(); - roots.add_parsable_certificates(&certs); + let certs_bytes = rustls_pemfile::certs(&mut buf).unwrap(); + let certs: Vec> = + certs_bytes.into_iter().map(CertificateDer::from).collect(); + + roots.add_parsable_certificates(certs); } diff --git a/zingo-status/src/confirmation_status.rs b/zingo-status/src/confirmation_status.rs index ecb6fdd0c4..1cc6284afc 100644 --- a/zingo-status/src/confirmation_status.rs +++ b/zingo-status/src/confirmation_status.rs @@ -1,5 +1,4 @@ -//! A note can either be: -//! Pending === not on-record on-chain +//! If a note is confirmed, it is: //! Confirmed === on-record on-chain at BlockHeight use zcash_primitives::consensus::BlockHeight; @@ -7,9 +6,17 @@ use zcash_primitives::consensus::BlockHeight; #[derive(Clone, Copy, Debug, PartialEq)] pub enum ConfirmationStatus { - /// The transaction is pending confirmation to the zcash blockchain. It may be waiting in the mempool. + /// the transaction has been calculated but not yet broadcast to the chain. + Calculated(BlockHeight), + + /// The transaction has been sent to the zcash blockchain. It could be in the mempool. /// The BlockHeight is the 1 + the height of the chain as the transaction was broadcast, i.e. the target height. - Pending(BlockHeight), + Transmitted(BlockHeight), + + /// The transaction is known to be or have been in the mempool. + /// The BlockHeight is the 1 + the height of the chain as the transaction entered the mempool, i.e. the target height. + Mempool(BlockHeight), + /// The transaction has been included in at-least one block mined to the zcash blockchain. /// The height of a confirmed block that contains the transaction. Confirmed(BlockHeight), @@ -19,31 +26,12 @@ impl ConfirmationStatus { /// Converts from a blockheight and `pending`. pending is deprecated and is only needed in loading from save. pub fn from_blockheight_and_pending_bool(blockheight: BlockHeight, pending: bool) -> Self { if pending { - Self::Pending(blockheight) + Self::Transmitted(blockheight) } else { Self::Confirmed(blockheight) } } - /// A wrapper matching the Pending case. - /// # Examples - /// - /// ``` - /// use zingo_status::confirmation_status::ConfirmationStatus; - /// use zcash_primitives::consensus::BlockHeight; - /// - /// let status = ConfirmationStatus::Pending(10.into()); - /// assert_eq!(status.is_pending(), true); - /// assert_eq!(status.is_confirmed(), false); - /// - /// let status = ConfirmationStatus::Confirmed(10.into()); - /// assert_eq!(status.is_pending(), false); - /// assert_eq!(status.is_confirmed(), true); - /// ``` - pub fn is_pending(&self) -> bool { - matches!(self, Self::Pending(_)) - } - /// A wrapper matching the Confirmed case. /// # Examples /// @@ -51,13 +39,10 @@ impl ConfirmationStatus { /// use zingo_status::confirmation_status::ConfirmationStatus; /// use zcash_primitives::consensus::BlockHeight; /// - /// let status = ConfirmationStatus::Pending(10.into()); - /// assert_eq!(status.is_confirmed(), false); - /// assert_eq!(status.is_pending(), true); - /// - /// let status = ConfirmationStatus::Confirmed(10.into()); - /// assert_eq!(status.is_confirmed(), true); - /// assert_eq!(status.is_pending(), false); + /// assert!(!ConfirmationStatus::Calculated(10.into()).is_confirmed()); + /// assert!(!ConfirmationStatus::Transmitted(10.into()).is_confirmed()); + /// assert!(!ConfirmationStatus::Mempool(10.into()).is_confirmed()); + /// assert!(ConfirmationStatus::Confirmed(10.into()).is_confirmed()); /// ``` pub fn is_confirmed(&self) -> bool { matches!(self, Self::Confirmed(_)) @@ -70,20 +55,21 @@ impl ConfirmationStatus { /// use zingo_status::confirmation_status::ConfirmationStatus; /// use zcash_primitives::consensus::BlockHeight; /// - /// let status = ConfirmationStatus::Confirmed(10.into()); - /// assert_eq!(status.is_confirmed_after_or_at(&9.into()), true); - /// - /// let status = ConfirmationStatus::Pending(10.into()); - /// assert_eq!(status.is_confirmed_after_or_at(&10.into()), false); - /// - /// let status = ConfirmationStatus::Confirmed(10.into()); - /// assert_eq!(status.is_confirmed_after_or_at(&11.into()), false); + /// assert!(!ConfirmationStatus::Calculated(10.into()).is_confirmed_after_or_at(&9.into())); + /// assert!(!ConfirmationStatus::Calculated(10.into()).is_confirmed_after_or_at(&10.into())); + /// assert!(!ConfirmationStatus::Calculated(10.into()).is_confirmed_after_or_at(&11.into())); + /// assert!(!ConfirmationStatus::Transmitted(10.into()).is_confirmed_after_or_at(&9.into())); + /// assert!(!ConfirmationStatus::Transmitted(10.into()).is_confirmed_after_or_at(&10.into())); + /// assert!(!ConfirmationStatus::Transmitted(10.into()).is_confirmed_after_or_at(&11.into())); + /// assert!(!ConfirmationStatus::Mempool(10.into()).is_confirmed_after_or_at(&9.into())); + /// assert!(!ConfirmationStatus::Mempool(10.into()).is_confirmed_after_or_at(&10.into())); + /// assert!(!ConfirmationStatus::Mempool(10.into()).is_confirmed_after_or_at(&11.into())); + /// assert!(ConfirmationStatus::Confirmed(10.into()).is_confirmed_after_or_at(&9.into())); + /// assert!(ConfirmationStatus::Confirmed(10.into()).is_confirmed_after_or_at(&10.into())); + /// assert!(!ConfirmationStatus::Confirmed(10.into()).is_confirmed_after_or_at(&11.into())); /// ``` pub fn is_confirmed_after_or_at(&self, comparison_height: &BlockHeight) -> bool { - match self { - Self::Confirmed(self_height) => self_height >= comparison_height, - _ => false, - } + matches!(self, Self::Confirmed(self_height) if self_height >= comparison_height) } /// To return true, the status must be confirmed and no later than specified height. @@ -93,22 +79,21 @@ impl ConfirmationStatus { /// use zingo_status::confirmation_status::ConfirmationStatus; /// use zcash_primitives::consensus::BlockHeight; /// - /// let status = ConfirmationStatus::Confirmed(10.into()); - /// assert_eq!(status.is_confirmed_before_or_at(&9.into()), false); - /// - /// let status = ConfirmationStatus::Pending(10.into()); - /// assert_eq!(status.is_confirmed_before_or_at(&10.into()), false); - /// - /// let status = ConfirmationStatus::Confirmed(10.into()); - /// assert_eq!(status.is_confirmed_before_or_at(&11.into()), true); + /// assert!(!ConfirmationStatus::Calculated(10.into()).is_confirmed_before_or_at(&9.into())); + /// assert!(!ConfirmationStatus::Calculated(10.into()).is_confirmed_before_or_at(&10.into())); + /// assert!(!ConfirmationStatus::Calculated(10.into()).is_confirmed_before_or_at(&11.into())); + /// assert!(!ConfirmationStatus::Transmitted(10.into()).is_confirmed_before_or_at(&9.into())); + /// assert!(!ConfirmationStatus::Transmitted(10.into()).is_confirmed_before_or_at(&10.into())); + /// assert!(!ConfirmationStatus::Transmitted(10.into()).is_confirmed_before_or_at(&11.into())); + /// assert!(!ConfirmationStatus::Mempool(10.into()).is_confirmed_before_or_at(&9.into())); + /// assert!(!ConfirmationStatus::Mempool(10.into()).is_confirmed_before_or_at(&10.into())); + /// assert!(!ConfirmationStatus::Mempool(10.into()).is_confirmed_before_or_at(&11.into())); + /// assert!(!ConfirmationStatus::Confirmed(10.into()).is_confirmed_before_or_at(&9.into())); + /// assert!(ConfirmationStatus::Confirmed(10.into()).is_confirmed_before_or_at(&10.into())); + /// assert!(ConfirmationStatus::Confirmed(10.into()).is_confirmed_before_or_at(&11.into())); /// ``` pub fn is_confirmed_before_or_at(&self, comparison_height: &BlockHeight) -> bool { - match self { - Self::Confirmed(self_height) => { - self.is_confirmed_before(comparison_height) || self_height == comparison_height - } - _ => false, - } + matches!(self, Self::Confirmed(self_height) if self_height <= comparison_height) } /// To return true, the status must be confirmed earlier than specified height. @@ -118,43 +103,21 @@ impl ConfirmationStatus { /// use zingo_status::confirmation_status::ConfirmationStatus; /// use zcash_primitives::consensus::BlockHeight; /// - /// let status = ConfirmationStatus::Confirmed(10.into()); - /// assert_eq!(status.is_confirmed_before(&9.into()), false); - /// - /// let status = ConfirmationStatus::Confirmed(10.into()); - /// assert_eq!(status.is_confirmed_before(&10.into()), false); - /// - /// let status = ConfirmationStatus::Confirmed(10.into()); - /// assert_eq!(status.is_confirmed_before(&11.into()), true); + /// assert!(!ConfirmationStatus::Calculated(10.into()).is_confirmed_before(&9.into())); + /// assert!(!ConfirmationStatus::Calculated(10.into()).is_confirmed_before(&10.into())); + /// assert!(!ConfirmationStatus::Calculated(10.into()).is_confirmed_before(&11.into())); + /// assert!(!ConfirmationStatus::Transmitted(10.into()).is_confirmed_before(&9.into())); + /// assert!(!ConfirmationStatus::Transmitted(10.into()).is_confirmed_before(&10.into())); + /// assert!(!ConfirmationStatus::Transmitted(10.into()).is_confirmed_before(&11.into())); + /// assert!(!ConfirmationStatus::Mempool(10.into()).is_confirmed_before(&9.into())); + /// assert!(!ConfirmationStatus::Mempool(10.into()).is_confirmed_before(&10.into())); + /// assert!(!ConfirmationStatus::Mempool(10.into()).is_confirmed_before(&11.into())); + /// assert!(!ConfirmationStatus::Confirmed(10.into()).is_confirmed_before(&9.into())); + /// assert!(!ConfirmationStatus::Confirmed(10.into()).is_confirmed_before(&10.into())); + /// assert!(ConfirmationStatus::Confirmed(10.into()).is_confirmed_before(&11.into())); /// ``` pub fn is_confirmed_before(&self, comparison_height: &BlockHeight) -> bool { - match self { - Self::Confirmed(self_height) => self_height < comparison_height, - _ => false, - } - } - - /// To return true, the status must have broadcast at or later than specified height. - /// # Examples - /// - /// ``` - /// use zingo_status::confirmation_status::ConfirmationStatus; - /// use zcash_primitives::consensus::BlockHeight; - /// - /// let status = ConfirmationStatus::Confirmed(10.into()); - /// assert_eq!(status.is_pending_after_or_at(&9.into()), false); - /// - /// let status = ConfirmationStatus::Pending(10.into()); - /// assert_eq!(status.is_pending_after_or_at(&10.into()), true); - /// - /// let status = ConfirmationStatus::Pending(10.into()); - /// assert_eq!(status.is_pending_after_or_at(&11.into()), false); - /// ``` - pub fn is_pending_after_or_at(&self, comparison_height: &BlockHeight) -> bool { - match self { - Self::Pending(self_height) => self_height >= comparison_height, - _ => false, - } + matches!(self, Self::Confirmed(self_height) if self_height < comparison_height) } /// To return true, the status must not be confirmed and it must have been submitted sufficiently far in the past. This allows deduction of expired transactions. @@ -164,19 +127,26 @@ impl ConfirmationStatus { /// use zingo_status::confirmation_status::ConfirmationStatus; /// use zcash_primitives::consensus::BlockHeight; /// - /// let status = ConfirmationStatus::Confirmed(16.into()); - /// assert_eq!(status.is_pending_before(&15.into()), false); - /// - /// let status = ConfirmationStatus::Pending(12.into()); - /// assert_eq!(status.is_pending_before(&13.into()), true); - /// - /// let status = ConfirmationStatus::Pending(14.into()); - /// assert_eq!(status.is_pending_before(&14.into()), false); - /// ``` + /// assert!(!ConfirmationStatus::Calculated(10.into()).is_pending_before(&9.into())); + /// assert!(!ConfirmationStatus::Calculated(10.into()).is_pending_before(&10.into())); + /// assert!(ConfirmationStatus::Calculated(10.into()).is_pending_before(&11.into())); + /// assert!(!ConfirmationStatus::Transmitted(10.into()).is_pending_before(&9.into())); + /// assert!(!ConfirmationStatus::Transmitted(10.into()).is_pending_before(&10.into())); + /// assert!(ConfirmationStatus::Transmitted(10.into()).is_pending_before(&11.into())); + /// assert!(!ConfirmationStatus::Mempool(10.into()).is_pending_before(&9.into())); + /// assert!(!ConfirmationStatus::Mempool(10.into()).is_pending_before(&10.into())); + /// assert!(ConfirmationStatus::Mempool(10.into()).is_pending_before(&11.into())); + /// assert!(!ConfirmationStatus::Confirmed(10.into()).is_pending_before(&9.into())); + /// assert!(!ConfirmationStatus::Confirmed(10.into()).is_pending_before(&10.into())); + /// assert!(!ConfirmationStatus::Confirmed(10.into()).is_pending_before(&11.into())); + /// ``` + // TODO remove 'pending' and fix spend status. pub fn is_pending_before(&self, comparison_height: &BlockHeight) -> bool { match self { - Self::Pending(self_height) => self_height < comparison_height, - Self::Confirmed(_) => false, + Self::Calculated(self_height) + | Self::Transmitted(self_height) + | Self::Mempool(self_height) => self_height < comparison_height, + _ => false, } } @@ -190,7 +160,7 @@ impl ConfirmationStatus { /// let status = ConfirmationStatus::Confirmed(16.into()); /// assert_eq!(status.get_confirmed_height(), Some(16.into())); /// - /// let status = ConfirmationStatus::Pending(15.into()); + /// let status = ConfirmationStatus::Mempool(15.into()); /// assert_eq!(status.get_confirmed_height(), None); /// ``` pub fn get_confirmed_height(&self) -> Option { @@ -200,26 +170,6 @@ impl ConfirmationStatus { } } - /// Returns if transaction is confirmed, otherwise returns the height it was broadcast to the mempool. - /// # Examples - /// - /// ``` - /// use zingo_status::confirmation_status::ConfirmationStatus; - /// use zcash_primitives::consensus::BlockHeight; - /// - /// let status = ConfirmationStatus::Confirmed(16.into()); - /// assert_eq!(status.get_pending_height(), None); - /// - /// let status = ConfirmationStatus::Pending(15.into()); - /// assert_eq!(status.get_pending_height(), Some(15.into())); - /// ``` - pub fn get_pending_height(&self) -> Option { - match self { - Self::Pending(self_height) => Some(*self_height), - _ => None, - } - } - /// # Examples /// /// ``` @@ -231,7 +181,9 @@ impl ConfirmationStatus { /// ``` pub fn get_height(&self) -> BlockHeight { match self { - Self::Pending(self_height) => *self_height, + Self::Calculated(self_height) => *self_height, + Self::Mempool(self_height) => *self_height, + Self::Transmitted(self_height) => *self_height, Self::Confirmed(self_height) => *self_height, } } @@ -240,8 +192,14 @@ impl ConfirmationStatus { impl std::fmt::Display for ConfirmationStatus { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::Pending(_) => { - write!(f, "pending") + Self::Calculated(_) => { + write!(f, "calculated") + } + Self::Transmitted(_) => { + write!(f, "transmitted") + } + Self::Mempool(_) => { + write!(f, "mempool") } Self::Confirmed(_) => { write!(f, "confirmed") diff --git a/zingo-sync/Cargo.toml b/zingo-sync/Cargo.toml new file mode 100644 index 0000000000..f6277dc049 --- /dev/null +++ b/zingo-sync/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "zingo-sync" +version = "0.1.0" +edition = "2021" + +[dependencies] +# Zingo +zingo-netutils = { path = "../zingo-netutils" } + +# Zcash +zcash_client_backend.workspace = true +zcash_primitives.workspace = true +zcash_note_encryption.workspace = true +zcash_keys.workspace = true +sapling-crypto.workspace = true +orchard.workspace = true +incrementalmerkletree.workspace = true +shardtree.workspace = true + +# Async +futures.workspace = true +tokio.workspace = true + +# Client +tonic.workspace = true + +# Logging +tracing.workspace = true + +# Metrics +memuse.workspace = true + +# Parallel processing +crossbeam-channel.workspace = true +rayon.workspace = true + +# Minimise boilerplate +getset.workspace = true diff --git a/zingo-sync/src/client.rs b/zingo-sync/src/client.rs new file mode 100644 index 0000000000..beaea957ea --- /dev/null +++ b/zingo-sync/src/client.rs @@ -0,0 +1,75 @@ +//! Module for handling all connections to the server + +use std::ops::Range; + +use zcash_client_backend::{ + data_api::chain::ChainState, + proto::{ + compact_formats::CompactBlock, + service::{BlockId, TreeState}, + }, +}; +use zcash_primitives::consensus::BlockHeight; + +use tokio::sync::{mpsc::UnboundedSender, oneshot}; + +pub mod fetch; + +/// Fetch requests are created and sent to the [`crate::client::fetch::fetch`] task when a connection to the server is required. +/// +/// Each variant includes a [`tokio::sync::oneshot::Sender`] for returning the fetched data to the requester. +#[derive(Debug)] +pub enum FetchRequest { + /// Gets the height of the blockchain from the server. + ChainTip(oneshot::Sender), + /// Gets the specified range of compact blocks from the server (end exclusive). + CompactBlockRange(oneshot::Sender>, Range), + /// Gets the tree states for a specified block height.. + TreeState(oneshot::Sender, BlockHeight), +} + +/// Gets the height of the blockchain from the server. +/// +/// Requires [`crate::client::fetch::fetch`] to be running concurrently, connected via the `fetch_request` channel. +pub async fn get_chain_height( + fetch_request_sender: UnboundedSender, +) -> Result { + let (sender, receiver) = oneshot::channel::(); + fetch_request_sender + .send(FetchRequest::ChainTip(sender)) + .unwrap(); + let chain_tip = receiver.await.unwrap(); + + Ok(BlockHeight::from_u32(chain_tip.height as u32)) +} +/// Gets the specified range of compact blocks from the server (end exclusive). +/// +/// Requires [`crate::client::fetch::fetch`] to be running concurrently, connected via the `fetch_request` channel. +pub async fn get_compact_block_range( + fetch_request_sender: UnboundedSender, + block_range: Range, +) -> Result, ()> { + let (sender, receiver) = oneshot::channel::>(); + fetch_request_sender + .send(FetchRequest::CompactBlockRange(sender, block_range)) + .unwrap(); + let compact_blocks = receiver.await.unwrap(); + + Ok(compact_blocks) +} +/// Gets the frontiers for a specified block height.. +/// +/// Requires [`crate::client::fetch::fetch`] to be running concurrently, connected via the `fetch_request` channel. +pub async fn get_frontiers( + fetch_request_sender: UnboundedSender, + block_height: BlockHeight, +) -> Result { + let (sender, receiver) = oneshot::channel::(); + fetch_request_sender + .send(FetchRequest::TreeState(sender, block_height)) + .unwrap(); + let tree_state = receiver.await.unwrap(); + let frontiers = tree_state.to_chain_state().unwrap(); + + Ok(frontiers) +} diff --git a/zingo-sync/src/client/fetch.rs b/zingo-sync/src/client/fetch.rs new file mode 100644 index 0000000000..c0f4be60b8 --- /dev/null +++ b/zingo-sync/src/client/fetch.rs @@ -0,0 +1,159 @@ +//! Queue and prioritise fetch requests to fetch data from the server + +use std::ops::Range; + +use tokio::sync::mpsc::UnboundedReceiver; + +use zcash_client_backend::proto::{ + compact_formats::CompactBlock, + service::{ + compact_tx_streamer_client::CompactTxStreamerClient, BlockId, BlockRange, ChainSpec, + TreeState, + }, +}; +use zcash_primitives::consensus::BlockHeight; + +use crate::client::FetchRequest; + +/// Receives [`self::FetchRequest`]'s via an [`tokio::sync::mpsc::UnboundedReceiver`] for queueing, +/// prioritisation and fetching from the server. +/// Returns the data specified in the [`self::FetchRequest`] variant via the provided [`tokio::sync::oneshot::Sender`]. +/// +/// Allows all requests to the server to be handled from a single task for efficiency and also enables +/// request prioritisation for further performance enhancement +pub async fn fetch( + mut fetch_request_receiver: UnboundedReceiver, + mut client: CompactTxStreamerClient, +) -> Result<(), ()> { + let mut fetch_request_queue: Vec = Vec::new(); + + loop { + // `fetcher` returns `Ok` here when all requests have successfully been fetched and the + // fetch_request channel is closed on sync completion. + if receive_fetch_requests(&mut fetch_request_receiver, &mut fetch_request_queue).await { + return Ok(()); + } + + let fetch_request = select_fetch_request(&mut fetch_request_queue); + + if let Some(request) = fetch_request { + fetch_from_server(&mut client, request).await.unwrap(); + } + } +} + +// receives fetch requests and populates the fetch request queue +// +// returns `true` if the fetch request channel is closed and all fetch requests have been completed, +// signalling sync is complete and no longer needs to fetch data from the server. +async fn receive_fetch_requests( + receiver: &mut UnboundedReceiver, + fetch_request_queue: &mut Vec, +) -> bool { + // if there are no fetch requests to process, sleep until the next fetch request is received + // or channel is closed + if fetch_request_queue.is_empty() { + if let Some(fetch_request) = receiver.recv().await { + fetch_request_queue.push(fetch_request); + } + } + // receive all remaining fetch requests from channel + // when channel is empty return `false` to continue fetching data from the server + // when channel is closed and all fetch requests are processed, return `true` + loop { + match receiver.try_recv() { + Ok(fetch_request) => fetch_request_queue.push(fetch_request), + Err(tokio::sync::mpsc::error::TryRecvError::Empty) => break, + Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => { + if fetch_request_queue.is_empty() { + return true; + } else { + break; + } + } + } + } + + false +} + +// TODO: placeholder for algorythm that selects the next fetch request to be processed +// return `None` if a fetch request could not be selected +fn select_fetch_request(fetch_request_queue: &mut Vec) -> Option { + // TODO: improve priority logic + if fetch_request_queue.first().is_some() { + Some(fetch_request_queue.remove(0)) + } else { + None + } +} + +// +async fn fetch_from_server( + client: &mut CompactTxStreamerClient, + fetch_request: FetchRequest, +) -> Result<(), ()> { + match fetch_request { + FetchRequest::ChainTip(sender) => { + tracing::info!("Fetching chain tip."); + let block_id = get_latest_block(client).await; + sender.send(block_id).unwrap(); + } + FetchRequest::CompactBlockRange(sender, block_range) => { + tracing::info!("Fetching compact blocks. {:?}", &block_range); + let compact_blocks = get_block_range(client, block_range).await; + sender.send(compact_blocks).unwrap(); + } + FetchRequest::TreeState(sender, block_height) => { + tracing::info!("Fetching tree state. {:?}", &block_height); + let tree_state = get_tree_state(client, block_height).await; + sender.send(tree_state).unwrap(); + } + } + + Ok(()) +} + +async fn get_latest_block( + client: &mut CompactTxStreamerClient, +) -> BlockId { + let request = tonic::Request::new(ChainSpec {}); + + client.get_latest_block(request).await.unwrap().into_inner() +} +async fn get_block_range( + client: &mut CompactTxStreamerClient, + block_range: Range, +) -> Vec { + let mut compact_blocks: Vec = + Vec::with_capacity(u64::from(block_range.end - block_range.start) as usize); + + let request = tonic::Request::new(BlockRange { + start: Some(BlockId { + height: u64::from(block_range.start), + hash: vec![], + }), + end: Some(BlockId { + height: u64::from(block_range.end) - 1, + hash: vec![], + }), + }); + let mut block_stream = client.get_block_range(request).await.unwrap().into_inner(); + + while let Some(compact_block) = block_stream.message().await.unwrap() { + compact_blocks.push(compact_block); + } + + compact_blocks +} +async fn get_tree_state( + client: &mut CompactTxStreamerClient, + block_height: BlockHeight, +) -> TreeState { + let request = tonic::Request::new(BlockId { + height: block_height.into(), + hash: vec![], + }); + + client.get_tree_state(request).await.unwrap().into_inner() +} diff --git a/zingo-sync/src/interface.rs b/zingo-sync/src/interface.rs new file mode 100644 index 0000000000..6a1145074c --- /dev/null +++ b/zingo-sync/src/interface.rs @@ -0,0 +1,94 @@ +//! Traits for interfacing a wallet with the sync engine + +use std::collections::{BTreeMap, HashMap}; +use std::fmt::Debug; + +use zcash_client_backend::keys::UnifiedFullViewingKey; +use zcash_primitives::consensus::BlockHeight; +use zcash_primitives::zip32::AccountId; + +use crate::primitives::{NullifierMap, SyncState, WalletBlock}; +use crate::witness::{ShardTreeData, ShardTrees}; + +/// Temporary dump for all neccessary wallet functionality for PoC +pub trait SyncWallet { + /// Errors associated with interfacing the sync engine with wallet data + type Error: Debug; + + /// Returns block height wallet was created + fn get_birthday(&self) -> Result; + + /// Returns mutable reference to wallet sync state + fn get_sync_state_mut(&mut self) -> Result<&mut SyncState, Self::Error>; + + /// Returns all unified full viewing keys known to this wallet. + fn get_unified_full_viewing_keys( + &self, + ) -> Result, Self::Error>; +} + +/// Trait for interfacing sync engine [`crate::primitives::WalletBlock`] with wallet data +pub trait SyncBlocks: SyncWallet { + // TODO: add method to get wallet data for writing defualt implementations on other methods + + /// Get a stored wallet compact block from wallet data by block height + /// Must return error if block is not found + fn get_wallet_block(&self, block_height: BlockHeight) -> Result; + + /// Get mutable reference to wallet blocks + fn get_wallet_blocks_mut( + &mut self, + ) -> Result<&mut BTreeMap, Self::Error>; + + /// Append wallet compact blocks to wallet data + fn append_wallet_blocks( + &mut self, + wallet_blocks: BTreeMap, + ) -> Result<(), Self::Error>; +} + +/// Trait for interfacing nullifiers with wallet data +pub trait SyncNullifiers: SyncWallet { + // TODO: add method to get wallet data for writing defualt implementations on other methods + + /// Get mutable reference to wallet nullifier map + fn get_nullifiers_mut(&mut self) -> Result<&mut NullifierMap, Self::Error>; + + /// Append nullifiers to wallet data + fn append_nullifiers(&mut self, nullifier_map: NullifierMap) -> Result<(), Self::Error>; +} + +/// Trait for interfacing shard tree data with wallet data +pub trait SyncShardTrees: SyncWallet { + /// Get mutable reference to shard trees + fn get_shard_trees_mut(&mut self) -> Result<&mut ShardTrees, Self::Error>; + + /// Update wallet data with shard tree data + fn update_shard_trees(&mut self, shard_tree_data: ShardTreeData) -> Result<(), Self::Error> { + let ShardTreeData { + sapling_initial_position, + orchard_initial_position, + sapling_leaves_and_retentions, + orchard_leaves_and_retentions, + } = shard_tree_data; + + self.get_shard_trees_mut() + .unwrap() + .sapling_mut() + .batch_insert( + sapling_initial_position, + sapling_leaves_and_retentions.into_iter(), + ) + .unwrap(); + self.get_shard_trees_mut() + .unwrap() + .orchard_mut() + .batch_insert( + orchard_initial_position, + orchard_leaves_and_retentions.into_iter(), + ) + .unwrap(); + + Ok(()) + } +} diff --git a/zingo-sync/src/keys.rs b/zingo-sync/src/keys.rs new file mode 100644 index 0000000000..536197ebe5 --- /dev/null +++ b/zingo-sync/src/keys.rs @@ -0,0 +1,160 @@ +//! Copied and modified from LRZ due to thread safety limitations and missing OVK + +use std::collections::HashMap; + +use getset::Getters; +use incrementalmerkletree::Position; +use orchard::{ + keys::{FullViewingKey, IncomingViewingKey, Scope}, + note_encryption::OrchardDomain, +}; +use sapling_crypto::{ + self as sapling, note_encryption::SaplingDomain, NullifierDerivingKey, SaplingIvk, +}; +use zcash_keys::keys::UnifiedFullViewingKey; +use zcash_note_encryption::Domain; + +pub(crate) type KeyId = (zcash_primitives::zip32::AccountId, Scope); + +/// A key that can be used to perform trial decryption and nullifier +/// computation for a [`CompactSaplingOutput`] or [`CompactOrchardAction`]. +pub trait ScanningKeyOps { + /// Prepare the key for use in batch trial decryption. + fn prepare(&self) -> D::IncomingViewingKey; + + /// Returns the account identifier for this key. An account identifier corresponds + /// to at most a single unified spending key's worth of spend authority, such that + /// both received notes and change spendable by that spending authority will be + /// interpreted as belonging to that account. + fn account_id(&self) -> &zcash_primitives::zip32::AccountId; + + /// Returns the [`zip32::Scope`] for which this key was derived, if known. + fn key_scope(&self) -> Option; + + /// Produces the nullifier for the specified note and witness, if possible. + /// + /// IVK-based implementations of this trait cannot successfully derive + /// nullifiers, in which this function will always return `None`. + fn nf(&self, note: &D::Note, note_position: Position) -> Option; +} +impl> ScanningKeyOps for &K { + fn prepare(&self) -> D::IncomingViewingKey { + (*self).prepare() + } + + fn account_id(&self) -> &zcash_primitives::zip32::AccountId { + (*self).account_id() + } + + fn key_scope(&self) -> Option { + (*self).key_scope() + } + + fn nf(&self, note: &D::Note, note_position: Position) -> Option { + (*self).nf(note, note_position) + } +} + +pub(crate) struct ScanningKey { + key_id: KeyId, + ivk: Ivk, + // TODO: Ovk + nk: Option, +} + +impl ScanningKeyOps + for ScanningKey +{ + fn prepare(&self) -> sapling::note_encryption::PreparedIncomingViewingKey { + sapling_crypto::note_encryption::PreparedIncomingViewingKey::new(&self.ivk) + } + + fn nf(&self, note: &sapling::Note, position: Position) -> Option { + self.nk.as_ref().map(|key| note.nf(key, position.into())) + } + + fn account_id(&self) -> &zcash_primitives::zip32::AccountId { + &self.key_id.0 + } + + fn key_scope(&self) -> Option { + Some(self.key_id.1) + } +} + +impl ScanningKeyOps + for ScanningKey +{ + fn prepare(&self) -> orchard::keys::PreparedIncomingViewingKey { + orchard::keys::PreparedIncomingViewingKey::new(&self.ivk) + } + + fn nf( + &self, + note: &orchard::note::Note, + _position: Position, + ) -> Option { + self.nk.as_ref().map(|key| note.nullifier(key)) + } + + fn account_id(&self) -> &zcash_primitives::zip32::AccountId { + &self.key_id.0 + } + + fn key_scope(&self) -> Option { + Some(self.key_id.1) + } +} + +/// A set of keys to be used in scanning for decryptable transaction outputs. +#[derive(Getters)] +#[getset(get = "pub(crate)")] +pub(crate) struct ScanningKeys { + sapling: HashMap>, + orchard: HashMap>, +} + +impl ScanningKeys { + /// Constructs a [`ScanningKeys`] from an iterator of [`zcash_keys::keys::UnifiedFullViewingKey`]s, + /// along with the account identifiers corresponding to those UFVKs. + pub(crate) fn from_account_ufvks( + ufvks: impl IntoIterator, + ) -> Self { + #![allow(clippy::type_complexity)] + + let mut sapling: HashMap> = + HashMap::new(); + let mut orchard: HashMap> = + HashMap::new(); + + for (account_id, ufvk) in ufvks { + if let Some(dfvk) = ufvk.sapling() { + for scope in [Scope::External, Scope::Internal] { + sapling.insert( + (account_id, scope), + ScanningKey { + key_id: (account_id, scope), + ivk: dfvk.to_ivk(scope), + nk: Some(dfvk.to_nk(scope)), + }, + ); + } + } + + if let Some(fvk) = ufvk.orchard() { + for scope in [Scope::External, Scope::Internal] { + orchard.insert( + (account_id, scope), + ScanningKey { + key_id: (account_id, scope), + ivk: fvk.to_ivk(scope), + nk: Some(fvk.clone()), + }, + ); + } + } + } + + Self { sapling, orchard } + } +} diff --git a/zingo-sync/src/lib.rs b/zingo-sync/src/lib.rs new file mode 100644 index 0000000000..da3250d433 --- /dev/null +++ b/zingo-sync/src/lib.rs @@ -0,0 +1,13 @@ +#![warn(missing_docs)] +//! Zingo sync engine prototype +//! +//! Entrypoint: [`crate::sync::sync`] + +pub mod client; +pub mod interface; +pub(crate) mod keys; +#[allow(missing_docs)] +pub mod primitives; +pub(crate) mod scan; +pub mod sync; +pub mod witness; diff --git a/zingo-sync/src/primitives.rs b/zingo-sync/src/primitives.rs new file mode 100644 index 0000000000..b609a15353 --- /dev/null +++ b/zingo-sync/src/primitives.rs @@ -0,0 +1,110 @@ +//! Module for primitive structs associated with the sync engine + +use std::collections::BTreeMap; + +use getset::{CopyGetters, Getters, MutGetters}; + +use zcash_client_backend::data_api::scanning::ScanRange; +use zcash_primitives::{block::BlockHash, consensus::BlockHeight, transaction::TxId}; + +/// Encapsulates the current state of sync +#[derive(Debug, Getters, MutGetters)] +#[getset(get = "pub", get_mut = "pub")] +pub struct SyncState { + scan_ranges: Vec, +} + +impl SyncState { + /// Create new SyncState + pub fn new() -> Self { + SyncState { + scan_ranges: Vec::new(), + } + } +} + +impl Default for SyncState { + fn default() -> Self { + Self::new() + } +} + +/// Output ID for a given pool type +#[derive(PartialEq, Eq, Hash, Clone, Copy, CopyGetters)] +#[getset(get_copy = "pub")] +pub struct OutputId { + /// ID of associated transaction + txid: TxId, + /// Index of output within the transactions bundle of the given pool type. + output_index: usize, +} + +impl OutputId { + /// Creates new OutputId from parts + pub fn from_parts(txid: TxId, output_index: usize) -> Self { + OutputId { txid, output_index } + } +} + +/// Binary tree map of nullifiers from transaction spends or actions +#[derive(Debug, MutGetters)] +#[getset(get = "pub", get_mut = "pub")] +pub struct NullifierMap { + sapling: BTreeMap, + orchard: BTreeMap, +} + +impl NullifierMap { + pub fn new() -> Self { + Self { + sapling: BTreeMap::new(), + orchard: BTreeMap::new(), + } + } +} + +impl Default for NullifierMap { + fn default() -> Self { + Self::new() + } +} + +/// Wallet block data +#[derive(Debug, Clone, CopyGetters)] +#[getset(get_copy = "pub")] +pub struct WalletBlock { + block_height: BlockHeight, + block_hash: BlockHash, + prev_hash: BlockHash, + time: u32, + #[getset(skip)] + txids: Vec, + sapling_commitment_tree_size: u32, + orchard_commitment_tree_size: u32, +} + +impl WalletBlock { + pub fn from_parts( + block_height: BlockHeight, + block_hash: BlockHash, + prev_hash: BlockHash, + time: u32, + txids: Vec, + sapling_commitment_tree_size: u32, + orchard_commitment_tree_size: u32, + ) -> Self { + Self { + block_height, + block_hash, + prev_hash, + time, + txids, + sapling_commitment_tree_size, + orchard_commitment_tree_size, + } + } + + pub fn txids(&self) -> &[TxId] { + &self.txids + } +} diff --git a/zingo-sync/src/scan.rs b/zingo-sync/src/scan.rs new file mode 100644 index 0000000000..820bc8393f --- /dev/null +++ b/zingo-sync/src/scan.rs @@ -0,0 +1,534 @@ +use std::{ + cmp, + collections::{BTreeMap, HashMap, HashSet}, +}; + +use incrementalmerkletree::{Marking, Position, Retention}; +use orchard::{note_encryption::CompactAction, tree::MerkleHashOrchard}; +use sapling_crypto::{note_encryption::CompactOutputDescription, Node}; +use tokio::sync::mpsc; +use zcash_client_backend::{ + data_api::scanning::ScanRange, + proto::compact_formats::{CompactBlock, CompactOrchardAction, CompactSaplingOutput, CompactTx}, +}; +use zcash_note_encryption::Domain; +use zcash_primitives::{ + block::BlockHash, + consensus::{BlockHeight, NetworkUpgrade, Parameters}, + transaction::TxId, +}; + +use crate::{ + client::{self, get_compact_block_range, FetchRequest}, + keys::{KeyId, ScanningKeyOps, ScanningKeys}, + primitives::{NullifierMap, OutputId, WalletBlock}, + witness::ShardTreeData, +}; + +use self::runners::{BatchRunners, DecryptedOutput}; + +pub(crate) mod runners; +pub(crate) mod workers; + +struct InitialScanData { + previous_block: Option, + sapling_initial_tree_size: u32, + orchard_initial_tree_size: u32, +} + +impl InitialScanData { + async fn new

( + fetch_request_sender: mpsc::UnboundedSender, + parameters: &P, + first_block: &CompactBlock, + previous_wallet_block: Option, + ) -> Result + where + P: Parameters + Sync + Send + 'static, + { + // gets initial tree size from previous block if available + // otherwise, from first block if available + // otherwise, fetches frontiers from server + let (sapling_initial_tree_size, orchard_initial_tree_size) = + if let Some(prev) = &previous_wallet_block { + ( + prev.sapling_commitment_tree_size(), + prev.orchard_commitment_tree_size(), + ) + } else if let Some(chain_metadata) = &first_block.chain_metadata { + // calculate initial tree size by subtracting number of outputs in block from the blocks final tree size + let sapling_output_count: u32 = first_block + .vtx + .iter() + .map(|tx| tx.outputs.len()) + .sum::() + .try_into() + .expect("Sapling output count cannot exceed a u32"); + let orchard_output_count: u32 = first_block + .vtx + .iter() + .map(|tx| tx.actions.len()) + .sum::() + .try_into() + .expect("Sapling output count cannot exceed a u32"); + + ( + chain_metadata + .sapling_commitment_tree_size + .checked_sub(sapling_output_count) + .unwrap(), + chain_metadata + .orchard_commitment_tree_size + .checked_sub(orchard_output_count) + .unwrap(), + ) + } else { + let sapling_activation_height = parameters + .activation_height(NetworkUpgrade::Sapling) + .expect("should have some sapling activation height"); + + match first_block.height().cmp(&sapling_activation_height) { + cmp::Ordering::Greater => { + let frontiers = + client::get_frontiers(fetch_request_sender, first_block.height() - 1) + .await + .unwrap(); + ( + frontiers.final_sapling_tree().tree_size() as u32, + frontiers.final_orchard_tree().tree_size() as u32, + ) + } + cmp::Ordering::Equal => (0, 0), + cmp::Ordering::Less => panic!("pre-sapling not supported!"), + } + }; + + Ok(InitialScanData { + previous_block: previous_wallet_block, + sapling_initial_tree_size, + orchard_initial_tree_size, + }) + } +} + +#[allow(dead_code)] +struct ScanData { + pub(crate) nullifiers: NullifierMap, + pub(crate) wallet_blocks: BTreeMap, + pub(crate) relevent_txids: HashSet, + pub(crate) decrypted_note_data: DecryptedNoteData, + pub(crate) shard_tree_data: ShardTreeData, +} + +pub(crate) struct ScanResults { + pub(crate) nullifiers: NullifierMap, + pub(crate) wallet_blocks: BTreeMap, + pub(crate) shard_tree_data: ShardTreeData, +} + +struct DecryptedNoteData { + sapling_nullifiers_and_positions: HashMap, + orchard_nullifiers_and_positions: HashMap, +} + +impl DecryptedNoteData { + fn new() -> Self { + DecryptedNoteData { + sapling_nullifiers_and_positions: HashMap::new(), + orchard_nullifiers_and_positions: HashMap::new(), + } + } +} + +impl Default for DecryptedNoteData { + fn default() -> Self { + Self::new() + } +} + +// scans a given range and returns all data relevent to the specified keys +// `previous_wallet_block` is the block with height [scan_range.start - 1] +pub(crate) async fn scan

( + fetch_request_sender: mpsc::UnboundedSender, + parameters: &P, + scanning_keys: &ScanningKeys, + scan_range: ScanRange, + previous_wallet_block: Option, +) -> Result +where + P: Parameters + Sync + Send + 'static, +{ + let compact_blocks = get_compact_block_range( + fetch_request_sender.clone(), + scan_range.block_range().clone(), + ) + .await + .unwrap(); + + let initial_scan_data = InitialScanData::new( + fetch_request_sender, + parameters, + compact_blocks + .first() + .expect("compacts blocks should not be empty"), + previous_wallet_block, + ) + .await + .unwrap(); + + let scan_data = + scan_compact_blocks(compact_blocks, parameters, scanning_keys, initial_scan_data).unwrap(); + + let ScanData { + nullifiers, + wallet_blocks, + relevent_txids: _, + decrypted_note_data: _, + shard_tree_data, + } = scan_data; + + // TODO: scan transactions + + Ok(ScanResults { + nullifiers, + wallet_blocks, + shard_tree_data, + }) +} + +#[allow(clippy::type_complexity)] +fn scan_compact_blocks

( + compact_blocks: Vec, + parameters: &P, + scanning_keys: &ScanningKeys, + initial_scan_data: InitialScanData, +) -> Result +where + P: Parameters + Sync + Send + 'static, +{ + check_continuity(&compact_blocks, initial_scan_data.previous_block.as_ref()).unwrap(); + + let mut runners = trial_decrypt(parameters, scanning_keys, &compact_blocks).unwrap(); + + let mut wallet_blocks: BTreeMap = BTreeMap::new(); + let mut nullifiers = NullifierMap::new(); + let mut relevent_txids: HashSet = HashSet::new(); + let mut decrypted_note_data = DecryptedNoteData::new(); + let mut shard_tree_data = ShardTreeData::new( + Position::from(u64::from(initial_scan_data.sapling_initial_tree_size)), + Position::from(u64::from(initial_scan_data.orchard_initial_tree_size)), + ); + let mut sapling_tree_size = initial_scan_data.sapling_initial_tree_size; + let mut orchard_tree_size = initial_scan_data.orchard_initial_tree_size; + for block in &compact_blocks { + let mut transactions = block.vtx.iter().peekable(); + while let Some(transaction) = transactions.next() { + // collect trial decryption results by transaction + let incoming_sapling_outputs = runners + .sapling + .collect_results(block.hash(), transaction.txid()); + let incoming_orchard_outputs = runners + .orchard + .collect_results(block.hash(), transaction.txid()); + + // gather the txids of all transactions relevent to the wallet + // the edge case of transactions that this capability created but did not receive change + // or create outgoing data is handled when the nullifiers are added and linked + incoming_sapling_outputs.iter().for_each(|(output_id, _)| { + relevent_txids.insert(output_id.txid()); + }); + incoming_orchard_outputs.iter().for_each(|(output_id, _)| { + relevent_txids.insert(output_id.txid()); + }); + // TODO: add outgoing outputs to relevent txids + + collect_nullifiers(&mut nullifiers, block.height(), transaction).unwrap(); + + shard_tree_data.sapling_leaves_and_retentions.extend( + calculate_sapling_leaves_and_retentions( + &transaction.outputs, + block.height(), + transactions.peek().is_none(), + &incoming_sapling_outputs, + ) + .unwrap(), + ); + shard_tree_data.orchard_leaves_and_retentions.extend( + calculate_orchard_leaves_and_retentions( + &transaction.actions, + block.height(), + transactions.peek().is_none(), + &incoming_orchard_outputs, + ) + .unwrap(), + ); + + calculate_nullifiers_and_positions( + sapling_tree_size, + scanning_keys.sapling(), + &incoming_sapling_outputs, + &mut decrypted_note_data.sapling_nullifiers_and_positions, + ); + calculate_nullifiers_and_positions( + orchard_tree_size, + scanning_keys.orchard(), + &incoming_orchard_outputs, + &mut decrypted_note_data.orchard_nullifiers_and_positions, + ); + + sapling_tree_size += u32::try_from(transaction.outputs.len()) + .expect("should not be more than 2^32 outputs in a transaction"); + orchard_tree_size += u32::try_from(transaction.actions.len()) + .expect("should not be more than 2^32 outputs in a transaction"); + } + + let wallet_block = WalletBlock::from_parts( + block.height(), + block.hash(), + block.prev_hash(), + block.time, + block.vtx.iter().map(|tx| tx.txid()).collect(), + sapling_tree_size, + orchard_tree_size, + ); + + check_tree_size(block, &wallet_block).unwrap(); + + wallet_blocks.insert(wallet_block.block_height(), wallet_block); + } + // TODO: map nullifiers + + Ok(ScanData { + nullifiers, + wallet_blocks, + relevent_txids, + decrypted_note_data, + shard_tree_data, + }) +} + +fn trial_decrypt

( + parameters: &P, + scanning_keys: &ScanningKeys, + compact_blocks: &[CompactBlock], +) -> Result, ()> +where + P: Parameters + Send + 'static, +{ + // TODO: add outgoing decryption + + let mut runners = BatchRunners::<(), ()>::for_keys(100, scanning_keys); + for block in compact_blocks { + runners.add_block(parameters, block.clone()).unwrap(); + } + runners.flush(); + + Ok(runners) +} + +// checks height and hash continuity of a batch of compact blocks. +// takes the last wallet compact block of the adjacent lower scan range, if available. +fn check_continuity( + compact_blocks: &[CompactBlock], + previous_compact_block: Option<&WalletBlock>, +) -> Result<(), ()> { + let mut prev_height: Option = None; + let mut prev_hash: Option = None; + + if let Some(prev) = previous_compact_block { + prev_height = Some(prev.block_height()); + prev_hash = Some(prev.block_hash()); + } + + for block in compact_blocks { + if let Some(prev_height) = prev_height { + if block.height() != prev_height + 1 { + panic!("height discontinuity"); + } + } + + if let Some(prev_hash) = prev_hash { + if block.prev_hash() != prev_hash { + panic!("hash discontinuity"); + } + } + + prev_height = Some(block.height()); + prev_hash = Some(block.hash()); + } + + Ok(()) +} + +fn check_tree_size(compact_block: &CompactBlock, wallet_block: &WalletBlock) -> Result<(), ()> { + if let Some(chain_metadata) = &compact_block.chain_metadata { + if chain_metadata.sapling_commitment_tree_size + != wallet_block.sapling_commitment_tree_size() + { + panic!("sapling tree size is incorrect!") + } + if chain_metadata.orchard_commitment_tree_size + != wallet_block.orchard_commitment_tree_size() + { + panic!("orchard tree size is incorrect!") + } + } + + Ok(()) +} + +// calculates nullifiers and positions of incoming decrypted outputs for a given compact transaction and insert into hash map +// `tree_size` is the tree size of the corresponding shielded pool up to - and not including - the compact transaction +// being processed +fn calculate_nullifiers_and_positions( + tree_size: u32, + keys: &HashMap, + incoming_decrypted_outputs: &HashMap>, + nullifiers_and_positions: &mut HashMap, +) where + D: Domain, + K: ScanningKeyOps, +{ + incoming_decrypted_outputs + .iter() + .for_each(|(output_id, incoming_output)| { + let position = Position::from(u64::from( + tree_size + u32::try_from(output_id.output_index()).unwrap(), + )); + let key = keys + .get(&incoming_output.ivk_tag) + .expect("key should be available as it was used to decrypt output"); + let nullifier = key + .nf(&incoming_output.note, position) + .expect("only fvks currently supported"); + nullifiers_and_positions.insert(*output_id, (nullifier, position)); + }); +} + +// TODO: unify sapling and orchard leaf and retention fns +// calculates the sapling note commitment tree leaves and shardtree retentions for a given compact transaction +fn calculate_sapling_leaves_and_retentions( + outputs: &[CompactSaplingOutput], + block_height: BlockHeight, + last_outputs_in_block: bool, + incoming_decrypted_outputs: &HashMap>, +) -> Result)>, ()> { + let incoming_output_indices: Vec = incoming_decrypted_outputs + .keys() + .copied() + .map(|output_id| output_id.output_index()) + .collect(); + + if outputs.is_empty() { + Ok(Vec::new()) + } else { + let last_output_index = outputs.len() - 1; + + let leaves_and_retentions = outputs + .iter() + .enumerate() + .map(|(output_index, output)| { + let note_commitment = CompactOutputDescription::try_from(output).unwrap().cmu; + let leaf = sapling_crypto::Node::from_cmu(¬e_commitment); + + let last_output_in_block: bool = + last_outputs_in_block && output_index == last_output_index; + let decrypted: bool = incoming_output_indices.contains(&output_index); + let retention = match (decrypted, last_output_in_block) { + (is_marked, true) => Retention::Checkpoint { + id: block_height, + marking: if is_marked { + Marking::Marked + } else { + Marking::None + }, + }, + (true, false) => Retention::Marked, + (false, false) => Retention::Ephemeral, + }; + + (leaf, retention) + }) + .collect(); + + Ok(leaves_and_retentions) + } +} +// calculates the orchard note commitment tree leaves and shardtree retentions for a given compact transaction +fn calculate_orchard_leaves_and_retentions( + actions: &[CompactOrchardAction], + block_height: BlockHeight, + last_outputs_in_block: bool, + incoming_decrypted_outputs: &HashMap>, +) -> Result)>, ()> { + let incoming_output_indices: Vec = incoming_decrypted_outputs + .keys() + .copied() + .map(|output_id| output_id.output_index()) + .collect(); + + if actions.is_empty() { + Ok(Vec::new()) + } else { + let last_output_index = actions.len() - 1; + + let leaves_and_retentions = actions + .iter() + .enumerate() + .map(|(output_index, output)| { + let note_commitment = CompactAction::try_from(output).unwrap().cmx(); + let leaf = MerkleHashOrchard::from_cmx(¬e_commitment); + + let last_output_in_block: bool = + last_outputs_in_block && output_index == last_output_index; + let decrypted: bool = incoming_output_indices.contains(&output_index); + let retention = match (decrypted, last_output_in_block) { + (is_marked, true) => Retention::Checkpoint { + id: block_height, + marking: if is_marked { + Marking::Marked + } else { + Marking::None + }, + }, + (true, false) => Retention::Marked, + (false, false) => Retention::Ephemeral, + }; + + (leaf, retention) + }) + .collect(); + + Ok(leaves_and_retentions) + } +} + +// converts and adds the nullifiers from a compact transaction to the nullifier map +fn collect_nullifiers( + nullifier_map: &mut NullifierMap, + block_height: BlockHeight, + transaction: &CompactTx, +) -> Result<(), ()> { + transaction + .spends + .iter() + .map(|spend| sapling_crypto::Nullifier::from_slice(spend.nf.as_slice()).unwrap()) + .for_each(|nullifier| { + nullifier_map + .sapling_mut() + .insert(nullifier, (block_height, transaction.txid())); + }); + transaction + .actions + .iter() + .map(|action| { + orchard::note::Nullifier::from_bytes(action.nullifier.as_slice().try_into().unwrap()) + .unwrap() + }) + .for_each(|nullifier| { + nullifier_map + .orchard_mut() + .insert(nullifier, (block_height, transaction.txid())); + }); + Ok(()) +} diff --git a/zingo-sync/src/scan/runners.rs b/zingo-sync/src/scan/runners.rs new file mode 100644 index 0000000000..37b8f487e9 --- /dev/null +++ b/zingo-sync/src/scan/runners.rs @@ -0,0 +1,606 @@ +//! Temporary copy of LRZ batch runners while we wait for their exposition and update LRZ + +use std::collections::HashMap; +use std::fmt; +use std::mem; +use std::sync::atomic::AtomicUsize; + +use crossbeam_channel as channel; + +use orchard::note_encryption::CompactAction; +use orchard::note_encryption::OrchardDomain; +use sapling_crypto::note_encryption::CompactOutputDescription; +use sapling_crypto::note_encryption::SaplingDomain; + +use zcash_client_backend::proto::compact_formats::CompactBlock; +use zcash_client_backend::scanning::ScanError; +use zcash_client_backend::ShieldedProtocol; +use zcash_note_encryption::{batch, BatchDomain, Domain, ShieldedOutput, COMPACT_NOTE_SIZE}; +use zcash_primitives::consensus; +use zcash_primitives::transaction::components::sapling::zip212_enforcement; +use zcash_primitives::{block::BlockHash, transaction::TxId}; + +use memuse::DynamicUsage; + +use crate::keys::KeyId; +use crate::keys::ScanningKeyOps as _; +use crate::keys::ScanningKeys; +use crate::primitives::OutputId; + +type TaggedSaplingBatch = Batch< + SaplingDomain, + sapling_crypto::note_encryption::CompactOutputDescription, + CompactDecryptor, +>; +type TaggedSaplingBatchRunner = BatchRunner< + SaplingDomain, + sapling_crypto::note_encryption::CompactOutputDescription, + CompactDecryptor, + Tasks, +>; + +type TaggedOrchardBatch = + Batch; +type TaggedOrchardBatchRunner = + BatchRunner; + +pub(crate) trait SaplingTasks: Tasks {} +impl> SaplingTasks for T {} + +pub(crate) trait OrchardTasks: Tasks {} +impl> OrchardTasks for T {} + +pub(crate) struct BatchRunners { + pub(crate) sapling: TaggedSaplingBatchRunner, + pub(crate) orchard: TaggedOrchardBatchRunner, +} + +impl BatchRunners +where + TS: SaplingTasks, + TO: OrchardTasks, +{ + pub(crate) fn for_keys(batch_size_threshold: usize, scanning_keys: &ScanningKeys) -> Self { + BatchRunners { + sapling: BatchRunner::new( + batch_size_threshold, + scanning_keys + .sapling() + .iter() + .map(|(id, key)| (*id, key.prepare())), + ), + orchard: BatchRunner::new( + batch_size_threshold, + scanning_keys + .orchard() + .iter() + .map(|(id, key)| (*id, key.prepare())), + ), + } + } + + pub(crate) fn flush(&mut self) { + self.sapling.flush(); + self.orchard.flush(); + } + + #[tracing::instrument(skip_all, fields(height = block.height))] + pub(crate) fn add_block

(&mut self, params: &P, block: CompactBlock) -> Result<(), ScanError> + where + P: consensus::Parameters + Send + 'static, + { + let block_hash = block.hash(); + let block_height = block.height(); + let zip212_enforcement = zip212_enforcement(params, block_height); + + for tx in block.vtx.into_iter() { + let txid = tx.txid(); + + self.sapling.add_outputs( + block_hash, + txid, + |_| SaplingDomain::new(zip212_enforcement), + &tx.outputs + .iter() + .enumerate() + .map(|(i, output)| { + CompactOutputDescription::try_from(output).map_err(|_| { + ScanError::EncodingInvalid { + at_height: block_height, + txid, + pool_type: ShieldedProtocol::Sapling, + index: i, + } + }) + }) + .collect::, _>>()?, + ); + + self.orchard.add_outputs( + block_hash, + txid, + OrchardDomain::for_compact_action, + &tx.actions + .iter() + .enumerate() + .map(|(i, action)| { + CompactAction::try_from(action).map_err(|_| ScanError::EncodingInvalid { + at_height: block_height, + txid, + pool_type: ShieldedProtocol::Orchard, + index: i, + }) + }) + .collect::, _>>()?, + ); + } + + Ok(()) + } +} + +/// A decrypted transaction output. +pub(crate) struct DecryptedOutput { + /// The tag corresponding to the incoming viewing key used to decrypt the note. + pub(crate) ivk_tag: KeyId, + /// The recipient of the note. + pub(crate) recipient: D::Recipient, + /// The note! + pub(crate) note: D::Note, + /// The memo field, or `()` if this is a decrypted compact output. + pub(crate) memo: M, +} + +impl fmt::Debug for DecryptedOutput +where + D::IncomingViewingKey: fmt::Debug, + D::Recipient: fmt::Debug, + D::Note: fmt::Debug, + M: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("DecryptedOutput") + .field("ivk_tag", &self.ivk_tag) + .field("recipient", &self.recipient) + .field("note", &self.note) + .field("memo", &self.memo) + .finish() + } +} + +/// A decryptor of transaction outputs. +pub(crate) trait Decryptor { + type Memo; + + // Once we reach MSRV 1.75.0, this can return `impl Iterator`. + fn batch_decrypt( + tags: &[KeyId], + ivks: &[D::IncomingViewingKey], + outputs: &[(D, Output)], + ) -> Vec>>; +} + +/// A decryptor of outputs as encoded in compact blocks. +pub(crate) struct CompactDecryptor; + +impl> Decryptor + for CompactDecryptor +{ + type Memo = (); + + fn batch_decrypt( + tags: &[KeyId], + ivks: &[D::IncomingViewingKey], + outputs: &[(D, Output)], + ) -> Vec>> { + batch::try_compact_note_decryption(ivks, outputs) + .into_iter() + .map(|res| { + res.map(|((note, recipient), ivk_idx)| DecryptedOutput { + ivk_tag: tags[ivk_idx], + recipient, + note, + memo: (), + }) + }) + .collect() + } +} + +/// A value correlated with an output index. +struct OutputIndex { + /// The index of the output within the corresponding shielded bundle. + output_index: usize, + /// The value for the output index. + value: V, +} + +type OutputItem = OutputIndex>; + +/// The sender for the result of batch scanning a specific transaction output. +struct OutputReplier(OutputIndex>>); + +impl DynamicUsage for OutputReplier { + #[inline(always)] + fn dynamic_usage(&self) -> usize { + // We count the memory usage of items in the channel on the receiver side. + 0 + } + + #[inline(always)] + fn dynamic_usage_bounds(&self) -> (usize, Option) { + (0, Some(0)) + } +} + +/// The receiver for the result of batch scanning a specific transaction. +struct BatchReceiver(channel::Receiver>); + +impl DynamicUsage for BatchReceiver { + fn dynamic_usage(&self) -> usize { + // We count the memory usage of items in the channel on the receiver side. + let num_items = self.0.len(); + + // We know we use unbounded channels, so the items in the channel are stored as a + // linked list. `crossbeam_channel` allocates memory for the linked list in blocks + // of 31 items. + const ITEMS_PER_BLOCK: usize = 31; + let num_blocks = (num_items + ITEMS_PER_BLOCK - 1) / ITEMS_PER_BLOCK; + + // The structure of a block is: + // - A pointer to the next block. + // - For each slot in the block: + // - Space for an item. + // - The state of the slot, stored as an AtomicUsize. + const PTR_SIZE: usize = std::mem::size_of::(); + let item_size = std::mem::size_of::>(); + const ATOMIC_USIZE_SIZE: usize = std::mem::size_of::(); + let block_size = PTR_SIZE + ITEMS_PER_BLOCK * (item_size + ATOMIC_USIZE_SIZE); + + num_blocks * block_size + } + + fn dynamic_usage_bounds(&self) -> (usize, Option) { + let usage = self.dynamic_usage(); + (usage, Some(usage)) + } +} + +/// A tracker for the batch scanning tasks that are currently running. +/// +/// This enables a [`BatchRunner`] to be optionally configured to track heap memory usage. +pub(crate) trait Tasks { + type Task: Task; + fn new() -> Self; + fn add_task(&self, item: Item) -> Self::Task; + fn run_task(&self, item: Item) { + let task = self.add_task(item); + rayon::spawn_fifo(|| task.run()); + } +} + +/// A batch scanning task. +pub(crate) trait Task: Send + 'static { + fn run(self); +} + +impl Tasks for () { + type Task = Item; + fn new() -> Self {} + fn add_task(&self, item: Item) -> Self::Task { + // Return the item itself as the task; we aren't tracking anything about it, so + // there is no need to wrap it in a newtype. + item + } +} + +/// A batch of outputs to trial decrypt. +pub(crate) struct Batch> { + tags: Vec, + ivks: Vec, + /// We currently store outputs and repliers as parallel vectors, because + /// [`batch::try_note_decryption`] accepts a slice of domain/output pairs + /// rather than a value that implements `IntoIterator`, and therefore we + /// can't just use `map` to select the parts we need in order to perform + /// batch decryption. Ideally the domain, output, and output replier would + /// all be part of the same struct, which would also track the output index + /// (that is captured in the outer `OutputIndex` of each `OutputReplier`). + outputs: Vec<(D, Output)>, + repliers: Vec>, +} + +impl DynamicUsage for Batch +where + D: BatchDomain + DynamicUsage, + D::IncomingViewingKey: DynamicUsage, + Output: DynamicUsage, + Dec: Decryptor, +{ + fn dynamic_usage(&self) -> usize { + self.tags.dynamic_usage() + + self.ivks.dynamic_usage() + + self.outputs.dynamic_usage() + + self.repliers.dynamic_usage() + } + + fn dynamic_usage_bounds(&self) -> (usize, Option) { + let (tags_lower, tags_upper) = self.tags.dynamic_usage_bounds(); + let (ivks_lower, ivks_upper) = self.ivks.dynamic_usage_bounds(); + let (outputs_lower, outputs_upper) = self.outputs.dynamic_usage_bounds(); + let (repliers_lower, repliers_upper) = self.repliers.dynamic_usage_bounds(); + + ( + tags_lower + ivks_lower + outputs_lower + repliers_lower, + tags_upper + .zip(ivks_upper) + .zip(outputs_upper) + .zip(repliers_upper) + .map(|(((a, b), c), d)| a + b + c + d), + ) + } +} + +impl Batch +where + D: BatchDomain, + Dec: Decryptor, +{ + /// Constructs a new batch. + fn new(tags: Vec, ivks: Vec) -> Self { + assert_eq!(tags.len(), ivks.len()); + Self { + tags, + ivks, + outputs: vec![], + repliers: vec![], + } + } + + /// Returns `true` if the batch is currently empty. + fn is_empty(&self) -> bool { + self.outputs.is_empty() + } +} + +impl Task for Batch +where + D: BatchDomain + Send + 'static, + D::IncomingViewingKey: Send, + D::Memo: Send, + D::Note: Send, + D::Recipient: Send, + Output: Send + 'static, + Dec: Decryptor + 'static, + Dec::Memo: Send, +{ + /// Runs the batch of trial decryptions, and reports the results. + fn run(self) { + // Deconstruct self so we can consume the pieces individually. + let Self { + tags, + ivks, + outputs, + repliers, + } = self; + + assert_eq!(outputs.len(), repliers.len()); + + let decryption_results = Dec::batch_decrypt(&tags, &ivks, &outputs); + for (decryption_result, OutputReplier(replier)) in + decryption_results.into_iter().zip(repliers.into_iter()) + { + // If `decryption_result` is `None` then we will just drop `replier`, + // indicating to the parent `BatchRunner` that this output was not for us. + if let Some(value) = decryption_result { + let result = OutputIndex { + output_index: replier.output_index, + value, + }; + + if replier.value.send(result).is_err() { + tracing::debug!("BatchRunner was dropped before batch finished"); + break; + } + } + } + } +} + +impl Batch +where + D: BatchDomain, + Output: Clone, + Dec: Decryptor, +{ + /// Adds the given outputs to this batch. + /// + /// `replier` will be called with the result of every output. + fn add_outputs( + &mut self, + domain: impl Fn(&Output) -> D, + outputs: &[Output], + replier: channel::Sender>, + ) { + self.outputs.extend( + outputs + .iter() + .cloned() + .map(|output| (domain(&output), output)), + ); + self.repliers.extend((0..outputs.len()).map(|output_index| { + OutputReplier(OutputIndex { + output_index, + value: replier.clone(), + }) + })); + } +} + +/// A `HashMap` key for looking up the result of a batch scanning a specific transaction. +#[derive(PartialEq, Eq, Hash)] +struct ResultKey(BlockHash, TxId); + +impl DynamicUsage for ResultKey { + #[inline(always)] + fn dynamic_usage(&self) -> usize { + 0 + } + + #[inline(always)] + fn dynamic_usage_bounds(&self) -> (usize, Option) { + (0, Some(0)) + } +} + +/// Logic to run batches of trial decryptions on the global threadpool. +pub(crate) struct BatchRunner +where + D: BatchDomain, + Dec: Decryptor, + T: Tasks>, +{ + batch_size_threshold: usize, + // The batch currently being accumulated. + acc: Batch, + // The running batches. + running_tasks: T, + // Receivers for the results of the running batches. + pending_results: HashMap>, +} + +impl DynamicUsage for BatchRunner +where + D: BatchDomain + DynamicUsage, + D::IncomingViewingKey: DynamicUsage, + Output: DynamicUsage, + Dec: Decryptor, + T: Tasks> + DynamicUsage, +{ + fn dynamic_usage(&self) -> usize { + self.acc.dynamic_usage() + + self.running_tasks.dynamic_usage() + + self.pending_results.dynamic_usage() + } + + fn dynamic_usage_bounds(&self) -> (usize, Option) { + let running_usage = self.running_tasks.dynamic_usage(); + + let bounds = ( + self.acc.dynamic_usage_bounds(), + self.pending_results.dynamic_usage_bounds(), + ); + ( + bounds.0 .0 + running_usage + bounds.1 .0, + bounds + .0 + .1 + .zip(bounds.1 .1) + .map(|(a, b)| a + running_usage + b), + ) + } +} + +impl BatchRunner +where + D: BatchDomain, + Dec: Decryptor, + T: Tasks>, +{ + /// Constructs a new batch runner for the given incoming viewing keys. + pub(crate) fn new( + batch_size_threshold: usize, + ivks: impl Iterator, + ) -> Self { + let (tags, ivks) = ivks.unzip(); + Self { + batch_size_threshold, + acc: Batch::new(tags, ivks), + running_tasks: T::new(), + pending_results: HashMap::default(), + } + } +} + +impl BatchRunner +where + D: BatchDomain + Send + 'static, + D::IncomingViewingKey: Clone + Send, + D::Memo: Send, + D::Note: Send, + D::Recipient: Send, + Output: Clone + Send + 'static, + Dec: Decryptor, + T: Tasks>, +{ + /// Batches the given outputs for trial decryption. + /// + /// `block_tag` is the hash of the block that triggered this txid being added to the + /// batch, or the all-zeros hash to indicate that no block triggered it (i.e. it was a + /// mempool change). + /// + /// If after adding the given outputs, the accumulated batch size is at least the size + /// threshold that was set via `Self::new`, `Self::flush` is called. Subsequent calls + /// to `Self::add_outputs` will be accumulated into a new batch. + pub(crate) fn add_outputs( + &mut self, + block_tag: BlockHash, + txid: TxId, + domain: impl Fn(&Output) -> D, + outputs: &[Output], + ) { + let (tx, rx) = channel::unbounded(); + self.acc.add_outputs(domain, outputs, tx); + self.pending_results + .insert(ResultKey(block_tag, txid), BatchReceiver(rx)); + + if self.acc.outputs.len() >= self.batch_size_threshold { + self.flush(); + } + } + + /// Runs the currently accumulated batch on the global threadpool. + /// + /// Subsequent calls to `Self::add_outputs` will be accumulated into a new batch. + pub(crate) fn flush(&mut self) { + if !self.acc.is_empty() { + let mut batch = Batch::new(self.acc.tags.clone(), self.acc.ivks.clone()); + mem::swap(&mut batch, &mut self.acc); + self.running_tasks.run_task(batch); + } + } + + /// Collects the pending decryption results for the given transaction. + /// + /// `block_tag` is the hash of the block that triggered this txid being added to the + /// batch, or the all-zeros hash to indicate that no block triggered it (i.e. it was a + /// mempool change). + pub(crate) fn collect_results( + &mut self, + block_tag: BlockHash, + txid: TxId, + ) -> HashMap> { + self.pending_results + .remove(&ResultKey(block_tag, txid)) + // We won't have a pending result if the transaction didn't have outputs of + // this runner's kind. + .map(|BatchReceiver(rx)| { + // This iterator will end once the channel becomes empty and disconnected. + // We created one sender per output, and each sender is dropped after the + // batch it is in completes (and in the case of successful decryptions, + // after the decrypted note has been sent to the channel). Completion of + // the iterator therefore corresponds to complete knowledge of the outputs + // of this transaction that could be decrypted. + rx.into_iter() + .map( + |OutputIndex { + output_index, + value, + }| { + (OutputId::from_parts(txid, output_index), value) + }, + ) + .collect() + }) + .unwrap_or_default() + } +} diff --git a/zingo-sync/src/scan/workers.rs b/zingo-sync/src/scan/workers.rs new file mode 100644 index 0000000000..d8a785d4ef --- /dev/null +++ b/zingo-sync/src/scan/workers.rs @@ -0,0 +1,173 @@ +use std::{ + collections::HashMap, + sync::{ + atomic::{self, AtomicBool}, + Arc, + }, +}; + +use tokio::{sync::mpsc, task::JoinHandle}; + +use zcash_client_backend::data_api::scanning::ScanRange; +use zcash_keys::keys::UnifiedFullViewingKey; +use zcash_primitives::{consensus::Parameters, zip32::AccountId}; + +use crate::{client::FetchRequest, keys::ScanningKeys, primitives::WalletBlock}; + +use super::{scan, ScanResults}; + +const SCAN_WORKER_POOLSIZE: usize = 2; + +pub(crate) struct Scanner

{ + workers: Vec, + scan_results_sender: mpsc::UnboundedSender<(ScanRange, ScanResults)>, + fetch_request_sender: mpsc::UnboundedSender, + parameters: P, + ufvks: HashMap, +} + +// TODO: add fn for checking and handling worker errors +impl

Scanner

+where + P: Parameters + Sync + Send + 'static, +{ + pub(crate) fn new( + scan_results_sender: mpsc::UnboundedSender<(ScanRange, ScanResults)>, + fetch_request_sender: mpsc::UnboundedSender, + parameters: P, + ufvks: HashMap, + ) -> Self { + let workers: Vec = Vec::with_capacity(SCAN_WORKER_POOLSIZE); + + Self { + workers, + scan_results_sender, + fetch_request_sender, + parameters, + ufvks, + } + } + + pub(crate) fn spawn_workers(&mut self) { + for _ in 0..SCAN_WORKER_POOLSIZE { + let (scan_task_sender, scan_task_receiver) = mpsc::unbounded_channel(); + let worker = ScanWorker::new( + scan_task_receiver, + self.scan_results_sender.clone(), + self.fetch_request_sender.clone(), + self.parameters.clone(), + self.ufvks.clone(), + ); + let is_scanning = Arc::clone(&worker.is_scanning); + let handle = tokio::spawn(async move { worker.run().await }); + self.workers.push(WorkerHandle { + _handle: handle, + is_scanning, + scan_task_sender, + }); + } + } + + pub(crate) fn is_worker_idle(&self) -> bool { + self.workers.iter().any(|worker| !worker.is_scanning()) + } + + pub(crate) fn add_scan_task(&self, scan_task: ScanTask) -> Result<(), ()> { + if let Some(worker) = self.workers.iter().find(|worker| !worker.is_scanning()) { + worker.add_scan_task(scan_task); + } else { + panic!("no idle workers!") + } + + Ok(()) + } +} + +struct WorkerHandle { + _handle: JoinHandle>, + is_scanning: Arc, + scan_task_sender: mpsc::UnboundedSender, +} + +impl WorkerHandle { + fn is_scanning(&self) -> bool { + self.is_scanning.load(atomic::Ordering::Acquire) + } + + fn add_scan_task(&self, scan_task: ScanTask) { + self.scan_task_sender.send(scan_task).unwrap(); + } +} + +struct ScanWorker

{ + is_scanning: Arc, + scan_task_receiver: mpsc::UnboundedReceiver, + scan_results_sender: mpsc::UnboundedSender<(ScanRange, ScanResults)>, + fetch_request_sender: mpsc::UnboundedSender, + parameters: P, + scanning_keys: ScanningKeys, +} + +impl

ScanWorker

+where + P: Parameters + Sync + Send + 'static, +{ + fn new( + scan_task_receiver: mpsc::UnboundedReceiver, + scan_results_sender: mpsc::UnboundedSender<(ScanRange, ScanResults)>, + fetch_request_sender: mpsc::UnboundedSender, + parameters: P, + ufvks: HashMap, + ) -> Self { + let scanning_keys = ScanningKeys::from_account_ufvks(ufvks); + Self { + is_scanning: Arc::new(AtomicBool::new(false)), + scan_task_receiver, + scan_results_sender, + fetch_request_sender, + parameters, + scanning_keys, + } + } + + async fn run(mut self) -> Result<(), ()> { + while let Some(scan_task) = self.scan_task_receiver.recv().await { + self.is_scanning.store(true, atomic::Ordering::Release); + + let scan_results = scan( + self.fetch_request_sender.clone(), + &self.parameters.clone(), + &self.scanning_keys, + scan_task.scan_range.clone(), + scan_task.previous_wallet_block, + ) + .await + .unwrap(); + + self.scan_results_sender + .send((scan_task.scan_range, scan_results)) + .unwrap(); + + self.is_scanning.store(false, atomic::Ordering::Release); + } + + Ok(()) + } +} + +pub(crate) struct ScanTask { + scan_range: ScanRange, + previous_wallet_block: Option, +} + +impl ScanTask { + pub(crate) fn from_parts( + scan_range: ScanRange, + previous_wallet_block: Option, + ) -> Self { + Self { + scan_range, + previous_wallet_block, + } + } +} diff --git a/zingo-sync/src/sync.rs b/zingo-sync/src/sync.rs new file mode 100644 index 0000000000..3aaf4ab91a --- /dev/null +++ b/zingo-sync/src/sync.rs @@ -0,0 +1,259 @@ +//! Entrypoint for sync engine + +use std::cmp; +use std::ops::Range; +use std::time::Duration; + +use crate::client::FetchRequest; +use crate::client::{fetch::fetch, get_chain_height}; +use crate::interface::{SyncBlocks, SyncNullifiers, SyncShardTrees, SyncWallet}; +use crate::primitives::SyncState; +use crate::scan::workers::{ScanTask, Scanner}; +use crate::scan::ScanResults; + +use tokio::sync::mpsc::error::TryRecvError; +use zcash_client_backend::{ + data_api::scanning::{ScanPriority, ScanRange}, + proto::service::compact_tx_streamer_client::CompactTxStreamerClient, +}; +use zcash_primitives::consensus::{BlockHeight, NetworkUpgrade, Parameters}; + +use futures::future::try_join_all; +use tokio::sync::mpsc; + +const BATCH_SIZE: u32 = 10; +// const BATCH_SIZE: u32 = 1_000; + +/// Syncs a wallet to the latest state of the blockchain +pub async fn sync( + client: CompactTxStreamerClient, + parameters: &P, + wallet: &mut W, +) -> Result<(), ()> +where + P: Parameters + Sync + Send + 'static, + W: SyncWallet + SyncBlocks + SyncNullifiers + SyncShardTrees, +{ + tracing::info!("Syncing wallet..."); + + let mut handles = Vec::new(); + + // create channel for sending fetch requests and launch fetcher task + let (fetch_request_sender, fetch_request_receiver) = mpsc::unbounded_channel(); + let fetcher_handle = tokio::spawn(fetch(fetch_request_receiver, client)); + handles.push(fetcher_handle); + + update_scan_ranges( + fetch_request_sender.clone(), + parameters, + wallet.get_birthday().unwrap(), + wallet.get_sync_state_mut().unwrap(), + ) + .await + .unwrap(); + + // create channel for receiving scan results and launch scanner + let (scan_results_sender, mut scan_results_receiver) = mpsc::unbounded_channel(); + let ufvks = wallet.get_unified_full_viewing_keys().unwrap(); + let mut scanner = Scanner::new( + scan_results_sender, + fetch_request_sender, + parameters.clone(), + ufvks, + ); + scanner.spawn_workers(); + + let mut interval = tokio::time::interval(Duration::from_millis(30)); + loop { + interval.tick().await; + + // if a scan worker is idle, send it a new scan task + if scanner.is_worker_idle() { + if let Some(scan_range) = prepare_next_scan_range(wallet.get_sync_state_mut().unwrap()) + { + let previous_wallet_block = wallet + .get_wallet_block(scan_range.block_range().start - 1) + .ok(); + + scanner + .add_scan_task(ScanTask::from_parts(scan_range, previous_wallet_block)) + .unwrap(); + } else { + // when no more ranges are available to scan, break out of the loop + break; + } + } + + match scan_results_receiver.try_recv() { + Ok((scan_range, scan_results)) => { + update_wallet_data(wallet, scan_results).unwrap(); + // TODO: link nullifiers and scan linked transactions + remove_irrelevant_data(wallet, &scan_range).unwrap(); + mark_scanned(scan_range, wallet.get_sync_state_mut().unwrap()).unwrap(); + } + Err(TryRecvError::Empty) => (), + Err(TryRecvError::Disconnected) => break, + } + } + + drop(scanner); + while let Some((scan_range, scan_results)) = scan_results_receiver.recv().await { + update_wallet_data(wallet, scan_results).unwrap(); + // TODO: link nullifiers and scan linked transactions + remove_irrelevant_data(wallet, &scan_range).unwrap(); + mark_scanned(scan_range, wallet.get_sync_state_mut().unwrap()).unwrap(); + } + + try_join_all(handles).await.unwrap(); + + Ok(()) +} + +// update scan_ranges to include blocks between the last known chain height (wallet height) and the chain height from the server +async fn update_scan_ranges

( + fetch_request_sender: mpsc::UnboundedSender, + parameters: &P, + wallet_birthday: BlockHeight, + sync_state: &mut SyncState, +) -> Result<(), ()> +where + P: Parameters, +{ + let chain_height = get_chain_height(fetch_request_sender).await.unwrap(); + + let scan_ranges = sync_state.scan_ranges_mut(); + + let wallet_height = if scan_ranges.is_empty() { + let sapling_activation_height = parameters + .activation_height(NetworkUpgrade::Sapling) + .expect("sapling activation height should always return Some"); + + match wallet_birthday.cmp(&sapling_activation_height) { + cmp::Ordering::Greater | cmp::Ordering::Equal => wallet_birthday, + cmp::Ordering::Less => sapling_activation_height, + } + } else { + scan_ranges + .last() + .expect("Vec should not be empty") + .block_range() + .end + }; + + if wallet_height > chain_height { + panic!("wallet is ahead of server!") + } + + let chain_tip_scan_range = ScanRange::from_parts( + Range { + start: wallet_height, + end: chain_height + 1, + }, + ScanPriority::Historic, + ); + scan_ranges.push(chain_tip_scan_range); + + if scan_ranges.is_empty() { + panic!("scan ranges should never be empty after updating") + } + + // TODO: add logic to combine chain tip scan range with wallet tip scan range + // TODO: add scan priority logic + + Ok(()) +} + +// returns `None` if there are no more ranges to scan +fn prepare_next_scan_range(sync_state: &mut SyncState) -> Option { + let scan_ranges = sync_state.scan_ranges_mut(); + + // placeholder for algorythm that determines highest priority range to scan + let (index, selected_scan_range) = scan_ranges.iter_mut().enumerate().find(|(_, range)| { + range.priority() != ScanPriority::Scanned && range.priority() != ScanPriority::Ignored + })?; + + // if scan range is larger than BATCH_SIZE, split off and return a batch from the lower end and update scan ranges + if let Some((lower_range, higher_range)) = selected_scan_range + .split_at(selected_scan_range.block_range().start + BlockHeight::from_u32(BATCH_SIZE)) + { + let lower_range_ignored = + ScanRange::from_parts(lower_range.block_range().clone(), ScanPriority::Ignored); + scan_ranges.splice(index..=index, vec![lower_range_ignored, higher_range]); + + Some(lower_range) + } else { + let selected_scan_range = selected_scan_range.clone(); + let selected_range_ignored = ScanRange::from_parts( + selected_scan_range.block_range().clone(), + ScanPriority::Ignored, + ); + scan_ranges.splice(index..=index, vec![selected_range_ignored]); + + Some(selected_scan_range.clone()) + } +} + +fn mark_scanned(scan_range: ScanRange, sync_state: &mut SyncState) -> Result<(), ()> { + let scan_ranges = sync_state.scan_ranges_mut(); + + if let Some((index, range)) = scan_ranges + .iter() + .enumerate() + .find(|(_, range)| range.block_range() == scan_range.block_range()) + { + scan_ranges[index] = + ScanRange::from_parts(range.block_range().clone(), ScanPriority::Scanned); + } else { + panic!("scanned range not found!") + } + // TODO: also combine adjacent scanned ranges together + + Ok(()) +} + +fn update_wallet_data(wallet: &mut W, scan_results: ScanResults) -> Result<(), ()> +where + W: SyncBlocks + SyncNullifiers + SyncShardTrees, +{ + let ScanResults { + nullifiers, + wallet_blocks, + shard_tree_data, + } = scan_results; + + // TODO: if scan priority is historic, retain only relevent blocks and nullifiers as we have all information and requires a lot of memory / storage + // must still retain top 100 blocks for re-org purposes + wallet.append_wallet_blocks(wallet_blocks).unwrap(); + wallet.append_nullifiers(nullifiers).unwrap(); + wallet.update_shard_trees(shard_tree_data).unwrap(); + // TODO: add trait to save wallet data to persistance for in-memory wallets + + Ok(()) +} + +fn remove_irrelevant_data(wallet: &mut W, scan_range: &ScanRange) -> Result<(), ()> +where + W: SyncBlocks + SyncNullifiers, +{ + if scan_range.priority() != ScanPriority::Historic { + return Ok(()); + } + + // TODO: also retain blocks that contain transactions relevant to the wallet + wallet + .get_wallet_blocks_mut() + .unwrap() + .retain(|height, _| *height >= scan_range.block_range().end); + wallet + .get_nullifiers_mut() + .unwrap() + .sapling_mut() + .retain(|_, (height, _)| *height >= scan_range.block_range().end); + wallet + .get_nullifiers_mut() + .unwrap() + .orchard_mut() + .retain(|_, (height, _)| *height >= scan_range.block_range().end); + + Ok(()) +} diff --git a/zingo-sync/src/witness.rs b/zingo-sync/src/witness.rs new file mode 100644 index 0000000000..7f684bcc47 --- /dev/null +++ b/zingo-sync/src/witness.rs @@ -0,0 +1,61 @@ +//! Module for stucts and types associated with witness construction + +use getset::{Getters, MutGetters}; +use incrementalmerkletree::{Position, Retention}; +use orchard::tree::MerkleHashOrchard; +use sapling_crypto::Node; +use shardtree::{store::memory::MemoryShardStore, ShardTree}; +use zcash_primitives::consensus::BlockHeight; + +const NOTE_COMMITMENT_TREE_DEPTH: u8 = 32; +const SHARD_HEIGHT: u8 = 16; +const MAX_CHECKPOINTS: usize = 100; + +type SaplingShardStore = MemoryShardStore; +type OrchardShardStore = MemoryShardStore; + +/// Shard tree wallet data struct +#[derive(Debug, Getters, MutGetters)] +#[getset(get = "pub", get_mut = "pub")] +pub struct ShardTrees { + /// Sapling shard tree + sapling: ShardTree, + /// Orchard shard tree + orchard: ShardTree, +} + +impl ShardTrees { + /// Create new ShardTrees + pub fn new() -> Self { + Self { + sapling: ShardTree::new(MemoryShardStore::empty(), MAX_CHECKPOINTS), + orchard: ShardTree::new(MemoryShardStore::empty(), MAX_CHECKPOINTS), + } + } +} + +impl Default for ShardTrees { + fn default() -> Self { + Self::new() + } +} + +/// Required data for updating [`shardtree::ShardTree`] +pub struct ShardTreeData { + pub(crate) sapling_initial_position: Position, + pub(crate) orchard_initial_position: Position, + pub(crate) sapling_leaves_and_retentions: Vec<(Node, Retention)>, + pub(crate) orchard_leaves_and_retentions: Vec<(MerkleHashOrchard, Retention)>, +} + +impl ShardTreeData { + /// Creates new ShardTreeData + pub fn new(sapling_initial_position: Position, orchard_initial_position: Position) -> Self { + ShardTreeData { + sapling_initial_position, + orchard_initial_position, + sapling_leaves_and_retentions: Vec::new(), + orchard_leaves_and_retentions: Vec::new(), + } + } +} diff --git a/zingo-testutils/Cargo.toml b/zingo-testutils/Cargo.toml index 64d3aa6da9..c13fdbc130 100644 --- a/zingo-testutils/Cargo.toml +++ b/zingo-testutils/Cargo.toml @@ -10,8 +10,8 @@ default = ["grpc-proxy"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -zingoconfig = { path = "../zingoconfig" } -zingolib = { path = "../zingolib" } +zingoconfig = { path = "../zingoconfig" , features = ["test-elevation"] } +zingolib = { path = "../zingolib" , features = ["test-elevation"] } zingo-netutils = { path = "../zingo-netutils", features = ["test-features"] } zingo-testvectors = { path = "../zingo-testvectors" } zingo-status = { path = "../zingo-status" } @@ -30,3 +30,8 @@ tokio = { workspace = true } http.workspace = true tonic = { workspace = true, optional = true } nonempty.workspace = true + +[dev-dependencies] +zingoconfig = { path = "../zingoconfig" , features = ["test-elevation"] } +zingolib = { path = "../zingolib" , features = ["test-elevation"] } + diff --git a/zingo-testvectors/Cargo.toml b/zingo-testvectors/Cargo.toml deleted file mode 100644 index a6dc450343..0000000000 --- a/zingo-testvectors/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "zingo-testvectors" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -zingoconfig = { path = "../zingoconfig" } - -zcash_primitives = { workspace = true } diff --git a/zingocli/Cargo.toml b/zingocli/Cargo.toml index 8aede2e073..a3af13608f 100644 --- a/zingocli/Cargo.toml +++ b/zingocli/Cargo.toml @@ -4,14 +4,14 @@ version = "0.2.0" edition = "2021" [dependencies] -zingolib = { path = "../zingolib/", features = ["deprecations"] } -zingoconfig = { path = "../zingoconfig/" } -zingo-testutils = { path = "../zingo-testutils" } +zingolib = { path = "../zingolib/", features = ["deprecations", "test-elevation"] } clap = { workspace = true } http = { workspace = true } -rustyline = { workspace = true } +json = { workspace = true } log = { workspace = true } +rustyline = { workspace = true } shellwords = { workspace = true } -json = { workspace = true } + +rustls.workspace = true diff --git a/zingocli/src/lib.rs b/zingocli/src/lib.rs index b0e8be48db..33c37c86b6 100644 --- a/zingocli/src/lib.rs +++ b/zingocli/src/lib.rs @@ -11,8 +11,8 @@ use std::sync::Arc; use log::{error, info}; use clap::{self, Arg}; -use zingo_testutils::regtest; -use zingoconfig::ChainType; +use zingolib::config::ChainType; +use zingolib::testutils::regtest; use zingolib::wallet::WalletBase; use zingolib::{commands, lightclient::LightClient}; @@ -37,15 +37,17 @@ pub fn build_clap_app() -> clap::ArgMatches { .arg(Arg::new("chain") .long("chain").short('c') .help(r#"What chain to expect, if it's not inferable from the server URI. One of "mainnet", "testnet", or "regtest""#)) - .arg(Arg::new("from") - .short('f') - .short_alias('s') - .long("from") - .alias("seed") - .alias("viewing-key") - .value_name("from") + .arg(Arg::new("seed") + .short('s') + .long("seed") + .value_name("SEED PHRASE") .value_parser(parse_seed) - .help("Create a new wallet with the given key. Can be a 24-word seed phrase or a viewkey. Will fail if wallet already exists")) + .help("Create a new wallet with the given 24-word seed phrase. Will fail if wallet already exists")) + .arg(Arg::new("viewkey") + .long("viewkey") + .value_name("UFVK") + .value_parser(parse_ufvk) + .help("Create a new wallet with the given encoded unified full viewing key. Will fail if wallet already exists")) .arg(Arg::new("birthday") .long("birthday") .value_name("birthday") @@ -56,7 +58,7 @@ pub fn build_clap_app() -> clap::ArgMatches { .value_name("server") .help("Lightwalletd server to connect to.") .value_parser(parse_uri) - .default_value(zingoconfig::DEFAULT_LIGHTWALLETD_SERVER)) + .default_value(zingolib::config::DEFAULT_LIGHTWALLETD_SERVER)) .arg(Arg::new("data-dir") .long("data-dir") .value_name("data-dir") @@ -93,6 +95,19 @@ fn parse_seed(s: &str) -> Result { Err("Unexpected failure to parse String!!".to_string()) } } +/// Parse encoded UFVK to String and check for whitespaces +fn parse_ufvk(s: &str) -> Result { + if let Ok(s) = s.parse::() { + let count = s.split_whitespace().count(); + if count == 1 { + Ok(s) + } else { + Err("Encoded UFVK should not contain whitespace!".to_string()) + } + } else { + Err("Unexpected failure to parse String!!".to_string()) + } +} #[cfg(target_os = "linux")] /// This function is only tested against Linux. fn report_permission_error() { @@ -276,12 +291,13 @@ fn short_circuit_on_help(params: Vec) { /// * behave correctly as a function of each parameter that may have been passed /// * add details of above here /// * handle parameters as efficiently as possible. -/// * If a ShortCircuitCommand -/// is specified, then the system should execute only logic necessary to support that command, -/// in other words "help" the ShortCircuitCommand _MUST_ not launch either zcashd or lightwalletd +/// * If a ShortCircuitCommand is specified, then the system should execute +/// only logic necessary to support that command, in other words "help" +/// the ShortCircuitCommand _MUST_ not launch either zcashd or lightwalletd impl ConfigTemplate { fn fill(matches: clap::ArgMatches) -> Result { let is_regtest = matches.get_flag("regtest"); // Begin short_circuit section + let params = if let Some(vals) = matches.get_many::("extra_args") { vals.cloned().collect() } else { @@ -295,7 +311,17 @@ impl ConfigTemplate { } else { None }; - let from = matches.get_one::("from"); + let seed = matches.get_one::("seed"); + let viewkey = matches.get_one::("viewkey"); + let from = if seed.is_some() && viewkey.is_some() { + return Err("Cannot load a wallet from both seed phrase and viewkey!".to_string()); + } else if seed.is_some() { + seed + } else if viewkey.is_some() { + viewkey + } else { + None + }; let maybe_birthday = matches .get_one::("birthday") .map(|bday| bday.to_string()); @@ -328,7 +354,7 @@ If you don't remember the block height, you can pass '--birthday 0' to scan from let data_dir = if let Some(dir) = matches.get_one::("data-dir") { PathBuf::from(dir.clone()) } else if is_regtest { - zingo_testutils::paths::get_regtest_dir() + zingolib::testutils::paths::get_regtest_dir() } else { PathBuf::from("wallets") }; @@ -348,16 +374,18 @@ If you don't remember the block height, you can pass '--birthday 0' to scan from } else { None }; - let server = zingoconfig::construct_lightwalletd_uri(server); + let server = zingolib::config::construct_lightwalletd_uri(server); let chaintype = if let Some(chain) = matches.get_one::("chain") { match chain.as_str() { "mainnet" => ChainType::Mainnet, "testnet" => ChainType::Testnet, - "regtest" => ChainType::Regtest(zingoconfig::RegtestNetwork::all_upgrades_active()), + "regtest" => { + ChainType::Regtest(zingolib::config::RegtestNetwork::all_upgrades_active()) + } _ => return Err(chain.clone()), } } else if is_regtest { - ChainType::Regtest(zingoconfig::RegtestNetwork::all_upgrades_active()) + ChainType::Regtest(zingolib::config::RegtestNetwork::all_upgrades_active()) } else { ChainType::Mainnet }; @@ -403,7 +431,7 @@ pub fn startup( } else { filled_template.data_dir.clone() }; - let config = zingoconfig::load_clientconfig( + let config = zingolib::config::load_clientconfig( filled_template.server.clone(), Some(data_dir), filled_template.chaintype, diff --git a/zingocli/src/main.rs b/zingocli/src/main.rs index d53c96a2a8..84ff5d3fb7 100644 --- a/zingocli/src/main.rs +++ b/zingocli/src/main.rs @@ -1,4 +1,8 @@ #![forbid(unsafe_code)] pub fn main() { + // install default crypto provider (ring) + if let Err(e) = rustls::crypto::ring::default_provider().install_default() { + eprintln!("Error installing crypto provider: {:?}", e) + }; zingo_cli::run_cli(); } diff --git a/zingoconfig/Cargo.toml b/zingoconfig/Cargo.toml index a486f5a9c7..91d7fa319b 100644 --- a/zingoconfig/Cargo.toml +++ b/zingoconfig/Cargo.toml @@ -5,11 +5,14 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +test-elevation = [] + [dependencies] zcash_primitives.workspace = true http.workspace = true -log4rs = { workspace = true } -log = { workspace = true } dirs = { workspace = true } +log = { workspace = true } +log4rs = { workspace = true } tempdir.workspace = true diff --git a/zingolib/CHANGELOG.md b/zingolib/CHANGELOG.md index 8df6adad97..4c3fe26749 100644 --- a/zingolib/CHANGELOG.md +++ b/zingolib/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- `TxMapAndMaybeTrees` renamed `TxMap` + +### Removed + +- `lightclient.bsync_data.uri()` + +## [mobile-release-1.4.3-0-g9fa99407] + ### Deprecated - `lightclient::LightClient::do_list_transactions` @@ -68,3 +78,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `fix_spent_at_height` - `TransactionRecord::net_spent` - `TransactionRecord::get_transparent_value_spent()` +- `LightWallet`: + - `send_to_addresses` diff --git a/zingolib/Cargo.toml b/zingolib/Cargo.toml index 3ead763d7d..f06e79a32e 100644 --- a/zingolib/Cargo.toml +++ b/zingolib/Cargo.toml @@ -7,18 +7,22 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] +ci = [] deprecations = ["lightclient-deprecated"] lightclient-deprecated = [] darkside_tests = [] +test-elevation = ["portpicker", "testvectors", "tempfile", "tempdir"] +testvectors = [] +tempfile = ["dep:tempfile"] +default = ["sync"] +sync = ['dep:zingo-sync'] +zaino-test = ['test-elevation'] [dependencies] -zingoconfig = { path = "../zingoconfig" } zingo-memo = { path = "../zingo-memo" } zingo-status = { path = "../zingo-status" } zingo-netutils = { path = "../zingo-netutils" } - -prost = { workspace = true } -tonic = { workspace = true } +zingo-sync = { path = "../zingo-sync", optional = true } orchard = { workspace = true } shardtree = { workspace = true, features = ["legacy-api"] } @@ -32,51 +36,59 @@ zcash_primitives = { workspace = true } zcash_proofs = { workspace = true } zip32.workspace = true -append-only-vec = { workspace = true } +bip0039.workspace = true +bip32 = { workspace = true, features = ["secp256k1-ffi"] } +bs58 = { workspace = true, features = ["check"] } -log = { workspace = true } -http.workspace = true +append-only-vec = { workspace = true } +base58 = { workspace = true } base64 = { workspace = true } +bls12_381 = { workspace = true } +byteorder = { workspace = true } bytes = { workspace = true } -rand = { workspace = true } -serde_json = { workspace = true } -tokio = { workspace = true, features = ["full"] } -reqwest = { workspace = true, features = ["json"] } +chrono = { workspace = true } +dirs.workspace = true +enum_dispatch = { workspace = true } +ff = { workspace = true } futures = { workspace = true } +getset = { workspace = true } +group = { workspace = true } hex = { workspace = true } -ring = { workspace = true } +http.workspace = true +indoc = { workspace = true } json = { workspace = true } -lazy_static = { workspace = true } -secp256k1 = { workspace = true } -ripemd160 = { workspace = true } -sha2 = { workspace = true } -base58 = { workspace = true } -byteorder = { workspace = true } -ff = { workspace = true } jubjub = { workspace = true } -bls12_381 = { workspace = true } -group = { workspace = true } -rust-embed = { workspace = true, features = ["debug-embed"] } -subtle = { workspace = true } +lazy_static = { workspace = true } +log = { workspace = true } +log4rs.workspace = true nonempty.workspace = true -tracing-subscriber = { workspace = true } -indoc = { workspace = true } -serde = { workspace = true, features = ["derive"] } +portpicker = { workspace = true, optional = true } +proptest = { workspace = true } +prost = { workspace = true } +rand = { workspace = true } +reqwest = { workspace = true, features = ["json"] } +ring = { workspace = true } +rust-embed = { workspace = true, features = ["debug-embed"] } sapling-crypto.workspace = true +secp256k1 = { workspace = true } secrecy = { workspace = true } -getset = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +sha2 = { workspace = true } +subtle = { workspace = true } +tempdir = {workspace = true, optional = true } +tempfile = {workspace = true, optional = true } test-case = { workspace = true } -proptest = { workspace = true } thiserror = { workspace = true } -hdwallet = { workspace = true } -chrono = { workspace = true } -enum_dispatch = { workspace = true } +tokio = { workspace = true, features = ["full"] } +tonic = { workspace = true } +tracing-subscriber = { workspace = true } +bech32 = "0.11.0" [dev-dependencies] portpicker = { workspace = true } tempfile = { workspace = true } concat-idents = { workspace = true } -zingo-testvectors = { path = "../zingo-testvectors" } [build-dependencies] build_utils = { workspace = true } diff --git a/zingolib/src/blaze/block_management_reorg_detection.rs b/zingolib/src/blaze/block_management_reorg_detection.rs index 05cbb2de34..a84abb7670 100644 --- a/zingolib/src/blaze/block_management_reorg_detection.rs +++ b/zingolib/src/blaze/block_management_reorg_detection.rs @@ -3,7 +3,7 @@ use crate::wallet::{ data::{BlockData, PoolNullifier}, notes::ShieldedNoteInterface, traits::DomainWalletExt, - tx_map_and_maybe_trees::TxMapAndMaybeTrees, + tx_map::TxMap, }; use incrementalmerkletree::frontier::CommitmentTree; use incrementalmerkletree::{frontier, witness::IncrementalWitness, Hashable}; @@ -67,7 +67,7 @@ impl BlockManagementData { blocks_in_current_batch: Arc::new(RwLock::new(vec![])), existing_blocks: Arc::new(RwLock::new(vec![])), unverified_treestates: Arc::new(RwLock::new(vec![])), - batch_size: zingoconfig::BATCH_SIZE, + batch_size: crate::config::BATCH_SIZE, highest_verified_trees: None, sync_status, } @@ -317,7 +317,7 @@ impl BlockManagementData { pub async fn invalidate_block( reorg_height: u64, existing_blocks: Arc>>, - transaction_metadata_set: Arc>, + transaction_metadata_set: Arc>, ) { let mut existing_blocks_writelock = existing_blocks.write().await; if existing_blocks_writelock.len() != 0 { @@ -343,7 +343,7 @@ impl BlockManagementData { &self, start_block: u64, end_block: u64, - transaction_metadata_set: Arc>, + transaction_metadata_set: Arc>, reorg_transmitter: UnboundedSender>, ) -> ( JoinHandle, String>>, @@ -505,7 +505,7 @@ impl BlockManagementData { /// currently of the opinion that this function should be factored into separate concerns. pub(crate) async fn get_note_witness( &self, - uri: Uri, + lightwalletd_uri: Uri, height: BlockHeight, transaction_num: usize, output_num: usize, @@ -521,7 +521,8 @@ impl BlockManagementData { let tree = if prev_height < activation_height { frontier::CommitmentTree::<::Node, 32>::empty() } else { - let tree_state = crate::grpc_connector::get_trees(uri, prev_height).await?; + let tree_state = + crate::grpc_connector::get_trees(lightwalletd_uri, prev_height).await?; let tree = hex::decode(D::get_tree(&tree_state)).unwrap(); self.unverified_treestates.write().await.push(tree_state); read_commitment_tree(&tree[..]).map_err(|e| format!("{}", e))? @@ -590,7 +591,7 @@ struct BlockManagementThreadData { impl BlockManagementThreadData { async fn handle_reorgs_populate_data_inner( mut self, - transaction_metadata_set: Arc>, + transaction_metadata_set: Arc>, reorg_transmitter: UnboundedSender>, ) -> Result, String> { // Temporary holding place for blocks while we process them. @@ -836,9 +837,7 @@ mod tests { .handle_reorgs_and_populate_block_mangement_data( start_block, end_block, - Arc::new(RwLock::new( - TxMapAndMaybeTrees::new_with_witness_trees_address_free(), - )), + Arc::new(RwLock::new(TxMap::new_with_witness_trees_address_free())), reorg_transmitter, ) .await; @@ -887,9 +886,7 @@ mod tests { .handle_reorgs_and_populate_block_mangement_data( start_block, end_block, - Arc::new(RwLock::new( - TxMapAndMaybeTrees::new_with_witness_trees_address_free(), - )), + Arc::new(RwLock::new(TxMap::new_with_witness_trees_address_free())), reorg_transmitter, ) .await; @@ -985,9 +982,7 @@ mod tests { .handle_reorgs_and_populate_block_mangement_data( start_block, end_block, - Arc::new(RwLock::new( - TxMapAndMaybeTrees::new_with_witness_trees_address_free(), - )), + Arc::new(RwLock::new(TxMap::new_with_witness_trees_address_free())), reorg_transmitter, ) .await; diff --git a/zingolib/src/blaze/fetch_compact_blocks.rs b/zingolib/src/blaze/fetch_compact_blocks.rs index b204dae0fd..710fea2f37 100644 --- a/zingolib/src/blaze/fetch_compact_blocks.rs +++ b/zingolib/src/blaze/fetch_compact_blocks.rs @@ -1,10 +1,10 @@ use std::{cmp::max, sync::Arc}; +use crate::config::ZingoConfig; use crate::grpc_connector::GrpcConnector; use log::debug; use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; use zcash_client_backend::proto::compact_formats::CompactBlock; -use zingoconfig::ZingoConfig; pub struct FetchCompactBlocks { config: ZingoConfig, } diff --git a/zingolib/src/blaze/fetch_taddr_transactions.rs b/zingolib/src/blaze/fetch_taddr_transactions.rs index 82af789de7..08e05218ac 100644 --- a/zingolib/src/blaze/fetch_taddr_transactions.rs +++ b/zingolib/src/blaze/fetch_taddr_transactions.rs @@ -9,11 +9,11 @@ use tokio::sync::mpsc::UnboundedReceiver; use tokio::sync::oneshot; use tokio::{sync::mpsc::UnboundedSender, task::JoinHandle}; +use crate::config::ZingoConfig; use zcash_primitives::consensus::BlockHeight; use zcash_primitives::consensus::BranchId; use zcash_primitives::consensus::Parameters; use zcash_primitives::transaction::Transaction; -use zingoconfig::ZingoConfig; pub struct FetchTaddrTransactions { wc: Arc, @@ -49,6 +49,11 @@ impl FetchTaddrTransactions { .addresses() .iter() .filter_map(|ua| ua.transparent()) + .chain( + wc.transparent_child_ephemeral_addresses() + .iter() + .map(|(taddr, _metadata)| taddr), + ) .map(|taddr| address_from_pubkeyhash(&config, *taddr)) .collect::>(); @@ -285,7 +290,7 @@ mod tests { 1, taddr_fetcher_transmitter, full_transaction_scanner_transmitter, - zingoconfig::Network::FakeMainnet, + crate::config::Network::FakeMainnet, ) .await; //Todo: Add regtest support diff --git a/zingolib/src/blaze/syncdata.rs b/zingolib/src/blaze/syncdata.rs index d716fe8798..1d2ef99926 100644 --- a/zingolib/src/blaze/syncdata.rs +++ b/zingolib/src/blaze/syncdata.rs @@ -1,35 +1,27 @@ use std::sync::Arc; -use http::Uri; use tokio::sync::RwLock; use zcash_client_backend::proto::service::TreeState; use super::{block_management_reorg_detection::BlockManagementData, sync_status::BatchSyncStatus}; use crate::wallet::data::BlockData; use crate::wallet::WalletOptions; -use zingoconfig::ZingoConfig; pub struct BlazeSyncData { pub(crate) block_data: BlockManagementData, - uri: Arc>, pub(crate) wallet_options: WalletOptions, } impl BlazeSyncData { - pub fn new(config: &ZingoConfig) -> Self { + pub fn new() -> Self { let sync_status = Arc::new(RwLock::new(BatchSyncStatus::default())); Self { - uri: config.lightwalletd_uri.clone(), block_data: BlockManagementData::new(sync_status), wallet_options: WalletOptions::default(), } } - pub fn uri(&self) -> Uri { - self.uri.read().unwrap().clone() - } - pub async fn setup_nth_batch( &mut self, start_block: u64, diff --git a/zingolib/src/blaze/targetted_rescan.rs b/zingolib/src/blaze/targetted_rescan.rs index bb88b11312..f2a89bc8c2 100644 --- a/zingolib/src/blaze/targetted_rescan.rs +++ b/zingolib/src/blaze/targetted_rescan.rs @@ -15,7 +15,7 @@ use zcash_primitives::{ use zingo_status::confirmation_status::ConfirmationStatus; pub async fn start( - wallet_blocks: Arc>>, + last_100_blocks: Arc>>, transaction_context: TransactionContext, fulltx_fetcher: UnboundedSender<(TxId, oneshot::Sender>)>, ) -> JoinHandle> { @@ -24,7 +24,7 @@ pub async fn start( // collect any outdated transaction records that are incomplete and missing output indexes let unindexed_records_result = - if let Some(highest_wallet_block) = wallet_blocks.read().await.first() { + if let Some(highest_wallet_block) = last_100_blocks.read().await.first() { transaction_context .unindexed_records(BlockHeight::from_u32(highest_wallet_block.height as u32)) .await diff --git a/zingolib/src/blaze/trial_decryptions.rs b/zingolib/src/blaze/trial_decryptions.rs index 7ff252a9a8..13b3b61f08 100644 --- a/zingolib/src/blaze/trial_decryptions.rs +++ b/zingolib/src/blaze/trial_decryptions.rs @@ -5,13 +5,14 @@ use crate::error::ZingoLibResult; +use crate::config::ZingoConfig; use crate::wallet::keys::unified::{External, Fvk as _, Ivk}; use crate::wallet::notes::ShieldedNoteInterface; use crate::wallet::{ data::PoolNullifier, keys::unified::WalletCapability, traits::{CompactOutput as _, DomainWalletExt, FromCommitment, Recipient}, - tx_map_and_maybe_trees::TxMapAndMaybeTrees, + tx_map::TxMap, utils::txid_from_slice, MemoDownloadOption, }; @@ -35,13 +36,12 @@ use zcash_primitives::{ transaction::{Transaction, TxId}, }; use zingo_status::confirmation_status::ConfirmationStatus; -use zingoconfig::ZingoConfig; use super::syncdata::BlazeSyncData; pub struct TrialDecryptions { wc: Arc, - transaction_metadata_set: Arc>, + transaction_metadata_set: Arc>, config: Arc, } @@ -49,7 +49,7 @@ impl TrialDecryptions { pub fn new( config: Arc, wc: Arc, - transaction_metadata_set: Arc>, + transaction_metadata_set: Arc>, ) -> Self { Self { config, @@ -89,10 +89,12 @@ impl TrialDecryptions { let mut workers = FuturesUnordered::new(); let mut cbs = vec![]; - let sapling_ivk = sapling_crypto::zip32::DiversifiableFullViewingKey::try_from(&*wc) - .ok() - .map(|key| key.derive_ivk()); - let orchard_ivk = orchard::keys::FullViewingKey::try_from(&*wc) + let sapling_ivk = sapling_crypto::zip32::DiversifiableFullViewingKey::try_from( + wc.unified_key_store(), + ) + .ok() + .map(|key| key.derive_ivk()); + let orchard_ivk = orchard::keys::FullViewingKey::try_from(wc.unified_key_store()) .ok() .map(|key| key.derive_ivk()); @@ -134,7 +136,7 @@ impl TrialDecryptions { bsync_data: Arc>, sapling_ivk: Option>, orchard_ivk: Option>, - transaction_metadata_set: Arc>, + transaction_metadata_set: Arc>, transaction_size_filter: Option, detected_transaction_id_sender: UnboundedSender<( TxId, @@ -264,10 +266,10 @@ impl TrialDecryptions { compact_block: &CompactBlock, ivk: D::IncomingViewingKey, height: BlockHeight, - config: &zingoconfig::ZingoConfig, + config: &crate::config::ZingoConfig, wc: &Arc, bsync_data: &Arc>, - transaction_metadata_set: &Arc>, + transaction_metadata_set: &Arc>, detected_transaction_id_sender: &UnboundedSender<( TxId, PoolNullifier, @@ -301,6 +303,7 @@ impl TrialDecryptions { .collect::>(); let maybe_decrypted_outputs = zcash_note_encryption::batch::try_compact_note_decryption(&[ivk], &outputs); + for maybe_decrypted_output in maybe_decrypted_outputs.into_iter().enumerate() { let (output_num, witnessed) = if let (i, Some(((note, to), _ivk_num))) = maybe_decrypted_output { @@ -314,14 +317,13 @@ impl TrialDecryptions { let config = config.clone(); workers.push(tokio::spawn(async move { - let Ok(fvk) = D::wc_to_fvk(&wc) else { + let Ok(fvk) = D::unified_key_store_to_fvk(wc.unified_key_store()) else { // skip any scanning if the wallet doesn't have viewing capability return Ok::<_, String>(()); }; //TODO: Wrong. We don't have fvk import, all our keys are spending let have_spending_key = true; - let uri = bsync_data.read().await.uri().clone(); // Get the witness for the note let witness = bsync_data @@ -329,7 +331,7 @@ impl TrialDecryptions { .await .block_data .get_note_witness::( - uri, + config.get_lightwalletd_uri(), height, transaction_num, i, @@ -426,7 +428,7 @@ fn update_witnesses( )>, BlockHeight, )>, - txmds_writelock: &mut TxMapAndMaybeTrees, + txmds_writelock: &mut TxMap, wc: &Arc, ) -> ZingoLibResult<()> where @@ -450,7 +452,7 @@ where transaction_id, Some(output_index), position + i as u64, - &D::wc_to_fvk(wc).unwrap(), + &D::unified_key_store_to_fvk(wc.unified_key_store()).unwrap(), )?; } nodes_retention.push((node, retention)); diff --git a/zingolib/src/blaze/update_notes.rs b/zingolib/src/blaze/update_notes.rs index 778b244a4c..46a8c610ca 100644 --- a/zingolib/src/blaze/update_notes.rs +++ b/zingolib/src/blaze/update_notes.rs @@ -1,8 +1,6 @@ use crate::error::ZingoLibResult; use crate::wallet::MemoDownloadOption; -use crate::wallet::{ - data::PoolNullifier, tx_map_and_maybe_trees::TxMapAndMaybeTrees, utils::txid_from_slice, -}; +use crate::wallet::{data::PoolNullifier, tx_map::TxMap, utils::txid_from_slice}; use std::sync::Arc; use futures::stream::FuturesUnordered; @@ -28,11 +26,11 @@ use super::syncdata::BlazeSyncData; /// If No, then: /// - Update the witness for this note pub struct UpdateNotes { - transaction_metadata_set: Arc>, + transaction_metadata_set: Arc>, } impl UpdateNotes { - pub fn new(wallet_txns: Arc>) -> Self { + pub fn new(wallet_txns: Arc>) -> Self { Self { transaction_metadata_set: wallet_txns, } diff --git a/zingolib/src/commands.rs b/zingolib/src/commands.rs index d1457bcffa..ac0b418c96 100644 --- a/zingolib/src/commands.rs +++ b/zingolib/src/commands.rs @@ -2,6 +2,7 @@ //! upgrade-or-replace use crate::data::proposal; +use crate::wallet::keys::unified::UnifiedKeyStore; use crate::wallet::MemoDownloadOption; use crate::{lightclient::LightClient, wallet}; use indoc::indoc; @@ -12,8 +13,8 @@ use std::convert::TryInto; use std::str::FromStr; use tokio::runtime::Runtime; use zcash_address::unified::{Container, Encoding, Ufvk}; -use zcash_client_backend::address::Address; -use zcash_primitives::consensus::Parameters; +use zcash_keys::address::Address; +use zcash_keys::keys::UnifiedFullViewingKey; use zcash_primitives::transaction::components::amount::NonNegativeAmount; use zcash_primitives::transaction::fees::zip317::MINIMUM_FEE; @@ -134,16 +135,36 @@ impl Command for WalletKindCommand { fn exec(&self, _args: &[&str], lightclient: &LightClient) -> String { RT.block_on(async move { if lightclient.do_seed_phrase().await.is_ok() { - object! {"kind" => "Seeded"}.pretty(4) - } else { - let capability = lightclient.wallet.wallet_capability(); - object! { - "kind" => "Loaded from key", - "transparent" => capability.transparent.kind_str(), - "sapling" => capability.sapling.kind_str(), - "orchard" => capability.orchard.kind_str(), + object! {"kind" => "Loaded from seed phrase", + "transparent" => true, + "sapling" => true, + "orchard" => true, } .pretty(4) + } else { + match lightclient.wallet.wallet_capability().unified_key_store() { + UnifiedKeyStore::Spend(_) => object! { + "kind" => "Loaded from unified spending key", + "transparent" => true, + "sapling" => true, + "orchard" => true, + } + .pretty(4), + UnifiedKeyStore::View(ufvk) => object! { + "kind" => "Loaded from unified full viewing key", + "transparent" => ufvk.transparent().is_some(), + "sapling" => ufvk.sapling().is_some(), + "orchard" => ufvk.orchard().is_some(), + } + .pretty(4), + UnifiedKeyStore::Empty => object! { + "kind" => "No keys found", + "transparent" => false, + "sapling" => false, + "orchard" => false, + } + .pretty(4), + } } }) } @@ -198,52 +219,65 @@ impl Command for ParseAddressCommand { match args.len() { 1 => json::stringify_pretty( [ - zingoconfig::ChainType::Mainnet, - zingoconfig::ChainType::Testnet, - zingoconfig::ChainType::Regtest( - zingoconfig::RegtestNetwork::all_upgrades_active(), + crate::config::ChainType::Mainnet, + crate::config::ChainType::Testnet, + crate::config::ChainType::Regtest( + crate::config::RegtestNetwork::all_upgrades_active(), ), ] .iter() .find_map(|chain| Address::decode(chain, args[0]).zip(Some(chain))) - .map(|(recipient_address, chain_name)| { - let chain_name_string = match chain_name { - zingoconfig::ChainType::Mainnet => "main", - zingoconfig::ChainType::Testnet => "test", - zingoconfig::ChainType::Regtest(_) => "regtest", - }; - - match recipient_address { - Address::Sapling(_) => object! { - "status" => "success", - "chain_name" => chain_name_string, - "address_kind" => "sapling", - }, - Address::Transparent(_) => object! { - "status" => "success", - "chain_name" => chain_name_string, - "address_kind" => "transparent", - }, - Address::Unified(ua) => { - let mut receivers_available = vec![]; - if ua.orchard().is_some() { - receivers_available.push("orchard") - } - if ua.sapling().is_some() { - receivers_available.push("sapling") - } - if ua.transparent().is_some() { - receivers_available.push("transparent") - } - object! { + .map_or( + object! { + "status" => "Invalid address", + "chain_name" => json::JsonValue::Null, + "address_kind" => json::JsonValue::Null, + }, + |(recipient_address, chain_name)| { + let chain_name_string = match chain_name { + crate::config::ChainType::Mainnet => "main", + crate::config::ChainType::Testnet => "test", + crate::config::ChainType::Regtest(_) => "regtest", + }; + match recipient_address { + Address::Sapling(_) => object! { + "status" => "success", + "chain_name" => chain_name_string, + "address_kind" => "sapling", + }, + Address::Transparent(_) => object! { "status" => "success", "chain_name" => chain_name_string, - "address_kind" => "unified", - "receivers_available" => receivers_available, + "address_kind" => "transparent", + }, + Address::Unified(ua) => { + let mut receivers_available = vec![]; + if ua.orchard().is_some() { + receivers_available.push("orchard") + } + if ua.sapling().is_some() { + receivers_available.push("sapling") + } + if ua.transparent().is_some() { + receivers_available.push("transparent") + } + object! { + "status" => "success", + "chain_name" => chain_name_string, + "address_kind" => "unified", + "receivers_available" => receivers_available, + } + } + Address::Tex(_) => { + object! { + "status" => "success", + "chain_name" => chain_name_string, + "address_kind" => "tex", + } } } - } - }), + }, + ), 4, ), _ => self.help().to_string(), @@ -623,16 +657,15 @@ impl Command for SpendableBalanceCommand { } fn exec(&self, args: &[&str], lightclient: &LightClient) -> String { - let (address, zennies_for_zingo) = - match parse_spendable_balance_args(args, &lightclient.config.chain) { - Ok(address_and_zennies) => address_and_zennies, - Err(e) => { - return format!( - "Error: {}\nTry 'help spendablebalance' for correct usage and examples.", - e - ); - } - }; + let (address, zennies_for_zingo) = match parse_spendable_balance_args(args) { + Ok(address_and_zennies) => address_and_zennies, + Err(e) => { + return format!( + "Error: {}\nTry 'help spendablebalance' for correct usage and examples.", + e + ); + } + }; RT.block_on(async move { match lightclient .get_spendable_shielded_balance(address, zennies_for_zingo) @@ -691,18 +724,20 @@ impl Command for ExportUfvkCommand { } fn exec(&self, _args: &[&str], lightclient: &LightClient) -> String { - let ufvk_res = lightclient.wallet.transaction_context.key.ufvk(); - match ufvk_res { - Ok(ufvk) => { - use zcash_address::unified::Encoding as _; - object! { - "ufvk" => ufvk.encode(&lightclient.config().chain.network_type()), - "birthday" => RT.block_on(lightclient.wallet.get_birthday()) - } - .pretty(2) - } - Err(e) => format!("Error: {e}"), + let ufvk: UnifiedFullViewingKey = match lightclient + .wallet + .wallet_capability() + .unified_key_store() + .try_into() + { + Ok(ufvk) => ufvk, + Err(e) => return e.to_string(), + }; + object! { + "ufvk" => ufvk.encode(&lightclient.config().chain), + "birthday" => RT.block_on(lightclient.wallet.get_birthday()) } + .pretty(2) } } @@ -836,7 +871,7 @@ impl Command for SendCommand { } fn exec(&self, args: &[&str], lightclient: &LightClient) -> String { - let receivers = match utils::parse_send_args(args, &lightclient.config().chain) { + let receivers = match utils::parse_send_args(args) { Ok(receivers) => receivers, Err(e) => { return format!( @@ -900,16 +935,15 @@ impl Command for SendAllCommand { } fn exec(&self, args: &[&str], lightclient: &LightClient) -> String { - let (address, zennies_for_zingo, memo) = - match utils::parse_send_all_args(args, &lightclient.config().chain) { - Ok(parse_results) => parse_results, - Err(e) => { - return format!( - "Error: {}\nTry 'help sendall' for correct usage and examples.", - e - ) - } - }; + let (address, zennies_for_zingo, memo) = match utils::parse_send_all_args(args) { + Ok(parse_results) => parse_results, + Err(e) => { + return format!( + "Error: {}\nTry 'help sendall' for correct usage and examples.", + e + ) + } + }; RT.block_on(async move { match lightclient .propose_send_all(address, zennies_for_zingo, memo) @@ -961,7 +995,7 @@ impl Command for QuickSendCommand { } fn exec(&self, args: &[&str], lightclient: &LightClient) -> String { - let receivers = match utils::parse_send_args(args, &lightclient.config().chain) { + let receivers = match utils::parse_send_args(args) { Ok(receivers) => receivers, Err(e) => { return format!( @@ -1255,6 +1289,32 @@ impl Command for TransactionsCommand { } } +struct DetailedTransactionsCommand {} +impl Command for DetailedTransactionsCommand { + fn help(&self) -> &'static str { + indoc! {r#" + Provides a detailed list of transaction summaries related to this wallet in order of blockheight. + + Usage: + detailed_transactions + "#} + } + + fn short_help(&self) -> &'static str { + "Provides a detailed list of transaction summaries related to this wallet in order of blockheight." + } + + fn exec(&self, args: &[&str], lightclient: &LightClient) -> String { + if !args.is_empty() { + return "Error: invalid arguments\nTry 'help detailed_transactions' for correct usage and examples" + .to_string(); + } + RT.block_on( + async move { format!("{}", lightclient.detailed_transaction_summaries().await) }, + ) + } +} + struct MemoBytesToAddressCommand {} impl Command for MemoBytesToAddressCommand { fn help(&self) -> &'static str { @@ -1697,6 +1757,10 @@ pub fn get_commands() -> HashMap<&'static str, Box> { ("setoption", Box::new(SetOptionCommand {})), ("valuetransfers", Box::new(ValueTransfersCommand {})), ("transactions", Box::new(TransactionsCommand {})), + ( + "detailed_transactions", + Box::new(DetailedTransactionsCommand {}), + ), ("value_to_address", Box::new(ValueToAddressCommand {})), ("sends_to_address", Box::new(SendsToAddressCommand {})), ( diff --git a/zingolib/src/commands/utils.rs b/zingolib/src/commands/utils.rs index 89267b3bb2..95b15dd49c 100644 --- a/zingolib/src/commands/utils.rs +++ b/zingolib/src/commands/utils.rs @@ -5,16 +5,15 @@ use crate::data::receivers::Receivers; use crate::utils::conversion::{address_from_str, zatoshis_from_u64}; use crate::wallet; use json::JsonValue; -use zcash_client_backend::address::Address; +use zcash_address::ZcashAddress; use zcash_primitives::memo::MemoBytes; use zcash_primitives::transaction::components::amount::NonNegativeAmount; -use zingoconfig::ChainType; // Parse the send arguments for `do_send`. // The send arguments have two possible formats: // - 1 argument in the form of a JSON string for multiple sends. '[{"address":"

", "value":, "memo":""}, ...]' // - 2 (+1 optional) arguments for a single address send. &["
", , ""] -pub(super) fn parse_send_args(args: &[&str], chain: &ChainType) -> Result { +pub(super) fn parse_send_args(args: &[&str]) -> Result { // Check for a single argument that can be parsed as JSON let send_args = if args.len() == 1 { let json_args = json::parse(args[0]).map_err(CommandError::ArgsNotJson)?; @@ -29,7 +28,7 @@ pub(super) fn parse_send_args(args: &[&str], chain: &ChainType) -> Result Result>() } else if args.len() == 2 || args.len() == 3 { let recipient_address = - address_from_str(args[0], chain).map_err(CommandError::ConversionFailed)?; + address_from_str(args[0]).map_err(CommandError::ConversionFailed)?; let amount_u64 = args[1] .trim() .parse::() @@ -76,13 +75,12 @@ pub(super) fn parse_send_args(args: &[&str], chain: &ChainType) -> Result", ""] pub(super) fn parse_send_all_args( args: &[&str], - chain: &ChainType, -) -> Result<(Address, bool, Option), CommandError> { - let address: Address; +) -> Result<(ZcashAddress, bool, Option), CommandError> { + let address: ZcashAddress; let memo: Option; let zennies_for_zingo: bool; if args.len() == 1 { - if let Ok(addr) = address_from_str(args[0], chain) { + if let Ok(addr) = address_from_str(args[0]) { address = addr; memo = None; check_memo_compatibility(&address, &memo)?; @@ -96,14 +94,14 @@ pub(super) fn parse_send_all_args( if json_arg.is_empty() { return Err(CommandError::EmptyJsonArray); } - address = address_from_json(&json_arg, chain)?; + address = address_from_json(&json_arg)?; memo = memo_from_json(&json_arg)?; check_memo_compatibility(&address, &memo)?; zennies_for_zingo = zennies_flag_from_json(&json_arg)?; } } else if args.len() == 2 { zennies_for_zingo = false; - address = address_from_str(args[0], chain).map_err(CommandError::ConversionFailed)?; + address = address_from_str(args[0]).map_err(CommandError::ConversionFailed)?; memo = Some( wallet::utils::interpret_memo_string(args[1].to_string()) .map_err(CommandError::InvalidMemo)?, @@ -123,15 +121,14 @@ pub(super) fn parse_send_all_args( // string. pub(super) fn parse_spendable_balance_args( args: &[&str], - chain: &ChainType, -) -> Result<(Address, bool), CommandError> { +) -> Result<(ZcashAddress, bool), CommandError> { if args.len() > 2 { return Err(CommandError::InvalidArguments); } - let address: Address; + let address: ZcashAddress; let zennies_for_zingo: bool; - if let Ok(addr) = address_from_str(args[0], chain) { + if let Ok(addr) = address_from_str(args[0]) { address = addr; zennies_for_zingo = false; } else { @@ -145,7 +142,7 @@ pub(super) fn parse_spendable_balance_args( if json_arg.is_empty() { return Err(CommandError::EmptyJsonArray); } - address = address_from_json(&json_arg, chain)?; + address = address_from_json(&json_arg)?; zennies_for_zingo = zennies_flag_from_json(&json_arg)?; } @@ -154,19 +151,17 @@ pub(super) fn parse_spendable_balance_args( // Checks send inputs do not contain memo's to transparent addresses. fn check_memo_compatibility( - address: &Address, + address: &ZcashAddress, memo: &Option, ) -> Result<(), CommandError> { - if let Address::Transparent(_) = address { - if memo.is_some() { - return Err(CommandError::IncompatibleMemo); - } + if !address.can_receive_memo() && memo.is_some() { + return Err(CommandError::IncompatibleMemo); } Ok(()) } -fn address_from_json(json_array: &JsonValue, chain: &ChainType) -> Result { +fn address_from_json(json_array: &JsonValue) -> Result { if !json_array.has_key("address") { return Err(CommandError::MissingKey("address".to_string())); } @@ -175,7 +170,7 @@ fn address_from_json(json_array: &JsonValue, chain: &ChainType) -> Result Result { @@ -219,8 +214,6 @@ fn memo_from_json(json_array: &JsonValue) -> Result, CommandEr #[cfg(test)] mod tests { - use zingoconfig::{ChainType, RegtestNetwork}; - use crate::{ commands::error::CommandError, data::receivers::Receiver, @@ -230,9 +223,8 @@ mod tests { #[test] fn parse_send_args() { - let chain = ChainType::Regtest(RegtestNetwork::all_upgrades_active()); let address_str = "zregtestsapling1fmq2ufux3gm0v8qf7x585wj56le4wjfsqsj27zprjghntrerntggg507hxh2ydcdkn7sx8kya7p"; - let recipient_address = address_from_str(address_str, &chain).unwrap(); + let recipient_address = address_from_str(address_str).unwrap(); let value_str = "100000"; let amount = zatoshis_from_u64(100_000).unwrap(); let memo_str = "test memo"; @@ -241,7 +233,7 @@ mod tests { // No memo let send_args = &[address_str, value_str]; assert_eq!( - super::parse_send_args(send_args, &chain).unwrap(), + super::parse_send_args(send_args).unwrap(), vec![crate::data::receivers::Receiver { recipient_address: recipient_address.clone(), amount, @@ -252,7 +244,7 @@ mod tests { // Memo let send_args = &[address_str, value_str, memo_str]; assert_eq!( - super::parse_send_args(send_args, &chain).unwrap(), + super::parse_send_args(send_args).unwrap(), vec![Receiver { recipient_address: recipient_address.clone(), amount, @@ -265,14 +257,11 @@ mod tests { {\"address\":\"zregtestsapling1fmq2ufux3gm0v8qf7x585wj56le4wjfsqsj27zprjghntrerntggg507hxh2ydcdkn7sx8kya7p\", \ \"amount\":100000, \"memo\":\"test memo\"}]"; assert_eq!( - super::parse_send_args(&[json], &chain).unwrap(), + super::parse_send_args(&[json]).unwrap(), vec![ Receiver { - recipient_address: address_from_str( - "tmBsTi2xWTjUdEXnuTceL7fecEQKeWaPDJd", - &chain - ) - .unwrap(), + recipient_address: address_from_str("tmBsTi2xWTjUdEXnuTceL7fecEQKeWaPDJd") + .unwrap(), amount: zatoshis_from_u64(50_000).unwrap(), memo: None }, @@ -287,7 +276,7 @@ mod tests { // Trim whitespace let send_args = &[address_str, "1 ", memo_str]; assert_eq!( - super::parse_send_args(send_args, &chain).unwrap(), + super::parse_send_args(send_args).unwrap(), vec![Receiver { recipient_address, amount: zatoshis_from_u64(1).unwrap(), @@ -297,8 +286,6 @@ mod tests { } mod fail_parse_send_args { - use zingoconfig::{ChainType, RegtestNetwork}; - use crate::commands::{error::CommandError, utils::parse_send_args}; mod json_array { @@ -306,34 +293,30 @@ mod tests { #[test] fn empty_json_array() { - let chain = ChainType::Regtest(RegtestNetwork::all_upgrades_active()); let json = "[]"; assert!(matches!( - parse_send_args(&[json], &chain), + parse_send_args(&[json]), Err(CommandError::EmptyJsonArray) )); } #[test] fn failed_json_parsing() { - let chain = ChainType::Regtest(RegtestNetwork::all_upgrades_active()); let args = [r#"testaddress{{"#]; assert!(matches!( - parse_send_args(&args, &chain), + parse_send_args(&args), Err(CommandError::ArgsNotJson(_)) )); } #[test] fn single_arg_not_an_array_unexpected_type() { - let chain = ChainType::Regtest(RegtestNetwork::all_upgrades_active()); let args = ["1"]; assert!(matches!( - parse_send_args(&args, &chain), + parse_send_args(&args), Err(CommandError::SingleArgNotJsonArray(_)) )); } #[test] fn invalid_memo() { - let chain = ChainType::Regtest(RegtestNetwork::all_upgrades_active()); let arg_contents = "[{\"address\": \"zregtestsapling1fmq2ufux3gm0v8qf7x585wj56le4wjfsqsj27zprjghntrerntggg507hxh2ydcdkn7sx8kya7p\", \"amount\": 123, \"memo\": \"testmemo\"}]"; let long_513_byte_memo = &"a".repeat(513); @@ -342,7 +325,7 @@ mod tests { let args = [long_memo_args.as_str()]; assert!(matches!( - parse_send_args(&args, &chain), + parse_send_args(&args), Err(CommandError::InvalidMemo(_)) )); } @@ -352,30 +335,27 @@ mod tests { #[test] fn two_args_wrong_amount() { - let chain = ChainType::Regtest(RegtestNetwork::all_upgrades_active()); let args = ["zregtestsapling1fmq2ufux3gm0v8qf7x585wj56le4wjfsqsj27zprjghntrerntggg507hxh2ydcdkn7sx8kya7p", "foo"]; assert!(matches!( - parse_send_args(&args, &chain), + parse_send_args(&args), Err(CommandError::ParseIntFromString(_)) )); } #[test] fn wrong_number_of_args() { - let chain = ChainType::Regtest(RegtestNetwork::all_upgrades_active()); let args = ["zregtestsapling1fmq2ufux3gm0v8qf7x585wj56le4wjfsqsj27zprjghntrerntggg507hxh2ydcdkn7sx8kya7p", "123", "3", "4"]; assert!(matches!( - parse_send_args(&args, &chain), + parse_send_args(&args), Err(CommandError::InvalidArguments) )); } #[test] fn invalid_memo() { - let chain = ChainType::Regtest(RegtestNetwork::all_upgrades_active()); let long_513_byte_memo = &"a".repeat(513); let args = ["zregtestsapling1fmq2ufux3gm0v8qf7x585wj56le4wjfsqsj27zprjghntrerntggg507hxh2ydcdkn7sx8kya7p", "123", long_513_byte_memo]; assert!(matches!( - parse_send_args(&args, &chain), + parse_send_args(&args), Err(CommandError::InvalidMemo(_)) )); } @@ -384,9 +364,8 @@ mod tests { #[test] fn parse_send_all_args() { - let chain = ChainType::Regtest(RegtestNetwork::all_upgrades_active()); let address_str = "zregtestsapling1fmq2ufux3gm0v8qf7x585wj56le4wjfsqsj27zprjghntrerntggg507hxh2ydcdkn7sx8kya7p"; - let address = address_from_str(address_str, &chain).unwrap(); + let address = address_from_str(address_str).unwrap(); let memo_str = "test memo"; let memo = wallet::utils::interpret_memo_string(memo_str.to_string()).unwrap(); @@ -396,7 +375,7 @@ mod tests { \"memo\":\"test memo\", \ \"zennies_for_zingo\":false}"]; assert_eq!( - super::parse_send_all_args(single_receiver, &chain).unwrap(), + super::parse_send_all_args(single_receiver).unwrap(), (address.clone(), false, Some(memo.clone())) ); // NonBool Zenny Flag @@ -405,35 +384,33 @@ mod tests { \"memo\":\"test memo\", \ \"zennies_for_zingo\":\"false\"}"]; assert!(matches!( - super::parse_send_all_args(nb_zenny, &chain), + super::parse_send_all_args(nb_zenny), Err(CommandError::ZenniesFlagNonBool(_)) )); // with memo let send_args = &[address_str, memo_str]; assert_eq!( - super::parse_send_all_args(send_args, &chain).unwrap(), + super::parse_send_all_args(send_args).unwrap(), (address.clone(), false, Some(memo.clone())) ); let send_args = &[address_str, memo_str]; assert_eq!( - super::parse_send_all_args(send_args, &chain).unwrap(), + super::parse_send_all_args(send_args).unwrap(), (address.clone(), false, Some(memo.clone())) ); // invalid address let send_args = &["invalid_address"]; assert!(matches!( - super::parse_send_all_args(send_args, &chain), + super::parse_send_all_args(send_args), Err(CommandError::ArgNotJsonOrValidAddress) )); } #[test] fn check_memo_compatibility() { - let chain = ChainType::Regtest(RegtestNetwork::all_upgrades_active()); - let sapling_address = address_from_str("zregtestsapling1fmq2ufux3gm0v8qf7x585wj56le4wjfsqsj27zprjghntrerntggg507hxh2ydcdkn7sx8kya7p", &chain).unwrap(); - let transparent_address = - address_from_str("tmBsTi2xWTjUdEXnuTceL7fecEQKeWaPDJd", &chain).unwrap(); + let sapling_address = address_from_str("zregtestsapling1fmq2ufux3gm0v8qf7x585wj56le4wjfsqsj27zprjghntrerntggg507hxh2ydcdkn7sx8kya7p").unwrap(); + let transparent_address = address_from_str("tmBsTi2xWTjUdEXnuTceL7fecEQKeWaPDJd").unwrap(); let memo = interpret_memo_string("test memo".to_string()).unwrap(); // shielded address with memo @@ -451,21 +428,19 @@ mod tests { #[test] fn address_from_json() { - let chain = ChainType::Regtest(RegtestNetwork::all_upgrades_active()); - // with address let json_str = "[{\"address\":\"zregtestsapling1fmq2ufux3gm0v8qf7x585wj56le4wjfsqsj27zprjghntrerntggg507hxh2ydcdkn7sx8kya7p\", \ \"amount\":100000, \"memo\":\"test memo\"}]"; let json_args = json::parse(json_str).unwrap(); let json_args = json_args.members().next().unwrap(); - super::address_from_json(json_args, &chain).unwrap(); + super::address_from_json(json_args).unwrap(); // without address let json_str = "[{\"amount\":100000, \"memo\":\"test memo\"}]"; let json_args = json::parse(json_str).unwrap(); let json_args = json_args.members().next().unwrap(); assert!(matches!( - super::address_from_json(json_args, &chain), + super::address_from_json(json_args), Err(CommandError::MissingKey(_)) )); @@ -475,7 +450,7 @@ mod tests { let json_args = json::parse(json_str).unwrap(); let json_args = json_args.members().next().unwrap(); assert!(matches!( - super::address_from_json(json_args, &chain), + super::address_from_json(json_args), Err(CommandError::UnexpectedType(_)) )); } diff --git a/zingoconfig/src/lib.rs b/zingolib/src/config.rs similarity index 94% rename from zingoconfig/src/lib.rs rename to zingolib/src/config.rs index 42c8d6aeae..d41093b11b 100644 --- a/zingoconfig/src/lib.rs +++ b/zingolib/src/config.rs @@ -30,10 +30,12 @@ use zcash_primitives::consensus::{ /// TODO: Add Doc Comment Here! pub const DEVELOPER_DONATION_ADDRESS: &str = "u1w47nzy4z5g9zvm4h2s4ztpl8vrdmlclqz5sz02742zs5j3tz232u4safvv9kplg7g06wpk5fx0k0rx3r9gg4qk6nkg4c0ey57l0dyxtatqf8403xat7vyge7mmen7zwjcgvryg22khtg3327s6mqqkxnpwlnrt27kxhwg37qys2kpn2d2jl2zkk44l7j7hq9az82594u3qaescr3c9v"; /// TODO: Add Doc Comment Here! -pub const ZENNIES_FOR_ZINGO_DONATION_ADDRESS: &str = "u1p32nu0pgev5cr0u6t4ja9lcn29kaw37xch8nyglwvp7grl07f72c46hxvw0u3q58ks43ntg324fmulc2xqf4xl3pv42s232m25vaukp05s6av9z76s3evsstax4u6f5g7tql5yqwuks9t4ef6vdayfmrsymenqtshgxzj59hdydzygesqa7pdpw463hu7afqf4an29m69kfasdwr494"; -/// TODO: Add Doc Comment Here! pub const ZENNIES_FOR_ZINGO_REGTEST_ADDRESS: &str = "uregtest14emvr2anyul683p43d0ck55c04r65ld6f0shetcn77z8j7m64hm4ku3wguf60s75f0g3s7r7g89z22f3ff5tsfgr45efj4pe2gyg5krqp5vvl3afu0280zp9ru2379zat5y6nkqkwjxsvpq5900kchcgzaw8v8z3ggt5yymnuj9hymtv3p533fcrk2wnj48g5vg42vle08c2xtanq0e"; /// TODO: Add Doc Comment Here! +pub const ZENNIES_FOR_ZINGO_TESTNET_ADDRESS: &str = "utest19zd9laj93deq4lkay48xcfyh0tjec786x6yrng38fp6zusgm0c84h3el99fngh8eks4kxv020r2h2njku6pf69anpqmjq5c3suzcjtlyhvpse0aqje09la48xk6a2cnm822s2yhuzfr47pp4dla9rakdk90g0cee070z57d3trqk87wwj4swz6uf6ts6p5z6lep3xyvueuvt7392tww"; +/// TODO: Add Doc Comment Here! +pub const ZENNIES_FOR_ZINGO_DONATION_ADDRESS: &str = "u1p32nu0pgev5cr0u6t4ja9lcn29kaw37xch8nyglwvp7grl07f72c46hxvw0u3q58ks43ntg324fmulc2xqf4xl3pv42s232m25vaukp05s6av9z76s3evsstax4u6f5g7tql5yqwuks9t4ef6vdayfmrsymenqtshgxzj59hdydzygesqa7pdpw463hu7afqf4an29m69kfasdwr494"; +/// TODO: Add Doc Comment Here! pub const ZENNIES_FOR_ZINGO_AMOUNT: u64 = 1_000_000; /// TODO: Add Doc Comment Here! pub const DEFAULT_LIGHTWALLETD_SERVER: &str = "https://zec.rocks:443"; @@ -163,7 +165,7 @@ impl ZingoConfigBuilder { /// Set the URI of the proxy server we download blockchain information from. /// # Examples /// ``` - /// use zingoconfig::ZingoConfigBuilder; + /// use zingolib::config::ZingoConfigBuilder; /// use http::Uri; /// assert_eq!(ZingoConfigBuilder::default().set_lightwalletd_uri(("https://zcash.mysideoftheweb.com:19067").parse::().unwrap()).lightwalletd_uri.clone().unwrap(), "https://zcash.mysideoftheweb.com:19067"); /// ``` @@ -178,8 +180,8 @@ impl ZingoConfigBuilder { /// Note "chain type" is not a formal standard. /// # Examples /// ``` - /// use zingoconfig::ZingoConfigBuilder; - /// use zingoconfig::ChainType::Testnet; + /// use zingolib::config::ZingoConfigBuilder; + /// use zingolib::config::ChainType::Testnet; /// assert_eq!(ZingoConfigBuilder::default().set_chain(Testnet).create().chain, Testnet); /// ``` pub fn set_chain(&mut self, chain: ChainType) -> &mut Self { @@ -190,7 +192,7 @@ impl ZingoConfigBuilder { /// Set the wallet directory where client transaction data will be stored in a wallet. /// # Examples /// ``` - /// use zingoconfig::ZingoConfigBuilder; + /// use zingolib::config::ZingoConfigBuilder; /// use tempdir::TempDir; /// let dir = TempDir::new("zingo_doc_test").unwrap().into_path(); /// let config = ZingoConfigBuilder::default().set_wallet_dir(dir.clone()).create(); @@ -233,8 +235,36 @@ impl Default for ZingoConfigBuilder { } impl ZingoConfig { - #[deprecated] - /// Create an unconnected (to any server) config to test for local wallet etc... + /// TODO: Add Doc Comment Here! + pub fn build(chain: ChainType) -> ZingoConfigBuilder { + ZingoConfigBuilder { + chain, + ..ZingoConfigBuilder::default() + } + } + + #[cfg(any(test, feature = "test-elevation"))] + /// create a ZingoConfig that helps a LightClient connect to a server. + pub fn create_testnet() -> ZingoConfig { + ZingoConfig::build(ChainType::Testnet) + .set_lightwalletd_uri( + ("https://zcash.mysideoftheweb.com:19067") + .parse::() + .unwrap(), + ) + .create() + } + + #[cfg(any(test, feature = "test-elevation"))] + /// create a ZingoConfig that helps a LightClient connect to a server. + pub fn create_mainnet() -> ZingoConfig { + ZingoConfig::build(ChainType::Mainnet) + .set_lightwalletd_uri(("https://zec.rocks:443").parse::().unwrap()) + .create() + } + + #[cfg(feature = "test-elevation")] + /// create a ZingoConfig that signals a LightClient not to connect to a server. pub fn create_unconnected(chain: ChainType, dir: Option) -> ZingoConfig { if let Some(dir) = dir { ZingoConfig::build(chain).set_wallet_dir(dir).create() @@ -243,13 +273,6 @@ impl ZingoConfig { } } - /// TODO: Add Doc Comment Here! - pub fn build(chain: ChainType) -> ZingoConfigBuilder { - ZingoConfigBuilder { - chain, - ..ZingoConfigBuilder::default() - } - } /// Convenience wrapper pub fn sapling_activation_height(&self) -> u64 { self.chain @@ -432,7 +455,7 @@ impl ZingoConfig { use std::time::{SystemTime, UNIX_EPOCH}; let mut backup_file_path = self.get_zingo_wallet_dir().into_path_buf(); - backup_file_path.push(&format!( + backup_file_path.push(format!( "zingo-wallet.backup.{}.dat", SystemTime::now() .duration_since(UNIX_EPOCH) @@ -638,6 +661,7 @@ impl RegtestNetwork { self.activation_heights .get_activation_height(NetworkUpgrade::Nu5), ), + NetworkUpgrade::Nu6 => None, } } } @@ -682,6 +706,7 @@ impl ActivationHeights { NetworkUpgrade::Heartwood => self.heartwood, NetworkUpgrade::Canopy => self.canopy, NetworkUpgrade::Nu5 => self.orchard, + NetworkUpgrade::Nu6 => todo!(), } } } diff --git a/zingolib/src/data.rs b/zingolib/src/data.rs index 520f871503..f7fd9a7c8d 100644 --- a/zingolib/src/data.rs +++ b/zingolib/src/data.rs @@ -4,10 +4,10 @@ pub mod witness_trees; /// transforming data related to the destination of a send. pub mod receivers { + use zcash_address::ZcashAddress; use zcash_client_backend::zip321::Payment; use zcash_client_backend::zip321::TransactionRequest; use zcash_client_backend::zip321::Zip321Error; - use zcash_keys::address; use zcash_primitives::memo::MemoBytes; use zcash_primitives::transaction::components::amount::NonNegativeAmount; @@ -17,14 +17,14 @@ pub mod receivers { /// The superficial representation of the the consumer's intended receiver #[derive(Clone, Debug, PartialEq)] pub struct Receiver { - pub(crate) recipient_address: address::Address, + pub(crate) recipient_address: ZcashAddress, pub(crate) amount: NonNegativeAmount, pub(crate) memo: Option, } impl Receiver { /// Create a new Receiver pub fn new( - recipient_address: address::Address, + recipient_address: ZcashAddress, amount: NonNegativeAmount, memo: Option, ) -> Self { @@ -37,21 +37,27 @@ pub mod receivers { } impl From for Payment { fn from(receiver: Receiver) -> Self { - Self { - recipient_address: receiver.recipient_address, - amount: receiver.amount, - memo: receiver.memo, - label: None, - message: None, - other_params: vec![], - } + Payment::new( + receiver.recipient_address, + receiver.amount, + receiver.memo, + None, + None, + vec![], + ) + .expect("memo compatability checked in 'parse_send_args'") } } /// Creates a [`zcash_client_backend::zip321::TransactionRequest`] from receivers. + /// Note this fn is called to calculate the spendable_shielded balance + /// shielding and TEX should be handled mutually exclusively pub fn transaction_request_from_receivers( receivers: Receivers, ) -> Result { + // If this succeeds: + // * zingolib learns whether there is a TEX address + // * if there's a TEX address it's readable. let payments = receivers .into_iter() .map(|receiver| receiver.into()) diff --git a/zingolib/src/data/witness_trees.rs b/zingolib/src/data/witness_trees.rs index 31b47a964f..41fb6d16ba 100644 --- a/zingolib/src/data/witness_trees.rs +++ b/zingolib/src/data/witness_trees.rs @@ -19,7 +19,7 @@ use zcash_note_encryption::Domain; use zcash_primitives::consensus::BlockHeight; use zcash_primitives::merkle_tree::HashSer; -use zingoconfig::MAX_REORG; +use crate::config::MAX_REORG; /// TODO: Add Doc Comment Here! pub const COMMITMENT_TREE_LEVELS: u8 = 32; diff --git a/zingolib/src/lib.rs b/zingolib/src/lib.rs index afb785ff2a..589ebb51c4 100644 --- a/zingolib/src/lib.rs +++ b/zingolib/src/lib.rs @@ -9,6 +9,7 @@ extern crate rust_embed; pub mod blaze; pub mod commands; +pub mod config; pub mod data; pub mod error; pub mod grpc_connector; @@ -16,8 +17,12 @@ pub mod lightclient; pub mod utils; pub mod wallet; -#[cfg(test)] +#[cfg(any(test, feature = "test-elevation"))] pub mod mocks; +#[cfg(any(test, feature = "test-elevation"))] +pub mod testutils; +#[cfg(any(test, feature = "testvectors"))] +pub mod testvectors; // This line includes the generated `git_description()` function directly into this scope. include!(concat!(env!("OUT_DIR"), "/git_description.rs")); diff --git a/zingolib/src/lightclient.rs b/zingolib/src/lightclient.rs index 0371c8c040..28efa5aeb4 100644 --- a/zingolib/src/lightclient.rs +++ b/zingolib/src/lightclient.rs @@ -12,7 +12,7 @@ use zcash_primitives::{ memo::{Memo, MemoBytes}, }; -use zingoconfig::ZingoConfig; +use crate::config::ZingoConfig; use crate::{ blaze::syncdata::BlazeSyncData, @@ -217,7 +217,7 @@ pub struct UserBalances { pub incoming_dust: u64, } -/// The LightClient provides a unified interface to the separate concerns that the zingolib library manages. +/// The LightClient connects one LightWallet to one lightwalletd server via gRPC. /// 1. initialization of stored state /// * from seed /// * from keys @@ -226,6 +226,8 @@ pub struct UserBalances { /// 2. synchronization of the client with the state of the blockchain via a gRPC server /// * pub struct LightClient { + // / the LightClient connects to one server. + // pub(crate) server_uri: Arc>, pub(crate) config: ZingoConfig, /// TODO: Add Doc Comment Here! pub wallet: LightWallet, @@ -242,7 +244,7 @@ pub struct LightClient { save_buffer: ZingoSaveBuffer, } -/// This is the omnibus interface to the library, we are currently in the process of refining this typez broad definition! +/// all the wonderfully intertwined ways to conjure a LightClient pub mod instantiation { use log::debug; use std::{ @@ -254,7 +256,7 @@ pub mod instantiation { sync::{Mutex, RwLock}, }; - use zingoconfig::ZingoConfig; + use crate::config::ZingoConfig; use super::{LightClient, ZingoSaveBuffer}; use crate::{ @@ -266,18 +268,16 @@ pub mod instantiation { // toDo rework ZingoConfig. /// This is the fundamental invocation of a LightClient. It lives in an asyncronous runtime. - pub async fn create_from_wallet_async( - wallet: LightWallet, - config: ZingoConfig, - ) -> io::Result { + pub async fn create_from_wallet_async(wallet: LightWallet) -> io::Result { let mut buffer: Vec = vec![]; wallet.write(&mut buffer).await?; + let config = wallet.transaction_context.config.clone(); Ok(LightClient { wallet, config: config.clone(), mempool_monitor: std::sync::RwLock::new(None), sync_lock: Mutex::new(()), - bsync_data: Arc::new(RwLock::new(BlazeSyncData::new(&config))), + bsync_data: Arc::new(RwLock::new(BlazeSyncData::new())), interrupt_sync: Arc::new(RwLock::new(false)), latest_proposal: Arc::new(RwLock::new(None)), save_buffer: ZingoSaveBuffer::new(buffer), @@ -319,10 +319,11 @@ pub mod instantiation { )); } } - let lightclient = LightClient::create_from_wallet_async( - LightWallet::new(config.clone(), wallet_base, birthday)?, + let lightclient = LightClient::create_from_wallet_async(LightWallet::new( config.clone(), - ) + wallet_base, + birthday, + )?) .await?; lightclient.set_wallet_initial_state(birthday).await; @@ -342,10 +343,11 @@ pub mod instantiation { wallet_base: WalletBase, height: u64, ) -> io::Result { - let lightclient = LightClient::create_from_wallet_async( - LightWallet::new(config.clone(), wallet_base, height)?, + let lightclient = LightClient::create_from_wallet_async(LightWallet::new( config.clone(), - ) + wallet_base, + height, + )?) .await?; Ok(lightclient) } @@ -472,7 +474,7 @@ impl LightClient { let new_address = self .wallet .wallet_capability() - .new_address(desired_receivers)?; + .new_address(desired_receivers, false)?; // self.save_internal_rust().await?; @@ -512,7 +514,7 @@ impl LightClient { } } - /// TODO!! This function sorts notes into + /// This function sorts notes into /// unspent /// spend_is_pending /// spend_is_confirmed @@ -633,9 +635,9 @@ async fn get_recent_median_price_from_gemini() -> Result { #[cfg(test)] mod tests { + use crate::config::{ChainType, RegtestNetwork, ZingoConfig}; + use crate::testvectors::seeds::CHIMNEY_BETTER_SEED; use tokio::runtime::Runtime; - use zingo_testvectors::seeds::CHIMNEY_BETTER_SEED; - use zingoconfig::{ChainType, RegtestNetwork, ZingoConfig}; use crate::{lightclient::LightClient, wallet::WalletBase}; diff --git a/zingolib/src/lightclient/deprecated.rs b/zingolib/src/lightclient/deprecated.rs index cb9d247df6..b178ce0a9b 100644 --- a/zingolib/src/lightclient/deprecated.rs +++ b/zingolib/src/lightclient/deprecated.rs @@ -112,6 +112,8 @@ impl LightClient { /// TODO: Add Doc Comment Here! #[allow(deprecated)] + //TODO: add this flag and address warnings + //#[deprecated = "please use transaction_summaries"] pub async fn do_list_transactions(&self) -> JsonValue { // Create a list of TransactionItems from wallet transactions let mut consumer_ui_notes = self diff --git a/zingolib/src/lightclient/describe.rs b/zingolib/src/lightclient/describe.rs index b2135b2022..8c7901f348 100644 --- a/zingolib/src/lightclient/describe.rs +++ b/zingolib/src/lightclient/describe.rs @@ -2,7 +2,10 @@ use ::orchard::note_encryption::OrchardDomain; use json::{object, JsonValue}; use sapling_crypto::note_encryption::SaplingDomain; -use std::collections::{HashMap, HashSet}; +use std::{ + cmp::Ordering, + collections::{HashMap, HashSet}, +}; use tokio::runtime::Runtime; use zcash_client_backend::{encoding::encode_payment_address, PoolType, ShieldedProtocol}; @@ -11,7 +14,12 @@ use zcash_primitives::{ memo::Memo, }; -use zingoconfig::margin_fee; +use crate::{ + config::margin_fee, + wallet::data::summaries::{ + SelfSendValueTransfer, SentValueTransfer, TransactionSummaryInterface, + }, +}; use super::{AccountBackupInfo, LightClient, PoolBalances, UserBalances}; use crate::{ @@ -20,9 +28,10 @@ use crate::{ data::{ finsight, summaries::{ - OrchardNoteSummary, SaplingNoteSummary, SpendStatus, TransactionSummaries, - TransactionSummary, TransactionSummaryBuilder, TransparentCoinSummary, - ValueTransfer, ValueTransferBuilder, ValueTransferKind, ValueTransfers, + basic_transaction_summary_parts, DetailedTransactionSummaries, + DetailedTransactionSummaryBuilder, TransactionSummaries, TransactionSummary, + TransactionSummaryBuilder, ValueTransfer, ValueTransferBuilder, ValueTransferKind, + ValueTransfers, }, OutgoingTxData, }, @@ -55,6 +64,7 @@ impl LightClient { } /// TODO: Add Doc Comment Here! + // todo use helpers pub async fn do_addresses(&self) -> JsonValue { let mut objectified_addresses = Vec::new(); for address in self.wallet.wallet_capability().addresses().iter() { @@ -97,7 +107,7 @@ impl LightClient { spendable_orchard_balance, unverified_orchard_balance, - transparent_balance: self.wallet.tbalance().await, + transparent_balance: self.wallet.get_transparent_balance().await, } } @@ -145,7 +155,7 @@ impl LightClient { tx.orchard_notes .iter() - .filter(|n| n.spent().is_none() && n.pending_spent.is_none()) + .filter(|n| n.spending_tx_status().is_none()) .for_each(|n| { let value = n.orchard_crypto_note.value().inner(); if !incoming && n.is_change { @@ -163,7 +173,7 @@ impl LightClient { tx.sapling_notes .iter() - .filter(|n| n.spent().is_none() && n.pending_spent.is_none()) + .filter(|n| n.spending_tx_status().is_none()) .for_each(|n| { let value = n.sapling_crypto_note.value().inner(); if !incoming && n.is_change { @@ -181,7 +191,7 @@ impl LightClient { tx.transparent_outputs .iter() - .filter(|n| !n.is_spent() && n.pending_spent.is_none()) + .filter(|n| n.spending_tx_status().is_none()) .for_each(|n| { // UTXOs are never 'change', as change would have been shielded. if incoming { @@ -322,7 +332,7 @@ impl LightClient { .blockheight(transaction_summary.blockheight()) .transaction_fee(transaction_summary.fee()) .zec_price(transaction_summary.zec_price()) - .kind(ValueTransferKind::Sent) + .kind(ValueTransferKind::Sent(SentValueTransfer::Send)) .value(value) .recipient_address(Some(address.clone())) .pool_received(None) @@ -365,7 +375,9 @@ impl LightClient { .blockheight(tx.blockheight()) .transaction_fee(tx.fee()) .zec_price(tx.zec_price()) - .kind(ValueTransferKind::MemoToSelf) + .kind(ValueTransferKind::Sent(SentValueTransfer::SendToSelf( + SelfSendValueTransfer::MemoToSelf, + ))) .value(0) .recipient_address(None) .pool_received(None) @@ -393,7 +405,9 @@ impl LightClient { .blockheight(tx.blockheight()) .transaction_fee(tx.fee()) .zec_price(tx.zec_price()) - .kind(ValueTransferKind::Shield) + .kind(ValueTransferKind::Sent(SentValueTransfer::SendToSelf( + SelfSendValueTransfer::Shield, + ))) .value(value) .recipient_address(None) .pool_received(Some( @@ -420,7 +434,9 @@ impl LightClient { .blockheight(tx.blockheight()) .transaction_fee(tx.fee()) .zec_price(tx.zec_price()) - .kind(ValueTransferKind::Shield) + .kind(ValueTransferKind::Sent(SentValueTransfer::SendToSelf( + SelfSendValueTransfer::Shield, + ))) .value(value) .recipient_address(None) .pool_received(Some( @@ -457,7 +473,9 @@ impl LightClient { .blockheight(tx.blockheight()) .transaction_fee(tx.fee()) .zec_price(tx.zec_price()) - .kind(ValueTransferKind::MemoToSelf) + .kind(ValueTransferKind::Sent(SentValueTransfer::SendToSelf( + SelfSendValueTransfer::MemoToSelf, + ))) .value(0) .recipient_address(None) .pool_received(None) @@ -474,7 +492,9 @@ impl LightClient { .blockheight(tx.blockheight()) .transaction_fee(tx.fee()) .zec_price(tx.zec_price()) - .kind(ValueTransferKind::SendToSelf) + .kind(ValueTransferKind::Sent(SentValueTransfer::SendToSelf( + SelfSendValueTransfer::Basic, + ))) .value(0) .recipient_address(None) .pool_received(None) @@ -591,87 +611,79 @@ impl LightClient { let mut transaction_summaries = transaction_records .values() .map(|tx| { - let kind = transaction_records.transaction_kind(tx, &self.config().chain); - let value = match kind { - TransactionKind::Received - | TransactionKind::Sent(SendType::Shield) - | TransactionKind::Sent(SendType::SendToSelf) => tx.total_value_received(), - TransactionKind::Sent(SendType::Send) => tx.value_outgoing(), - }; - let fee = transaction_records.calculate_transaction_fee(tx).ok(); - let orchard_notes = tx - .orchard_notes - .iter() - .map(|output| { - let spend_status = if let Some((txid, _)) = output.spent() { - SpendStatus::Spent(*txid) - } else if let Some((txid, _)) = output.pending_spent() { - SpendStatus::PendingSpent(*txid) - } else { - SpendStatus::Unspent - }; + let (kind, value, fee, orchard_notes, sapling_notes, transparent_coins) = + basic_transaction_summary_parts(tx, transaction_records, &self.config().chain); - let memo = if let Some(Memo::Text(memo_text)) = &output.memo { - Some(memo_text.to_string()) - } else { - None - }; + TransactionSummaryBuilder::new() + .txid(tx.txid) + .datetime(tx.datetime) + .blockheight(tx.status.get_height()) + .kind(kind) + .value(value) + .fee(fee) + .status(tx.status) + .zec_price(tx.price) + .orchard_notes(orchard_notes) + .sapling_notes(sapling_notes) + .transparent_coins(transparent_coins) + .outgoing_tx_data(tx.outgoing_tx_data.clone()) + .build() + .expect("all fields should be populated") + }) + .collect::>(); + transaction_summaries.sort_by(|sum1, sum2| { + match sum1.blockheight().cmp(&sum2.blockheight()) { + Ordering::Equal => { + let starts_with_tex = |summary: &TransactionSummary| { + summary.outgoing_tx_data().iter().any(|outgoing_txdata| { + outgoing_txdata.recipient_address.starts_with("tex") + }) + }; + match (starts_with_tex(sum1), starts_with_tex(sum2)) { + (true, false) => Ordering::Greater, + (false, true) => Ordering::Less, + (false, false) | (true, true) => Ordering::Equal, + } + } + otherwise => otherwise, + } + }); - OrchardNoteSummary::from_parts( - output.value(), - spend_status, - output.output_index, - memo, - ) - }) - .collect::>(); - let sapling_notes = tx - .sapling_notes - .iter() - .map(|output| { - let spend_status = if let Some((txid, _)) = output.spent() { - SpendStatus::Spent(*txid) - } else if let Some((txid, _)) = output.pending_spent() { - SpendStatus::PendingSpent(*txid) - } else { - SpendStatus::Unspent - }; + TransactionSummaries::new(transaction_summaries) + } - let memo = if let Some(Memo::Text(memo_text)) = &output.memo { - Some(memo_text.to_string()) - } else { - None - }; + /// TODO: doc comment + pub async fn transaction_summaries_json_string(&self) -> String { + json::JsonValue::from(self.transaction_summaries().await).pretty(2) + } - SaplingNoteSummary::from_parts( - output.value(), - spend_status, - output.output_index, - memo, - ) - }) - .collect::>(); - let transparent_coins = tx - .transparent_outputs - .iter() - .map(|output| { - let spend_status = if let Some((txid, _)) = output.spent() { - SpendStatus::Spent(*txid) - } else if let Some((txid, _)) = output.pending_spent() { - SpendStatus::PendingSpent(*txid) - } else { - SpendStatus::Unspent - }; + /// Provides a detailed list of transaction summaries related to this wallet in order of blockheight + pub async fn detailed_transaction_summaries(&self) -> DetailedTransactionSummaries { + let transaction_map = self + .wallet + .transaction_context + .transaction_metadata_set + .read() + .await; + let transaction_records = &transaction_map.transaction_records_by_id; - TransparentCoinSummary::from_parts( - output.value(), - spend_status, - output.output_index, - ) - }) - .collect::>(); + let mut transaction_summaries = transaction_records + .values() + .map(|tx| { + let (kind, value, fee, orchard_notes, sapling_notes, transparent_coins) = + basic_transaction_summary_parts(tx, transaction_records, &self.config().chain); + let orchard_nullifiers: Vec = tx + .spent_orchard_nullifiers + .iter() + .map(|nullifier| hex::encode(nullifier.to_bytes())) + .collect(); + let sapling_nullifiers: Vec = tx + .spent_sapling_nullifiers + .iter() + .map(hex::encode) + .collect(); - TransactionSummaryBuilder::new() + DetailedTransactionSummaryBuilder::new() .txid(tx.txid) .datetime(tx.datetime) .blockheight(tx.status.get_height()) @@ -684,18 +696,20 @@ impl LightClient { .sapling_notes(sapling_notes) .transparent_coins(transparent_coins) .outgoing_tx_data(tx.outgoing_tx_data.clone()) + .orchard_nullifiers(orchard_nullifiers) + .sapling_nullifiers(sapling_nullifiers) .build() .expect("all fields should be populated") }) .collect::>(); transaction_summaries.sort_by_key(|tx| tx.blockheight()); - TransactionSummaries::new(transaction_summaries) + DetailedTransactionSummaries::new(transaction_summaries) } /// TODO: doc comment - pub async fn transaction_summaries_json_string(&self) -> String { - json::JsonValue::from(self.transaction_summaries().await).pretty(2) + pub async fn detailed_transaction_summaries_json_string(&self) -> String { + json::JsonValue::from(self.detailed_transaction_summaries().await).pretty(2) } /// TODO: Add Doc Comment Here! @@ -722,7 +736,7 @@ impl LightClient { let value_transfers = self.value_transfers().await.0; let mut memobytes_by_address = HashMap::new(); for value_transfer in value_transfers { - if let ValueTransferKind::Sent = value_transfer.kind() { + if let ValueTransferKind::Sent(SentValueTransfer::Send) = value_transfer.kind() { let address = value_transfer .recipient_address() .expect("sent value transfer should always have a recipient_address") @@ -789,24 +803,25 @@ impl LightClient { self.wallet.transaction_context.transaction_metadata_set.read().await.transaction_records_by_id.iter() .flat_map( |(transaction_id, transaction_metadata)| { transaction_metadata.sapling_notes.iter().filter_map(move |note_metadata| - if !all_notes && note_metadata.spent.is_some() { + if !all_notes && note_metadata.spending_tx_status().is_some() { None } else { let address = LightWallet::note_address::(&self.config.chain, note_metadata, &self.wallet.wallet_capability()); - let spendable = transaction_metadata.status.is_confirmed_after_or_at(&anchor_height) && note_metadata.spent.is_none() && note_metadata.pending_spent.is_none(); + let spendable = transaction_metadata.status.is_confirmed_after_or_at(&anchor_height) && note_metadata.spending_tx_status().is_none(); let created_block:u32 = transaction_metadata.status.get_height().into(); + // this object should be created by the DomainOuput trait if this doesnt get deprecated Some(object!{ "created_in_block" => created_block, "datetime" => transaction_metadata.datetime, - "created_in_txid" => format!("{}", transaction_id.clone()), + "created_in_txid" => format!("{}", transaction_id), "value" => note_metadata.sapling_crypto_note.value().inner(), "pending" => !transaction_metadata.status.is_confirmed(), "address" => address, "spendable" => spendable, - "spent" => note_metadata.spent.map(|(spent_transaction_id, _)| format!("{}", spent_transaction_id)), - "spent_at_height" => note_metadata.spent.map(|(_, h)| h), - "pending_spent" => note_metadata.pending_spent.map(|(spent_transaction_id, _)| format!("{}", spent_transaction_id)), + "spent" => note_metadata.spending_tx_status().and_then(|(s_txid, status)| {if status.is_confirmed() {Some(format!("{}", s_txid))} else {None}}), + "pending_spent" => note_metadata.spending_tx_status().and_then(|(s_txid, status)| {if !status.is_confirmed() {Some(format!("{}", s_txid))} else {None}}), + "spent_at_height" => note_metadata.spending_tx_status().map(|(_, status)| u32::from(status.get_height())), }) } ) @@ -831,25 +846,25 @@ impl LightClient { let mut spent_orchard_notes: Vec = vec![]; self.wallet.transaction_context.transaction_metadata_set.read().await.transaction_records_by_id.iter() .flat_map( |(transaction_id, transaction_metadata)| { - transaction_metadata.orchard_notes.iter().filter_map(move |orch_note_metadata| - if !all_notes && orch_note_metadata.is_spent() { + transaction_metadata.orchard_notes.iter().filter_map(move |note_metadata| + if !all_notes && note_metadata.is_spent_confirmed() { None } else { - let address = LightWallet::note_address::(&self.config.chain, orch_note_metadata, &self.wallet.wallet_capability()); - let spendable = transaction_metadata.status.is_confirmed_after_or_at(&anchor_height) && orch_note_metadata.spent.is_none() && orch_note_metadata.pending_spent.is_none(); + let address = LightWallet::note_address::(&self.config.chain, note_metadata, &self.wallet.wallet_capability()); + let spendable = transaction_metadata.status.is_confirmed_after_or_at(&anchor_height) && note_metadata.spending_tx_status().is_none(); let created_block:u32 = transaction_metadata.status.get_height().into(); Some(object!{ "created_in_block" => created_block, "datetime" => transaction_metadata.datetime, "created_in_txid" => format!("{}", transaction_id), - "value" => orch_note_metadata.orchard_crypto_note.value().inner(), + "value" => note_metadata.orchard_crypto_note.value().inner(), "pending" => !transaction_metadata.status.is_confirmed(), "address" => address, "spendable" => spendable, - "spent" => orch_note_metadata.spent.map(|(spent_transaction_id, _)| format!("{}", spent_transaction_id)), - "spent_at_height" => orch_note_metadata.spent.map(|(_, h)| h), - "pending_spent" => orch_note_metadata.pending_spent.map(|(spent_transaction_id, _)| format!("{}", spent_transaction_id)), + "spent" => note_metadata.spending_tx_status().and_then(|(s_txid, status)| {if status.is_confirmed() {Some(format!("{}", s_txid))} else {None}}), + "pending_spent" => note_metadata.spending_tx_status().and_then(|(s_txid, status)| {if !status.is_confirmed() {Some(format!("{}", s_txid))} else {None}}), + "spent_at_height" => note_metadata.spending_tx_status().map(|(_, status)| u32::from(status.get_height())), }) } ) @@ -864,7 +879,7 @@ impl LightClient { ) } - async fn list_transparent_notes( + async fn list_transparent_outputs( &self, all_notes: bool, ) -> (Vec, Vec, Vec) { @@ -873,28 +888,30 @@ impl LightClient { let mut spent_transparent_notes: Vec = vec![]; self.wallet.transaction_context.transaction_metadata_set.read().await.transaction_records_by_id.iter() - .flat_map( |(transaction_id, wtx)| { - wtx.transparent_outputs.iter().filter_map(move |utxo| - if !all_notes && utxo.is_spent() { + .flat_map( |(transaction_id, transaction_record)| { + transaction_record.transparent_outputs.iter().filter_map(move |utxo| + if !all_notes && utxo.is_spent_confirmed() { None } else { - let created_block:u32 = wtx.status.get_height().into(); + let created_block:u32 = transaction_record.status.get_height().into(); let recipient = zcash_client_backend::address::Address::decode(&self.config.chain, &utxo.address); let taddr = match recipient { Some(zcash_client_backend::address::Address::Transparent(taddr)) => taddr, _otherwise => panic!("Read invalid taddr from wallet-local Utxo, this should be impossible"), }; + let spendable = transaction_record.status.is_confirmed() && utxo.spending_tx_status().is_none(); Some(object!{ "created_in_block" => created_block, - "datetime" => wtx.datetime, + "datetime" => transaction_record.datetime, "created_in_txid" => format!("{}", transaction_id), "value" => utxo.value, "scriptkey" => hex::encode(utxo.script.clone()), "address" => self.wallet.wallet_capability().get_ua_from_contained_transparent_receiver(&taddr).map(|ua| ua.encode(&self.config.chain)), - "spent" => utxo.spent().map(|(spent_transaction_id, _)| format!("{}", spent_transaction_id)), - "spent_at_height" => utxo.spent().map(|(_, h)| h), - "pending_spent" => utxo.pending_spent.map(|(spent_transaction_id, _)| format!("{}", spent_transaction_id)), + "spendable" => spendable, + "spent" => utxo.spending_tx_status().and_then(|(s_txid, status)| {if status.is_confirmed() {Some(format!("{}", s_txid))} else {None}}), + "pending_spent" => utxo.spending_tx_status().and_then(|(s_txid, status)| {if !status.is_confirmed() {Some(format!("{}", s_txid))} else {None}}), + "spent_at_height" => utxo.spending_tx_status().map(|(_, status)| u32::from(status.get_height())), }) } ) @@ -943,7 +960,7 @@ impl LightClient { mut unspent_transparent_notes, mut spent_transparent_notes, mut pending_spent_transparent_notes, - ) = self.list_transparent_notes(all_notes).await; + ) = self.list_transparent_outputs(all_notes).await; unspent_sapling_notes.sort_by_key(|note| note["created_in_block"].as_u64()); spent_sapling_notes.sort_by_key(|note| note["created_in_block"].as_u64()); @@ -977,7 +994,7 @@ impl LightClient { let value_transfers = self.value_transfers().await.0; let mut amount_by_address = HashMap::new(); for value_transfer in value_transfers { - if let ValueTransferKind::Sent = value_transfer.kind() { + if let ValueTransferKind::Sent(SentValueTransfer::Send) = value_transfer.kind() { let address = value_transfer .recipient_address() .expect("sent value transfer should always have a recipient_address") diff --git a/zingolib/src/lightclient/propose.rs b/zingolib/src/lightclient/propose.rs index 8992d8dbeb..ef0a4ed0a7 100644 --- a/zingolib/src/lightclient/propose.rs +++ b/zingolib/src/lightclient/propose.rs @@ -1,18 +1,15 @@ //! LightClient function do_propose generates a proposal to send to specified addresses. -use std::convert::Infallible; -use std::num::NonZeroU32; -use std::ops::DerefMut; - -use zcash_client_backend::data_api::wallet::input_selection::GreedyInputSelector; +use zcash_address::ZcashAddress; use zcash_client_backend::zip321::TransactionRequest; -use zcash_client_backend::zip321::Zip321Error; -use zcash_client_backend::ShieldedProtocol; -use zcash_primitives::{memo::MemoBytes, transaction::components::amount::NonNegativeAmount}; +use zcash_primitives::transaction::components::amount::NonNegativeAmount; -use thiserror::Error; -use zingoconfig::ZENNIES_FOR_ZINGO_AMOUNT; -use zingoconfig::ZENNIES_FOR_ZINGO_DONATION_ADDRESS; +use crate::config::ChainType; +use crate::config::ZENNIES_FOR_ZINGO_AMOUNT; +use crate::config::ZENNIES_FOR_ZINGO_DONATION_ADDRESS; +use crate::config::ZENNIES_FOR_ZINGO_REGTEST_ADDRESS; +use crate::config::ZENNIES_FOR_ZINGO_TESTNET_ADDRESS; +use crate::wallet::propose::{ProposeSendError, ProposeShieldError}; use crate::data::proposal::ProportionalFeeProposal; use crate::data::proposal::ProportionalFeeShieldProposal; @@ -20,182 +17,34 @@ use crate::data::proposal::ZingoProposal; use crate::data::receivers::transaction_request_from_receivers; use crate::data::receivers::Receiver; use crate::lightclient::LightClient; -use crate::wallet::send::change_memo_from_transaction_request; -use crate::wallet::tx_map_and_maybe_trees::TxMapAndMaybeTrees; -use crate::wallet::tx_map_and_maybe_trees::TxMapAndMaybeTreesTraitError; -use zingoconfig::ChainType; - -type GISKit = GreedyInputSelector< - TxMapAndMaybeTrees, - zcash_client_backend::fees::zip317::SingleOutputChangeStrategy, ->; - -// This private helper is a very small DRY, but it has already corrected a minor -// divergence in change strategy. -// Because shielding operations are never expected to create dust notes this change -// is not a bugfix. -fn build_default_giskit(memo: Option) -> GISKit { - let change_strategy = zcash_client_backend::fees::zip317::SingleOutputChangeStrategy::new( - zcash_primitives::transaction::fees::zip317::FeeRule::standard(), - memo, - ShieldedProtocol::Orchard, - ); // review consider change strategy! - GISKit::new( - change_strategy, - zcash_client_backend::fees::DustOutputPolicy::new( - zcash_client_backend::fees::DustAction::AllowDustChange, +impl LightClient { + fn append_zingo_zenny_receiver(&self, receivers: &mut Vec) { + let zfz_address = match self.config().chain { + ChainType::Mainnet => ZENNIES_FOR_ZINGO_DONATION_ADDRESS, + ChainType::Testnet => ZENNIES_FOR_ZINGO_TESTNET_ADDRESS, + ChainType::Regtest(_) => ZENNIES_FOR_ZINGO_REGTEST_ADDRESS, + }; + let dev_donation_receiver = Receiver::new( + crate::utils::conversion::address_from_str(zfz_address).expect("Hard coded str"), + NonNegativeAmount::from_u64(ZENNIES_FOR_ZINGO_AMOUNT).expect("Hard coded u64."), None, - ), - ) -} -/// Errors that can result from do_propose -#[derive(Debug, Error)] -pub enum ProposeSendError { - /// error in using trait to create spend proposal - #[error("{0}")] - Proposal( - zcash_client_backend::data_api::error::Error< - TxMapAndMaybeTreesTraitError, - TxMapAndMaybeTreesTraitError, - zcash_client_backend::data_api::wallet::input_selection::GreedyInputSelectorError< - zcash_primitives::transaction::fees::zip317::FeeError, - zcash_client_backend::wallet::NoteId, - >, - zcash_primitives::transaction::fees::zip317::FeeError, - >, - ), - /// failed to construct a transaction request - #[error("{0}")] - TransactionRequestFailed(#[from] Zip321Error), - /// send all is transferring no value - #[error("send all is transferring no value. only enough funds to pay the fees!")] - ZeroValueSendAll, - /// failed to calculate balance. - #[error("failed to calculated balance. {0}")] - BalanceError(#[from] crate::wallet::error::BalanceError), -} - -/// Errors that can result from do_propose -#[derive(Debug, Error)] -pub enum ProposeShieldError { - /// error in parsed addresses - #[error("{0}")] - Receiver(zcash_client_backend::zip321::Zip321Error), - #[error("{0}")] - /// error in using trait to create shielding proposal - Component( - zcash_client_backend::data_api::error::Error< - TxMapAndMaybeTreesTraitError, - TxMapAndMaybeTreesTraitError, - zcash_client_backend::data_api::wallet::input_selection::GreedyInputSelectorError< - zcash_primitives::transaction::fees::zip317::FeeError, - Infallible, - >, - zcash_primitives::transaction::fees::zip317::FeeError, - >, - ), -} - -fn append_zingo_zenny_receiver(receivers: &mut Vec) { - let dev_donation_receiver = Receiver::new( - crate::utils::conversion::address_from_str( - ZENNIES_FOR_ZINGO_DONATION_ADDRESS, - &ChainType::Mainnet, - ) - .expect("Hard coded str"), - NonNegativeAmount::from_u64(ZENNIES_FOR_ZINGO_AMOUNT).expect("Hard coded u64."), - None, - ); - receivers.push(dev_donation_receiver); -} + ); + receivers.push(dev_donation_receiver); + } -impl LightClient { /// Stores a proposal in the `latest_proposal` field of the LightClient. /// This field must be populated in order to then send a transaction. async fn store_proposal(&self, proposal: ZingoProposal) { let mut latest_proposal_lock = self.latest_proposal.write().await; *latest_proposal_lock = Some(proposal); } - - /// Creates a proposal from a transaction request. - pub(crate) async fn create_send_proposal( - &self, - request: TransactionRequest, - ) -> Result { - let memo = change_memo_from_transaction_request(&request); - - let input_selector = build_default_giskit(Some(memo)); - let mut tmamt = self - .wallet - .transaction_context - .transaction_metadata_set - .write() - .await; - - zcash_client_backend::data_api::wallet::propose_transfer::< - TxMapAndMaybeTrees, - ChainType, - GISKit, - TxMapAndMaybeTreesTraitError, - >( - tmamt.deref_mut(), - &self.wallet.transaction_context.config.chain, - zcash_primitives::zip32::AccountId::ZERO, - &input_selector, - request, - NonZeroU32::MIN, //review! use custom constant? - ) - .map_err(ProposeSendError::Proposal) - } - - /// The shield operation consumes a proposal that transfers value - /// into the Orchard pool. - /// - /// The proposal is generated with this method, which operates on - /// the balance transparent pool, without other input. - /// In other words, shield does not take a user-specified amount - /// to shield, rather it consumes all transparent value in the wallet that - /// can be consumsed without costing more in zip317 fees than is being transferred. - pub(crate) async fn create_shield_proposal( - &self, - ) -> Result { - let input_selector = build_default_giskit(None); - - let mut tmamt = self - .wallet - .transaction_context - .transaction_metadata_set - .write() - .await; - - let proposed_shield = zcash_client_backend::data_api::wallet::propose_shielding::< - TxMapAndMaybeTrees, - ChainType, - GISKit, - TxMapAndMaybeTreesTraitError, - >( - &mut tmamt, - &self.wallet.transaction_context.config.chain, - &input_selector, - // don't shield dust - NonNegativeAmount::const_from_u64(10_000), - &self.get_transparent_addresses(), - // review! do we want to require confirmations? - // make it configurable? - 0, - ) - .map_err(ProposeShieldError::Component)?; - - Ok(proposed_shield) - } - /// Creates and stores a proposal from a transaction request. pub async fn propose_send( &self, request: TransactionRequest, - ) -> Result { - let proposal = self.create_send_proposal(request).await?; + ) -> Result { + let proposal = self.wallet.create_send_proposal(request).await?; self.store_proposal(ZingoProposal::Transfer(proposal.clone())) .await; Ok(proposal) @@ -204,7 +53,7 @@ impl LightClient { /// Creates and stores a proposal for sending all shielded funds to a given address. pub async fn propose_send_all( &self, - address: zcash_keys::address::Address, + address: ZcashAddress, zennies_for_zingo: bool, memo: Option, ) -> Result { @@ -216,11 +65,11 @@ impl LightClient { } let mut receivers = vec![Receiver::new(address, spendable_balance, memo)]; if zennies_for_zingo { - append_zingo_zenny_receiver(&mut receivers); + self.append_zingo_zenny_receiver(&mut receivers); } let request = transaction_request_from_receivers(receivers) .map_err(ProposeSendError::TransactionRequestFailed)?; - let proposal = self.create_send_proposal(request).await?; + let proposal = self.wallet.create_send_proposal(request).await?; self.store_proposal(ZingoProposal::Transfer(proposal.clone())) .await; Ok(proposal) @@ -238,7 +87,7 @@ impl LightClient { // TODO: move spendable balance and create proposal to wallet layer pub async fn get_spendable_shielded_balance( &self, - address: zcash_keys::address::Address, + address: ZcashAddress, zennies_for_zingo: bool, ) -> Result { let confirmed_shielded_balance = self @@ -251,10 +100,10 @@ impl LightClient { None, )]; if zennies_for_zingo { - append_zingo_zenny_receiver(&mut receivers); + self.append_zingo_zenny_receiver(&mut receivers); } let request = transaction_request_from_receivers(receivers)?; - let failing_proposal = self.create_send_proposal(request).await; + let failing_proposal = self.wallet.create_send_proposal(request).await; let shortfall = match failing_proposal { Err(ProposeSendError::Proposal( @@ -289,20 +138,11 @@ impl LightClient { )) } - fn get_transparent_addresses(&self) -> Vec { - self.wallet - .wallet_capability() - .transparent_child_addresses() - .iter() - .map(|(_index, sk)| *sk) - .collect::>() - } - /// Creates and stores a proposal for shielding all transparent funds.. pub async fn propose_shield( &self, ) -> Result { - let proposal = self.create_shield_proposal().await?; + let proposal = self.wallet.create_shield_proposal().await?; self.store_proposal(ZingoProposal::Shield(proposal.clone())) .await; Ok(proposal) @@ -315,9 +155,9 @@ mod shielding { async fn create_basic_client() -> crate::lightclient::LightClient { crate::lightclient::LightClient::create_unconnected( - &zingoconfig::ZingoConfigBuilder::default().create(), + &crate::config::ZingoConfigBuilder::default().create(), crate::wallet::WalletBase::MnemonicPhrase( - zingo_testvectors::seeds::HOSPITAL_MUSEUM_SEED.to_string(), + crate::testvectors::seeds::HOSPITAL_MUSEUM_SEED.to_string(), ), 0, ) @@ -327,7 +167,7 @@ mod shielding { #[tokio::test] async fn propose_shield_missing_scan_prerequisite() { let basic_client = create_basic_client().await; - let propose_shield_result = basic_client.create_shield_proposal().await; + let propose_shield_result = basic_client.wallet.create_shield_proposal().await; match propose_shield_result { Err(ProposeShieldError::Component( zcash_client_backend::data_api::error::Error::ScanRequired, @@ -339,7 +179,7 @@ mod shielding { async fn get_transparent_addresses() { let basic_client = create_basic_client().await; assert_eq!( - basic_client.get_transparent_addresses(), + basic_client.wallet.get_transparent_addresses(), [zcash_primitives::legacy::TransparentAddress::PublicKeyHash( [ 161, 138, 222, 242, 254, 121, 71, 105, 93, 131, 177, 31, 59, 185, 120, 148, diff --git a/zingolib/src/lightclient/read.rs b/zingolib/src/lightclient/read.rs index 6506cbcbc7..7689cad716 100644 --- a/zingolib/src/lightclient/read.rs +++ b/zingolib/src/lightclient/read.rs @@ -7,7 +7,7 @@ use std::{ }; use tokio::runtime::Runtime; -use zingoconfig::ZingoConfig; +use crate::config::ZingoConfig; use super::LightClient; use crate::wallet::LightWallet; @@ -20,7 +20,7 @@ impl LightClient { ) -> io::Result { let wallet = LightWallet::read_internal(&mut reader, config).await?; - let lc = LightClient::create_from_wallet_async(wallet, config.clone()).await?; + let lc = LightClient::create_from_wallet_async(wallet).await?; debug!( "Read wallet with birthday {}", diff --git a/zingolib/src/lightclient/send.rs b/zingolib/src/lightclient/send.rs index e83aebf50a..366f1b2c79 100644 --- a/zingolib/src/lightclient/send.rs +++ b/zingolib/src/lightclient/send.rs @@ -6,12 +6,12 @@ use super::LightClient; use super::LightWalletSendProgress; impl LightClient { - async fn get_submission_height(&self) -> Result { + async fn get_latest_block(&self) -> Result { Ok(BlockHeight::from_u32( crate::grpc_connector::get_latest_block(self.config.get_lightwalletd_uri()) .await? .height as u32, - ) + 1) + )) } /// TODO: Add Doc Comment Here! @@ -26,62 +26,76 @@ impl LightClient { /// patterns for newfangled propose flow pub mod send_with_proposal { - use std::{convert::Infallible, ops::DerefMut as _}; + use std::convert::Infallible; - use hdwallet::traits::Deserialize as _; use nonempty::NonEmpty; - use secp256k1::SecretKey; + use zcash_client_backend::proposal::Proposal; use zcash_client_backend::wallet::NoteId; use zcash_client_backend::zip321::TransactionRequest; - use zcash_client_backend::{proposal::Proposal, wallet::TransparentAddressMetadata}; - use zcash_keys::keys::UnifiedSpendingKey; - use zcash_primitives::transaction::TxId; + + use zcash_primitives::transaction::{Transaction, TxId}; use thiserror::Error; - use zcash_proofs::prover::LocalTxProver; + use zingo_status::confirmation_status::ConfirmationStatus; use crate::lightclient::LightClient; - use crate::{ - lightclient::propose::{ProposeSendError, ProposeShieldError}, - wallet::utils::read_sapling_params, - }; + use crate::wallet::now; + use crate::wallet::propose::{ProposeSendError, ProposeShieldError}; #[allow(missing_docs)] // error types document themselves - #[derive(Debug, Error)] - pub enum CompleteAndBroadcastError { + #[derive(Clone, Debug, Error)] + pub enum TransactionCacheError { #[error("No witness trees. This is viewkey watch, not spendkey wallet.")] NoSpendCapability, - #[error("No proposal. Call do_propose first.")] - NoProposal, - #[error("Cant get submission height. Server connection?: {0:?}")] - SubmissionHeight(String), - #[error("Could not load sapling_params: {0:?}")] - SaplingParams(String), - #[error("Could not find UnifiedSpendKey: {0:?}")] - UnifiedSpendKey(std::io::Error), - #[error("Can't Calculate {0:?}")] - Calculation( - zcash_client_backend::data_api::error::Error< - crate::wallet::tx_map_and_maybe_trees::TxMapAndMaybeTreesTraitError, - std::convert::Infallible, - std::convert::Infallible, - zcash_primitives::transaction::fees::zip317::FeeError, - >, - ), + #[error("No Tx in cached!")] + NoCachedTx, + #[error("Multistep transaction with non-tex steps")] + InvalidMultiStep, + } + + #[allow(missing_docs)] // error types document themselves + #[derive(Clone, Debug, Error)] + pub enum BroadcastCachedTransactionsError { + #[error("Cant broadcast: {0:?}")] + Cache(#[from] TransactionCacheError), + #[error("Couldnt fetch server height: {0:?}")] + Height(String), #[error("Broadcast failed: {0:?}")] Broadcast(String), - #[error("Sending to exchange addresses is not supported yet!")] - ExchangeAddressesNotSupported, } #[allow(missing_docs)] // error types document themselves #[derive(Debug, Error)] - pub enum CompleteAndBroadcastStoredProposal { + pub enum RecordCachedTransactionsError { + #[error("Cant record: {0:?}")] + Cache(#[from] TransactionCacheError), + #[error("Couldnt fetch server height: {0:?}")] + Height(String), + #[error("Decoding failed: {0:?}")] + Decode(#[from] std::io::Error), + } + + #[allow(missing_docs)] // error types document themselves + #[derive(Debug, Error)] + pub enum CompleteAndBroadcastError { + #[error("The transaction could not be calculated: {0:?}")] + BuildTransaction(#[from] crate::wallet::send::BuildTransactionError), + #[error("Recording created transaction failed: {0:?}")] + Record(#[from] RecordCachedTransactionsError), + #[error("Broadcast failed: {0:?}")] + Broadcast(#[from] BroadcastCachedTransactionsError), + #[error("TxIds did not work through?")] + EmptyList, + } + + #[allow(missing_docs)] // error types document themselves + #[derive(Debug, Error)] + pub enum CompleteAndBroadcastStoredProposalError { #[error("No proposal. Call do_propose first.")] NoStoredProposal, #[error("send {0:?}")] - CompleteAndBroadcast(CompleteAndBroadcastError), + CompleteAndBroadcast(#[from] CompleteAndBroadcastError), } #[allow(missing_docs)] // error types document themselves @@ -103,114 +117,184 @@ pub mod send_with_proposal { } impl LightClient { - /// Calculates, signs and broadcasts transactions from a proposal. - async fn complete_and_broadcast( + /// When a transactions are created, they are added to "spending_data". + /// This step records all cached transactions into TransactionRecord s. + /// This overwrites confirmation status to Calculated (not Broadcast) + /// so only call this immediately after creating the transaction + /// + /// With the introduction of multistep transacations to support ZIP320 + /// we begin ordering transactions in the "spending_data" cache such + /// that any output that's used to fund a subsequent transaction is + /// added prior to that fund-requiring transaction. + /// After some consideration we don't see why the spending_data should + /// be stored out-of-order with respect to earlier transactions funding + /// later ones in the cache, so we implement an in order cache. + async fn record_created_transactions( &self, - proposal: &Proposal, - ) -> Result, CompleteAndBroadcastError> { - let result = { - if self - .wallet + ) -> Result, RecordCachedTransactionsError> { + let mut tx_map = self + .wallet + .transaction_context + .transaction_metadata_set + .write() + .await; + let current_height = self + .get_latest_block() + .await + .map_err(RecordCachedTransactionsError::Height)?; + let mut transactions_to_record = vec![]; + if let Some(spending_data) = tx_map.spending_data_mut() { + for (_txid, raw_tx) in spending_data.cached_raw_transactions().iter() { + transactions_to_record.push(Transaction::read( + raw_tx.as_slice(), + zcash_primitives::consensus::BranchId::for_height( + &self.wallet.transaction_context.config.chain, + current_height + 1, + ), + )?); + } + } else { + return Err(RecordCachedTransactionsError::Cache( + TransactionCacheError::NoSpendCapability, + )); + } + drop(tx_map); + let mut txids = vec![]; + for transaction in transactions_to_record { + self.wallet + .transaction_context + .scan_full_tx( + &transaction, + ConfirmationStatus::Calculated(current_height + 1), + Some(now() as u32), + crate::wallet::utils::get_price( + now(), + &self.wallet.price.read().await.clone(), + ), + ) + .await; + self.wallet .transaction_context .transaction_metadata_set - .read() + .write() .await - .witness_trees() - .is_none() - { - return Err(CompleteAndBroadcastError::NoSpendCapability); + .transaction_records_by_id + .update_note_spend_statuses( + transaction.txid(), + Some(( + transaction.txid(), + ConfirmationStatus::Calculated(current_height + 1), + )), + ); + txids.push(transaction.txid()); + } + Ok(txids) + } + + /// When a transaction is created, it is added to a cache. This step broadcasts the cache and sets its status to transmitted. + /// only broadcasts transactions marked as calculated (not broadcast). when it broadcasts them, it marks them as broadcast. + async fn broadcast_created_transactions( + &self, + ) -> Result, BroadcastCachedTransactionsError> { + let mut tx_map = self + .wallet + .transaction_context + .transaction_metadata_set + .write() + .await; + let current_height = self + .get_latest_block() + .await + .map_err(BroadcastCachedTransactionsError::Height)?; + let calculated_tx_cache = tx_map + .spending_data() + .as_ref() + .ok_or(BroadcastCachedTransactionsError::Cache( + TransactionCacheError::NoSpendCapability, + ))? + .cached_raw_transactions() + .clone(); + let mut txids = vec![]; + for (txid, raw_tx) in calculated_tx_cache { + let mut spend_status = None; + // only send the txid if its status is Calculated. when we do, change its status to Transmitted. + if let Some(transaction_record) = tx_map.transaction_records_by_id.get_mut(&txid) { + if matches!(transaction_record.status, ConfirmationStatus::Calculated(_)) { + match crate::grpc_connector::send_transaction( + self.get_server_uri(), + raw_tx.into_boxed_slice(), + ) + .await + { + Ok(serverz_txid_string) => { + txids.push(crate::utils::txid::compare_txid_to_string( + txid, + serverz_txid_string, + self.wallet.transaction_context.config.accept_server_txids, + )); + transaction_record.status = + ConfirmationStatus::Transmitted(current_height + 1); + + spend_status = + Some((transaction_record.txid, transaction_record.status)); + } + Err(server_err) => { + return Err(BroadcastCachedTransactionsError::Broadcast(server_err)) + } + }; + } } - let submission_height = self - .get_submission_height() - .await - .map_err(CompleteAndBroadcastError::SubmissionHeight)?; - - let (sapling_output, sapling_spend): (Vec, Vec) = - read_sapling_params().map_err(CompleteAndBroadcastError::SaplingParams)?; - let sapling_prover = LocalTxProver::from_bytes(&sapling_spend, &sapling_output); - let unified_spend_key = - UnifiedSpendingKey::try_from(self.wallet.wallet_capability().as_ref()) - .map_err(CompleteAndBroadcastError::UnifiedSpendKey)?; - - // We don't support zip320 yet. Only one step. - if proposal.steps().len() != 1 { - return Err(CompleteAndBroadcastError::ExchangeAddressesNotSupported); + if let Some(s) = spend_status { + tx_map + .transaction_records_by_id + .update_note_spend_statuses(s.0, spend_status); } + } - let step = proposal.steps().first(); - - // The 'UnifiedSpendingKey' we create is not a 'proper' USK, in that the - // transparent key it contains is not the account spending key, but the - // externally-scoped derivative key. The goal is to fix this, but in the - // interim we use this special-case logic. - fn usk_to_tkey( - unified_spend_key: &UnifiedSpendingKey, - t_metadata: &TransparentAddressMetadata, - ) -> SecretKey { - hdwallet::ExtendedPrivKey::deserialize( - &unified_spend_key.transparent().to_bytes(), - ) - .expect("This a hack to do a type conversion, and will not fail") - .derive_private_key(t_metadata.address_index().into()) - // This is unwrapped in librustzcash, so I'm not too worried about it - .expect("private key derivation failed") - .private_key - } + tx_map + .spending_data_mut() + .as_mut() + .ok_or(BroadcastCachedTransactionsError::Cache( + TransactionCacheError::NoSpendCapability, + ))? + .cached_raw_transactions_mut() + .clear(); - let build_result = - zcash_client_backend::data_api::wallet::calculate_proposed_transaction( - self.wallet - .transaction_context - .transaction_metadata_set - .write() - .await - .deref_mut(), - &self.wallet.transaction_context.config.chain, - &sapling_prover, - &sapling_prover, - &unified_spend_key, - zcash_client_backend::wallet::OvkPolicy::Sender, - proposal.fee_rule(), - proposal.min_target_height(), - &[], - step, - Some(usk_to_tkey), - Some(self.wallet.wallet_capability().first_sapling_address()), - ) - .map_err(CompleteAndBroadcastError::Calculation)?; + Ok(txids) + } - self.wallet - .send_to_addresses_inner( - build_result.transaction(), - submission_height, - |transaction_bytes| { - crate::grpc_connector::send_transaction( - self.get_server_uri(), - transaction_bytes, - ) - }, - ) - .await - .map_err(CompleteAndBroadcastError::Broadcast) - .map(NonEmpty::singleton) - }; + async fn complete_and_broadcast( + &self, + proposal: &Proposal, + ) -> Result, CompleteAndBroadcastError> { + self.wallet.create_transaction(proposal).await?; + + self.record_created_transactions().await?; + + let broadcast_result = self.broadcast_created_transactions().await; self.wallet - .set_send_result( - result - .as_ref() - .map(|txids| txids.first().to_string()) - .map_err(|e| e.to_string()), - ) + .set_send_result(broadcast_result.clone().map_err(|e| e.to_string()).map( + |vec_txids| { + vec_txids + .iter() + .map(|txid| "created txid: ".to_string() + &txid.to_string()) + .collect::>() + .join(" & ") + }, + )) .await; - result + let broadcast_txids = NonEmpty::from_vec(broadcast_result?) + .ok_or(CompleteAndBroadcastError::EmptyList)?; + + Ok(broadcast_txids) } /// Calculates, signs and broadcasts transactions from a stored proposal. pub async fn complete_and_broadcast_stored_proposal( &self, - ) -> Result, CompleteAndBroadcastStoredProposal> { + ) -> Result, CompleteAndBroadcastStoredProposalError> { if let Some(proposal) = self.latest_proposal.read().await.as_ref() { match proposal { crate::lightclient::ZingoProposal::Transfer(transfer_proposal) => { @@ -222,9 +306,9 @@ pub mod send_with_proposal { .await } } - .map_err(CompleteAndBroadcastStoredProposal::CompleteAndBroadcast) + .map_err(CompleteAndBroadcastStoredProposalError::CompleteAndBroadcast) } else { - Err(CompleteAndBroadcastStoredProposal::NoStoredProposal) + Err(CompleteAndBroadcastStoredProposalError::NoStoredProposal) } } @@ -233,29 +317,38 @@ pub mod send_with_proposal { &self, request: TransactionRequest, ) -> Result, QuickSendError> { - let proposal = self.create_send_proposal(request).await?; + let proposal = self.wallet.create_send_proposal(request).await?; Ok(self.complete_and_broadcast::(&proposal).await?) } /// Shields all transparent funds without confirmation. pub async fn quick_shield(&self) -> Result, QuickShieldError> { - let proposal = self.create_shield_proposal().await?; + let proposal = self.wallet.create_shield_proposal().await?; Ok(self.complete_and_broadcast::(&proposal).await?) } } - #[cfg(test)] + #[cfg(all(test, feature = "testvectors"))] mod tests { - use zingo_testvectors::seeds::ABANDON_ART_SEED; - use zingoconfig::ZingoConfigBuilder; + use zcash_client_backend::PoolType; use crate::{ - lightclient::{send::send_with_proposal::CompleteAndBroadcastError, LightClient}, - mocks::ProposalBuilder, + lightclient::sync::test::sync_example_wallet, + testutils::chain_generics::{ + conduct_chain::ConductChain as _, live_chain::LiveChain, with_assertions, + }, + wallet::disk::testing::examples::{ + ExampleCBBHRWIILGBRABABSSHSMTPRVersion, ExampleMSKMGDBHOTBPETCJWCSPGOPPVersion, + ExampleTestnetWalletSeed, ExampleWalletNetwork, + }, }; #[tokio::test] - async fn complete_and_broadcast() { + async fn complete_and_broadcast_unconnected_error() { + use crate::{ + config::ZingoConfigBuilder, lightclient::LightClient, + mocks::proposal::ProposalBuilder, testvectors::seeds::ABANDON_ART_SEED, + }; let lc = LightClient::create_unconnected( &ZingoConfigBuilder::default().create(), crate::wallet::WalletBase::MnemonicPhrase(ABANDON_ART_SEED.to_string()), @@ -264,16 +357,69 @@ pub mod send_with_proposal { .await .unwrap(); let proposal = ProposalBuilder::default().build(); - assert_eq!( - CompleteAndBroadcastError::SubmissionHeight( - "Error getting client: InvalidScheme".to_string(), - ) - .to_string(), - lc.complete_and_broadcast(&proposal) - .await - .unwrap_err() - .to_string(), - ); + lc.complete_and_broadcast(&proposal).await.unwrap_err(); + // TODO: match on specific error + } + + #[ignore = "live testnet"] + #[tokio::test] + /// this is a live sync test. its execution time scales linearly since last updated + /// this is a live send test. whether it can work depends on the state of live wallet on the blockchain + /// this wallet contains archaic diversified addresses, which may clog the new send engine. + async fn testnet_mskmgdbhotbpetcjwcspgopp_shield_multi_account() { + std::env::set_var("RUST_BACKTRACE", "1"); + let client = crate::lightclient::sync::test::sync_example_wallet( + ExampleWalletNetwork::Testnet(ExampleTestnetWalletSeed::MSKMGDBHOTBPETCJWCSPGOPP( + ExampleMSKMGDBHOTBPETCJWCSPGOPPVersion::Ga74fed621, + )), + ) + .await; + + with_assertions::propose_shield_bump_sync(&mut LiveChain::setup().await, &client, true) + .await; + } + + #[ignore = "live testnet"] + #[tokio::test] + /// this is a live sync test. its execution time scales linearly since last updated + /// this is a live send test. whether it can work depends on the state of live wallet on the blockchain + async fn testnet_cbbhrwiilgbrababsshsmtpr_send_to_self_orchard_hot() { + std::env::set_var("RUST_BACKTRACE", "1"); + let client = sync_example_wallet(ExampleWalletNetwork::Testnet( + ExampleTestnetWalletSeed::CBBHRWIILGBRABABSSHSMTPR( + ExampleCBBHRWIILGBRABABSSHSMTPRVersion::G2f3830058, + ), + )) + .await; + + with_assertions::propose_send_bump_sync_all_recipients( + &mut LiveChain::setup().await, + &client, + vec![( + &client, + PoolType::Shielded(zcash_client_backend::ShieldedProtocol::Orchard), + 10_000, + None, + )], + false, + ) + .await; + } + + #[ignore = "live testnet"] + #[tokio::test] + /// this is a live sync test. its execution time scales linearly since last updated + async fn testnet_cbbhrwiilgbrababsshsmtpr_shield_hot() { + std::env::set_var("RUST_BACKTRACE", "1"); + let client = sync_example_wallet(ExampleWalletNetwork::Testnet( + ExampleTestnetWalletSeed::CBBHRWIILGBRABABSSHSMTPR( + ExampleCBBHRWIILGBRABABSSHSMTPRVersion::G2f3830058, + ), + )) + .await; + + with_assertions::propose_shield_bump_sync(&mut LiveChain::setup().await, &client, true) + .await; } } } diff --git a/zingolib/src/lightclient/sync.rs b/zingolib/src/lightclient/sync.rs index 66f07107a9..f789c90f47 100644 --- a/zingolib/src/lightclient/sync.rs +++ b/zingolib/src/lightclient/sync.rs @@ -26,7 +26,7 @@ use zcash_primitives::{ transaction::Transaction, }; -use zingoconfig::MAX_REORG; +use crate::config::MAX_REORG; static LOG_INIT: std::sync::Once = std::sync::Once::new(); @@ -191,24 +191,68 @@ impl LightClient { BlockHeight::from_u32(rtransaction.height as u32), ), ) { - let price = price.read().await.clone(); - //debug!("Mempool attempting to scan {}", tx.txid()); - let status = ConfirmationStatus::Pending(BlockHeight::from_u32( - rtransaction.height as u32, + let status = ConfirmationStatus::Mempool(BlockHeight::from_u32( + // The mempool transaction's height field is the height + // it entered the mempool. Making it one above that height, + // i.e. the target height, keeps this value consistant with + // the transmitted height, which we record as the target height. + rtransaction.height as u32 + 1, )); - - TransactionContext::new( - &config, - key.clone(), - transaction_metadata_set.clone(), - ) - .scan_full_tx( - &transaction, - status, - Some(now() as u32), - get_price(now(), &price), - ) - .await; + let tms_readlock = transaction_metadata_set.read().await; + let record = tms_readlock + .transaction_records_by_id + .get(&transaction.txid()); + match record { + None => { + // We only need this for the record, and we can't hold it + // for the later scan_full_tx call, as it needs write access. + drop(tms_readlock); + let price = price.read().await.clone(); + //debug!("Mempool attempting to scan {}", tx.txid()); + + TransactionContext::new( + &config, + key.clone(), + transaction_metadata_set.clone(), + ) + .scan_full_tx( + &transaction, + status, + Some(now() as u32), + get_price(now(), &price), + ) + .await; + transaction_metadata_set + .write() + .await + .transaction_records_by_id + .update_note_spend_statuses( + transaction.txid(), + Some((transaction.txid(), status)), + ); + } + Some(r) => { + if matches!(r.status, ConfirmationStatus::Transmitted(_)) { + // In this case, we need write access, to change the status + // from Transmitted to Mempool + drop(tms_readlock); + let mut tms_writelock = + transaction_metadata_set.write().await; + tms_writelock + .transaction_records_by_id + .get_mut(&transaction.txid()) + .expect("None case has already been handled") + .status = status; + tms_writelock + .transaction_records_by_id + .update_note_spend_statuses( + transaction.txid(), + Some((transaction.txid(), status)), + ); + drop(tms_writelock); + } + } + } } } }); @@ -288,7 +332,7 @@ impl LightClient { // to trigger a sync, which will then reorg the remaining blocks BlockManagementData::invalidate_block( last_synced_height, - self.wallet.blocks.clone(), + self.wallet.last_100_blocks.clone(), self.wallet .transaction_context .transaction_metadata_set @@ -303,7 +347,7 @@ impl LightClient { let mut latest_block_batches = vec![]; let mut prev = last_scanned_height; while latest_block_batches.is_empty() || prev != latest_blockid.height { - let batch = cmp::min(latest_blockid.height, prev + zingoconfig::BATCH_SIZE); + let batch = cmp::min(latest_blockid.height, prev + crate::config::BATCH_SIZE); prev = batch; latest_block_batches.push(batch); } @@ -327,7 +371,7 @@ impl LightClient { println!("sync hit error {}. Rolling back", err); BlockManagementData::invalidate_block( self.wallet.last_synced_height().await, - self.wallet.blocks.clone(), + self.wallet.last_100_blocks.clone(), self.wallet .transaction_context .transaction_metadata_set @@ -510,7 +554,7 @@ impl LightClient { // 3. Targetted rescan to update transactions with missing information let targetted_rescan_handle = crate::blaze::targetted_rescan::start( - self.wallet.blocks.clone(), + self.wallet.last_100_blocks.clone(), transaction_context, full_transaction_fetcher_transmitter, ) @@ -621,3 +665,57 @@ impl LightClient { response } } + +#[cfg(all(test, feature = "testvectors"))] +pub mod test { + use crate::{ + lightclient::LightClient, + wallet::disk::testing::examples::{ + ExampleCBBHRWIILGBRABABSSHSMTPRVersion, ExampleHHCCLALTPCCKCSSLPCNETBLRVersion, + ExampleMSKMGDBHOTBPETCJWCSPGOPPVersion, ExampleMainnetWalletSeed, + ExampleTestnetWalletSeed, ExampleWalletNetwork, + }, + }; + + pub(crate) async fn sync_example_wallet(wallet_case: ExampleWalletNetwork) -> LightClient { + std::env::set_var("RUST_BACKTRACE", "1"); + let wallet = wallet_case.load_example_wallet().await; + let lc = LightClient::create_from_wallet_async(wallet).await.unwrap(); + lc.do_sync(true).await.unwrap(); + lc + } + + /// this is a live sync test. its execution time scales linearly since last updated + #[ignore = "testnet and mainnet tests should be ignored due to increasingly large execution times"] + #[tokio::test] + async fn testnet_sync_mskmgdbhotbpetcjwcspgopp_latest() { + sync_example_wallet(ExampleWalletNetwork::Testnet( + ExampleTestnetWalletSeed::MSKMGDBHOTBPETCJWCSPGOPP( + ExampleMSKMGDBHOTBPETCJWCSPGOPPVersion::Ga74fed621, + ), + )) + .await; + } + /// this is a live sync test. its execution time scales linearly since last updated + #[ignore = "testnet and mainnet tests should be ignored due to increasingly large execution times"] + #[tokio::test] + async fn testnet_sync_cbbhrwiilgbrababsshsmtpr_latest() { + sync_example_wallet(ExampleWalletNetwork::Testnet( + ExampleTestnetWalletSeed::CBBHRWIILGBRABABSSHSMTPR( + ExampleCBBHRWIILGBRABABSSHSMTPRVersion::G2f3830058, + ), + )) + .await; + } + /// this is a live sync test. its execution time scales linearly since last updated + #[tokio::test] + #[ignore = "testnet and mainnet tests should be ignored due to increasingly large execution times"] + async fn mainnet_sync_hhcclaltpcckcsslpcnetblr_latest() { + sync_example_wallet(ExampleWalletNetwork::Mainnet( + ExampleMainnetWalletSeed::HHCCLALTPCCKCSSLPCNETBLR( + ExampleHHCCLALTPCCKCSSLPCNETBLRVersion::Gf0aaf9347, + ), + )) + .await; + } +} diff --git a/zingolib/src/mocks.rs b/zingolib/src/mocks.rs index e9f36d92ec..9665605a90 100644 --- a/zingolib/src/mocks.rs +++ b/zingolib/src/mocks.rs @@ -2,7 +2,6 @@ //! Tools to facilitate mocks for structs of external crates and general mocking utilities for testing -pub use proposal::{ProposalBuilder, StepBuilder}; pub use sapling_crypto_note::SaplingCryptoNoteBuilder; fn zaddr_from_seed( @@ -63,7 +62,7 @@ pub fn random_zaddr() -> ( } pub mod nullifier { - //! Module for mocking nullifiers from [`sapling_crypto::note::Nullifier`] and [`orchard::note::Nullifier`] + //! Module for mocking nullifiers from [`sapling_crypto::Nullifier`] and [`orchard::note::Nullifier`] use crate::utils::build_method; @@ -348,19 +347,18 @@ pub mod proposal { use sapling_crypto::value::NoteValue; use sapling_crypto::Rseed; + use zcash_address::ZcashAddress; use zcash_client_backend::fees::TransactionBalance; use zcash_client_backend::proposal::{Proposal, ShieldedInputs, Step, StepOutput}; use zcash_client_backend::wallet::{ReceivedNote, WalletTransparentOutput}; use zcash_client_backend::zip321::{Payment, TransactionRequest}; use zcash_client_backend::{PoolType, ShieldedProtocol}; - use zcash_keys::address::Address; use zcash_primitives::consensus::BlockHeight; use zcash_primitives::transaction::{ components::amount::NonNegativeAmount, fees::zip317::FeeRule, }; use zcash_client_backend::wallet::NoteId; - use zingoconfig::{ChainType, RegtestNetwork}; use crate::utils::conversion::address_from_str; use crate::utils::{build_method, build_method_push}; @@ -570,7 +568,7 @@ pub mod proposal { /// let payment = PaymentBuilder::default().build(); /// ```` pub struct PaymentBuilder { - recipient_address: Option
, + recipient_address: Option, amount: Option, } @@ -583,7 +581,7 @@ pub mod proposal { } } - build_method!(recipient_address, Address); + build_method!(recipient_address, ZcashAddress); build_method!(amount, NonNegativeAmount); /// Builds after all fields have been set. @@ -601,11 +599,7 @@ pub mod proposal { let mut builder = Self::new(); builder .recipient_address( - address_from_str( - zingo_testvectors::REG_O_ADDR_FROM_ABANDONART, - &ChainType::Regtest(RegtestNetwork::all_upgrades_active()), - ) - .unwrap(), + address_from_str(crate::testvectors::REG_O_ADDR_FROM_ABANDONART).unwrap(), ) .amount(NonNegativeAmount::from_u64(100_000).unwrap()); builder diff --git a/zingo-testutils/src/lib.rs b/zingolib/src/testutils.rs similarity index 86% rename from zingo-testutils/src/lib.rs rename to zingolib/src/testutils.rs index a3fa4d30c8..11bb2ab15a 100644 --- a/zingo-testutils/src/lib.rs +++ b/zingolib/src/testutils.rs @@ -5,32 +5,30 @@ pub mod interrupts; +use crate::wallet::data::summaries::{ + OrchardNoteSummary, SaplingNoteSummary, SpendSummary, TransactionSummary, + TransactionSummaryInterface as _, TransparentCoinSummary, +}; +use crate::wallet::keys::unified::WalletCapability; +use crate::wallet::WalletBase; use grpc_proxy::ProxyServer; pub use incrementalmerkletree; use std::cmp; use std::collections::HashMap; use std::io::Read; -use std::path::{Path, PathBuf}; use std::string::String; use std::sync::atomic::AtomicBool; use std::sync::Arc; use std::time::Duration; use tokio::task::JoinHandle; -use zcash_address::unified::{Fvk, Ufvk}; -use zingolib::wallet::data::summaries::{ - OrchardNoteSummary, SaplingNoteSummary, SpendStatus, TransactionSummary, TransparentCoinSummary, -}; -use zingolib::wallet::keys::unified::WalletCapability; -use zingolib::wallet::WalletBase; +use zcash_address::unified::Fvk; +use crate::config::ZingoConfig; +use crate::lightclient::LightClient; use json::JsonValue; use log::debug; use regtest::RegtestManager; use tokio::time::sleep; -use zingoconfig::{ChainType, ZingoConfig}; -use zingolib::lightclient::LightClient; - -use crate::scenarios::setup::TestEnvironmentGenerator; pub mod assertions; pub mod chain_generics; @@ -48,31 +46,27 @@ pub mod regtest; /// TODO: Add Doc Comment Here! pub fn build_fvks_from_wallet_capability(wallet_capability: &WalletCapability) -> [Fvk; 3] { - let o_fvk = Fvk::Orchard( - orchard::keys::FullViewingKey::try_from(wallet_capability) - .unwrap() - .to_bytes(), - ); - let s_fvk = Fvk::Sapling( - zcash_client_backend::keys::sapling::DiversifiableFullViewingKey::try_from( - wallet_capability, - ) - .unwrap() - .to_bytes(), - ); - let mut t_fvk_bytes = [0u8; 65]; - let t_ext_pk: zingolib::wallet::keys::extended_transparent::ExtendedPubKey = - (wallet_capability).try_into().unwrap(); - t_fvk_bytes[0..32].copy_from_slice(&t_ext_pk.chain_code[..]); - t_fvk_bytes[32..65].copy_from_slice(&t_ext_pk.public_key.serialize()[..]); - let t_fvk = Fvk::P2pkh(t_fvk_bytes); - [o_fvk, s_fvk, t_fvk] + let orchard_vk: orchard::keys::FullViewingKey = + wallet_capability.unified_key_store().try_into().unwrap(); + let sapling_vk: sapling_crypto::zip32::DiversifiableFullViewingKey = + wallet_capability.unified_key_store().try_into().unwrap(); + let transparent_vk: zcash_primitives::legacy::keys::AccountPubKey = + wallet_capability.unified_key_store().try_into().unwrap(); + + let mut transparent_vk_bytes = [0u8; 65]; + transparent_vk_bytes.copy_from_slice(&transparent_vk.serialize()); + + [ + Fvk::Orchard(orchard_vk.to_bytes()), + Fvk::Sapling(sapling_vk.to_bytes()), + Fvk::P2pkh(transparent_vk_bytes), + ] } /// TODO: Add Doc Comment Here! pub async fn build_fvk_client(fvks: &[&Fvk], zingoconfig: &ZingoConfig) -> LightClient { let ufvk = zcash_address::unified::Encoding::encode( - &::try_from_items( + &::try_from_items( fvks.iter().copied().cloned().collect(), ) .unwrap(), @@ -82,14 +76,6 @@ pub async fn build_fvk_client(fvks: &[&Fvk], zingoconfig: &ZingoConfig) -> Light .await .unwrap() } - -/// Converts a Lightclient with spending capability to a Lightclient with only viewing capability -pub async fn sk_client_to_fvk_client(client: &LightClient) -> LightClient { - let [o_fvk, s_fvk, t_fvk] = - build_fvks_from_wallet_capability(&client.wallet.wallet_capability().clone()); - build_fvk_client(&[&o_fvk, &s_fvk, &t_fvk], client.config()).await -} - async fn get_synced_wallet_height(client: &LightClient) -> Result { client.do_sync(true).await?; Ok(client @@ -193,7 +179,7 @@ fn check_orchard_note_summary_equality( }; for i in 0..first.len() { if !(first[i].value() == second[i].value() - && check_spend_status_equality(first[i].spend_status(), second[i].spend_status()) + && check_spend_status_equality(first[i].spend_summary(), second[i].spend_summary()) && first[i].memo() == second[i].memo()) { return false; @@ -212,7 +198,7 @@ fn check_sapling_note_summary_equality( }; for i in 0..first.len() { if !(first[i].value() == second[i].value() - && check_spend_status_equality(first[i].spend_status(), second[i].spend_status()) + && check_spend_status_equality(first[i].spend_summary(), second[i].spend_summary()) && first[i].memo() == second[i].memo()) { return false; @@ -231,7 +217,7 @@ fn check_transparent_coin_summary_equality( }; for i in 0..first.len() { if !(first[i].value() == second[i].value() - && check_spend_status_equality(first[i].spend_status(), second[i].spend_status())) + && check_spend_status_equality(first[i].spend_summary(), second[i].spend_summary())) { return false; } @@ -239,12 +225,16 @@ fn check_transparent_coin_summary_equality( true } -fn check_spend_status_equality(first: SpendStatus, second: SpendStatus) -> bool { +fn check_spend_status_equality(first: SpendSummary, second: SpendSummary) -> bool { matches!( (first, second), - (SpendStatus::Unspent, SpendStatus::Unspent) - | (SpendStatus::Spent(_), SpendStatus::Spent(_)) - | (SpendStatus::PendingSpent(_), SpendStatus::PendingSpent(_)) + (SpendSummary::Unspent, SpendSummary::Unspent) + | (SpendSummary::Spent(_), SpendSummary::Spent(_)) + | ( + SpendSummary::TransmittedSpent(_), + SpendSummary::TransmittedSpent(_) + ) + | (SpendSummary::MempoolSpent(_), SpendSummary::MempoolSpent(_)) ) } @@ -260,7 +250,7 @@ pub async fn send_value_between_clients_and_sync( "recipient address is: {}", &recipient.do_addresses().await[0]["address"] ); - let txid = crate::lightclient::from_inputs::quick_send( + let txid = lightclient::from_inputs::quick_send( sender, vec![( &crate::get_base_address_macro!(recipient, address_type), @@ -321,26 +311,6 @@ async fn check_wallet_chainheight_value(client: &LightClient, target: u32) -> Re Ok(get_synced_wallet_height(client).await? != target) } -/// TODO: Add Doc Comment Here! -pub fn get_wallet_nym(nym: &str) -> Result<(String, PathBuf, PathBuf), String> { - match nym { - "sap_only" | "orch_only" | "orch_and_sapl" | "tadd_only" => { - let one_sapling_wallet = format!( - "{}/tests/data/wallets/v26/202302_release/regtest/{nym}/zingo-wallet.dat", - paths::get_cargo_manifest_dir().to_string_lossy() - ); - let wallet_path = Path::new(&one_sapling_wallet); - let wallet_dir = wallet_path.parent().unwrap(); - Ok(( - one_sapling_wallet.clone(), - wallet_path.to_path_buf(), - wallet_dir.to_path_buf(), - )) - } - _ => Err(format!("nym {nym} not a valid wallet directory")), - } -} - /// TODO: Add Doc Comment Here! pub struct RecordingReader { from: Reader, @@ -358,28 +328,6 @@ where } } -/// TODO: Add Doc Comment Here! -pub async fn load_wallet( - dir: PathBuf, - chaintype: ChainType, -) -> (zingolib::wallet::LightWallet, ZingoConfig) { - let wallet = dir.join("zingo-wallet.dat"); - let lightwalletd_uri = TestEnvironmentGenerator::new(None).get_lightwalletd_uri(); - let zingo_config = - zingoconfig::load_clientconfig(lightwalletd_uri, Some(dir), chaintype, true).unwrap(); - let from = std::fs::File::open(wallet).unwrap(); - - let read_lengths = vec![]; - let mut recording_reader = RecordingReader { from, read_lengths }; - - ( - zingolib::wallet::LightWallet::read_internal(&mut recording_reader, &zingo_config) - .await - .unwrap(), - zingo_config, - ) -} - /// Number of notes created and consumed in a transaction. #[derive(Debug)] pub struct TxNotesCount { @@ -669,27 +617,29 @@ pub mod scenarios { //! All scenarios have a default (i.e. faucet_default) which take minimal parameters and //! build the scenario with the most common settings. This simplifies test writing in //! most cases by removing the need for configuration. - use self::setup::ClientBuilder; use super::regtest::{ChildProcessHandler, RegtestManager}; - use crate::{get_base_address_macro, increase_height_and_wait_for_client}; + use crate::get_base_address_macro; + use crate::lightclient::LightClient; + use crate::testutils::increase_height_and_wait_for_client; + use crate::testvectors::{seeds::HOSPITAL_MUSEUM_SEED, BASE_HEIGHT}; + use setup::ClientBuilder; use zcash_client_backend::{PoolType, ShieldedProtocol}; - use zingo_testvectors::{self, seeds::HOSPITAL_MUSEUM_SEED, BASE_HEIGHT}; - use zingolib::lightclient::LightClient; /// TODO: Add Doc Comment Here! pub mod setup { use super::BASE_HEIGHT; - use zcash_client_backend::{PoolType, ShieldedProtocol}; - use zingo_testvectors::{ + use crate::testvectors::{ seeds, REG_O_ADDR_FROM_ABANDONART, REG_T_ADDR_FROM_ABANDONART, REG_Z_ADDR_FROM_ABANDONART, }; + use zcash_client_backend::{PoolType, ShieldedProtocol}; use super::super::paths::get_regtest_dir; use super::{ChildProcessHandler, RegtestManager}; + use crate::testutils::poll_server_height; + use crate::{lightclient::LightClient, wallet::WalletBase}; use std::path::PathBuf; use tokio::time::sleep; - use zingolib::{lightclient::LightClient, wallet::WalletBase}; /// TODO: Add Doc Comment Here! pub struct ScenarioBuilder { @@ -733,7 +683,7 @@ pub mod scenarios { fn configure_scenario( &mut self, mine_to_pool: Option, - regtest_network: &zingoconfig::RegtestNetwork, + regtest_network: &crate::config::RegtestNetwork, ) { let mine_to_address = match mine_to_pool { Some(PoolType::Shielded(ShieldedProtocol::Orchard)) => { @@ -767,18 +717,14 @@ pub mod scenarios { self.regtest_manager .generate_n_blocks(BASE_HEIGHT - 1) .unwrap(); - while crate::poll_server_height(&self.regtest_manager) - .as_u32() - .unwrap() - < BASE_HEIGHT - { + while poll_server_height(&self.regtest_manager).as_u32().unwrap() < BASE_HEIGHT { sleep(std::time::Duration::from_millis(50)).await; } } /// TODO: Add Doc Comment Here! pub async fn new_load_1153_saplingcb_regtest_chain( - regtest_network: &zingoconfig::RegtestNetwork, + regtest_network: &crate::config::RegtestNetwork, ) -> Self { let mut sb = ScenarioBuilder::build_scenario(None, None); let source = get_regtest_dir().join("data/chain_cache/blocks_1153/zcashd/regtest"); @@ -807,7 +753,7 @@ pub mod scenarios { mine_to_pool: Option, zingo_wallet_dir: Option, set_lightwalletd_port: Option, - regtest_network: &zingoconfig::RegtestNetwork, + regtest_network: &crate::config::RegtestNetwork, ) -> Self { let mut sb = if let Some(conf) = zingo_wallet_dir { ScenarioBuilder::build_scenario(Some(conf), set_lightwalletd_port) @@ -843,8 +789,8 @@ pub mod scenarios { pub fn make_unique_data_dir_and_load_config( &mut self, - regtest_network: zingoconfig::RegtestNetwork, - ) -> zingoconfig::ZingoConfig { + regtest_network: crate::config::RegtestNetwork, + ) -> crate::config::ZingoConfig { //! Each client requires a unique data_dir, we use the //! client_number counter for this. self.client_number += 1; @@ -860,13 +806,13 @@ pub mod scenarios { pub fn create_clientconfig( &self, conf_path: PathBuf, - regtest_network: zingoconfig::RegtestNetwork, - ) -> zingoconfig::ZingoConfig { + regtest_network: crate::config::RegtestNetwork, + ) -> crate::config::ZingoConfig { std::fs::create_dir(&conf_path).unwrap(); - zingoconfig::load_clientconfig( + crate::config::load_clientconfig( self.server_id.clone(), Some(conf_path), - zingoconfig::ChainType::Regtest(regtest_network), + crate::config::ChainType::Regtest(regtest_network), true, ) .unwrap() @@ -876,7 +822,7 @@ pub mod scenarios { pub async fn build_faucet( &mut self, overwrite: bool, - regtest_network: zingoconfig::RegtestNetwork, + regtest_network: crate::config::RegtestNetwork, ) -> LightClient { //! A "faucet" is a lightclient that receives mining rewards self.build_client( @@ -894,7 +840,7 @@ pub mod scenarios { mnemonic_phrase: String, birthday: u64, overwrite: bool, - regtest_network: zingoconfig::RegtestNetwork, + regtest_network: crate::config::RegtestNetwork, ) -> LightClient { let zingo_config = self.make_unique_data_dir_and_load_config(regtest_network); LightClient::create_from_wallet_base_async( @@ -923,12 +869,9 @@ pub mod scenarios { TestEnvironmentGenerator::pick_unused_port_to_string(None); let lightwalletd_rpcservice_port = TestEnvironmentGenerator::pick_unused_port_to_string(set_lightwalletd_port); - let regtest_manager = RegtestManager::new( - tempdir::TempDir::new("zingo_libtonode_test") - .unwrap() - .into_path(), - ); - let server_uri = zingoconfig::construct_lightwalletd_uri(Some(format!( + let regtest_manager = + RegtestManager::new(tempfile::TempDir::new().unwrap().into_path()); + let server_uri = crate::config::construct_lightwalletd_uri(Some(format!( "http://127.0.0.1:{lightwalletd_rpcservice_port}" ))); Self { @@ -943,15 +886,15 @@ pub mod scenarios { pub(crate) fn create_zcash_conf( &self, mine_to_address: Option<&str>, - regtest_network: &zingoconfig::RegtestNetwork, + regtest_network: &crate::config::RegtestNetwork, ) -> PathBuf { let config = match mine_to_address { - Some(address) => zingo_testvectors::config_template_fillers::zcashd::funded( + Some(address) => crate::testvectors::config_template_fillers::zcashd::funded( address, &self.zcashd_rpcservice_port, regtest_network, ), - None => zingo_testvectors::config_template_fillers::zcashd::basic( + None => crate::testvectors::config_template_fillers::zcashd::basic( &self.zcashd_rpcservice_port, regtest_network, "", @@ -964,7 +907,7 @@ pub mod scenarios { pub(crate) fn create_lightwalletd_conf(&self) -> PathBuf { self.write_contents_and_return_path( "lightwalletd", - zingo_testvectors::config_template_fillers::lightwalletd::basic( + crate::testvectors::config_template_fillers::lightwalletd::basic( &self.lightwalletd_rpcservice_port, ), ) @@ -1009,7 +952,7 @@ pub mod scenarios { /// TODO: Add Doc Comment Here! pub async fn unfunded_client( - regtest_network: zingoconfig::RegtestNetwork, + regtest_network: crate::config::RegtestNetwork, ) -> (RegtestManager, ChildProcessHandler, LightClient) { let mut scenario_builder = setup::ScenarioBuilder::build_configure_launch(None, None, None, ®test_network) @@ -1026,7 +969,7 @@ pub mod scenarios { /// TODO: Add Doc Comment Here! pub async fn unfunded_client_default() -> (RegtestManager, ChildProcessHandler, LightClient) { - let regtest_network = zingoconfig::RegtestNetwork::all_upgrades_active(); + let regtest_network = crate::config::RegtestNetwork::all_upgrades_active(); unfunded_client(regtest_network).await } @@ -1042,7 +985,7 @@ pub mod scenarios { /// become interesting (e.g. without experimental features, or txindices) we'll create more setups. pub async fn faucet( mine_to_pool: PoolType, - regtest_network: zingoconfig::RegtestNetwork, + regtest_network: crate::config::RegtestNetwork, ) -> (RegtestManager, ChildProcessHandler, LightClient) { let mut sb = setup::ScenarioBuilder::build_configure_launch( Some(mine_to_pool), @@ -1062,7 +1005,7 @@ pub mod scenarios { /// TODO: Add Doc Comment Here! pub async fn faucet_default() -> (RegtestManager, ChildProcessHandler, LightClient) { - let regtest_network = zingoconfig::RegtestNetwork::all_upgrades_active(); + let regtest_network = crate::config::RegtestNetwork::all_upgrades_active(); faucet( PoolType::Shielded(ShieldedProtocol::Orchard), regtest_network, @@ -1073,7 +1016,7 @@ pub mod scenarios { /// TODO: Add Doc Comment Here! pub async fn faucet_recipient( mine_to_pool: PoolType, - regtest_network: zingoconfig::RegtestNetwork, + regtest_network: crate::config::RegtestNetwork, ) -> ( RegtestManager, ChildProcessHandler, @@ -1114,7 +1057,7 @@ pub mod scenarios { LightClient, LightClient, ) { - let regtest_network = zingoconfig::RegtestNetwork::all_upgrades_active(); + let regtest_network = crate::config::RegtestNetwork::all_upgrades_active(); faucet_recipient( PoolType::Shielded(ShieldedProtocol::Orchard), regtest_network, @@ -1128,7 +1071,7 @@ pub mod scenarios { sapling_funds: Option, transparent_funds: Option, mine_to_pool: PoolType, - regtest_network: zingoconfig::RegtestNetwork, + regtest_network: crate::config::RegtestNetwork, ) -> ( RegtestManager, ChildProcessHandler, @@ -1145,7 +1088,7 @@ pub mod scenarios { .unwrap(); let orchard_txid = if let Some(funds) = orchard_funds { Some( - crate::lightclient::from_inputs::quick_send( + super::lightclient::from_inputs::quick_send( &faucet, vec![(&get_base_address_macro!(recipient, "unified"), funds, None)], ) @@ -1159,7 +1102,7 @@ pub mod scenarios { }; let sapling_txid = if let Some(funds) = sapling_funds { Some( - crate::lightclient::from_inputs::quick_send( + super::lightclient::from_inputs::quick_send( &faucet, vec![(&get_base_address_macro!(recipient, "sapling"), funds, None)], ) @@ -1173,7 +1116,7 @@ pub mod scenarios { }; let transparent_txid = if let Some(funds) = transparent_funds { Some( - crate::lightclient::from_inputs::quick_send( + super::lightclient::from_inputs::quick_send( &faucet, vec![( &get_base_address_macro!(recipient, "transparent"), @@ -1214,7 +1157,7 @@ pub mod scenarios { LightClient, String, ) { - let regtest_network = zingoconfig::RegtestNetwork::all_upgrades_active(); + let regtest_network = crate::config::RegtestNetwork::all_upgrades_active(); let ( regtest_manager, cph, @@ -1243,7 +1186,7 @@ pub mod scenarios { /// TODO: Add Doc Comment Here! pub async fn custom_clients( mine_to_pool: PoolType, - regtest_network: zingoconfig::RegtestNetwork, + regtest_network: crate::config::RegtestNetwork, ) -> (RegtestManager, ChildProcessHandler, ClientBuilder) { let sb = setup::ScenarioBuilder::build_configure_launch( Some(mine_to_pool), @@ -1264,9 +1207,9 @@ pub mod scenarios { RegtestManager, ChildProcessHandler, ClientBuilder, - zingoconfig::RegtestNetwork, + crate::config::RegtestNetwork, ) { - let regtest_network = zingoconfig::RegtestNetwork::all_upgrades_active(); + let regtest_network = crate::config::RegtestNetwork::all_upgrades_active(); let (regtest_manager, cph, client_builder) = custom_clients( PoolType::Shielded(ShieldedProtocol::Orchard), regtest_network, @@ -1277,7 +1220,7 @@ pub mod scenarios { /// TODO: Add Doc Comment Here! pub async fn unfunded_mobileclient() -> (RegtestManager, ChildProcessHandler) { - let regtest_network = zingoconfig::RegtestNetwork::all_upgrades_active(); + let regtest_network = crate::config::RegtestNetwork::all_upgrades_active(); let scenario_builder = setup::ScenarioBuilder::build_configure_launch( None, None, @@ -1293,7 +1236,7 @@ pub mod scenarios { /// TODO: Add Doc Comment Here! pub async fn funded_orchard_mobileclient(value: u64) -> (RegtestManager, ChildProcessHandler) { - let regtest_network = zingoconfig::RegtestNetwork::all_upgrades_active(); + let regtest_network = crate::config::RegtestNetwork::all_upgrades_active(); let mut scenario_builder = setup::ScenarioBuilder::build_configure_launch( Some(PoolType::Shielded(ShieldedProtocol::Sapling)), None, @@ -1310,7 +1253,7 @@ pub mod scenarios { .build_client(HOSPITAL_MUSEUM_SEED.to_string(), 0, false, regtest_network) .await; faucet.do_sync(false).await.unwrap(); - crate::lightclient::from_inputs::quick_send( + super::lightclient::from_inputs::quick_send( &faucet, vec![(&get_base_address_macro!(recipient, "unified"), value, None)], ) @@ -1330,7 +1273,7 @@ pub mod scenarios { pub async fn funded_orchard_with_3_txs_mobileclient( value: u64, ) -> (RegtestManager, ChildProcessHandler) { - let regtest_network = zingoconfig::RegtestNetwork::all_upgrades_active(); + let regtest_network = crate::config::RegtestNetwork::all_upgrades_active(); let mut scenario_builder = setup::ScenarioBuilder::build_configure_launch( Some(PoolType::Shielded(ShieldedProtocol::Sapling)), None, @@ -1350,7 +1293,7 @@ pub mod scenarios { .await .unwrap(); // received from a faucet - crate::lightclient::from_inputs::quick_send( + super::lightclient::from_inputs::quick_send( &faucet, vec![(&get_base_address_macro!(recipient, "unified"), value, None)], ) @@ -1360,7 +1303,7 @@ pub mod scenarios { .await .unwrap(); // send to a faucet - crate::lightclient::from_inputs::quick_send( + super::lightclient::from_inputs::quick_send( &recipient, vec![( &get_base_address_macro!(faucet, "unified"), @@ -1374,7 +1317,7 @@ pub mod scenarios { .await .unwrap(); // send to self sapling - crate::lightclient::from_inputs::quick_send( + super::lightclient::from_inputs::quick_send( &recipient, vec![( &get_base_address_macro!(recipient, "sapling"), @@ -1398,7 +1341,7 @@ pub mod scenarios { pub async fn funded_orchard_sapling_transparent_shielded_mobileclient( value: u64, ) -> (RegtestManager, ChildProcessHandler) { - let regtest_network = zingoconfig::RegtestNetwork::all_upgrades_active(); + let regtest_network = crate::config::RegtestNetwork::all_upgrades_active(); let mut scenario_builder = setup::ScenarioBuilder::build_configure_launch( Some(PoolType::Shielded(ShieldedProtocol::Sapling)), None, @@ -1418,7 +1361,7 @@ pub mod scenarios { .await .unwrap(); // received from a faucet to orchard - crate::lightclient::from_inputs::quick_send( + super::lightclient::from_inputs::quick_send( &faucet, vec![( &get_base_address_macro!(recipient, "unified"), @@ -1432,7 +1375,7 @@ pub mod scenarios { .await .unwrap(); // received from a faucet to sapling - crate::lightclient::from_inputs::quick_send( + super::lightclient::from_inputs::quick_send( &faucet, vec![( &get_base_address_macro!(recipient, "sapling"), @@ -1446,7 +1389,7 @@ pub mod scenarios { .await .unwrap(); // received from a faucet to transparent - crate::lightclient::from_inputs::quick_send( + super::lightclient::from_inputs::quick_send( &faucet, vec![( &get_base_address_macro!(recipient, "transparent"), @@ -1460,7 +1403,7 @@ pub mod scenarios { .await .unwrap(); // send to a faucet - crate::lightclient::from_inputs::quick_send( + super::lightclient::from_inputs::quick_send( &recipient, vec![( &get_base_address_macro!(faucet, "unified"), @@ -1474,7 +1417,7 @@ pub mod scenarios { .await .unwrap(); // send to self orchard - crate::lightclient::from_inputs::quick_send( + super::lightclient::from_inputs::quick_send( &recipient, vec![( &get_base_address_macro!(recipient, "unified"), @@ -1488,7 +1431,7 @@ pub mod scenarios { .await .unwrap(); // send to self sapling - crate::lightclient::from_inputs::quick_send( + super::lightclient::from_inputs::quick_send( &recipient, vec![( &get_base_address_macro!(recipient, "sapling"), @@ -1502,7 +1445,7 @@ pub mod scenarios { .await .unwrap(); // send to self transparent - crate::lightclient::from_inputs::quick_send( + super::lightclient::from_inputs::quick_send( &recipient, vec![( &get_base_address_macro!(recipient, "transparent"), @@ -1537,7 +1480,7 @@ pub mod scenarios { /// TODO: Add Doc Comment Here! pub async fn unsynced_basic() -> ChildProcessHandler { - let regtest_network = zingoconfig::RegtestNetwork::all_upgrades_active(); + let regtest_network = crate::config::RegtestNetwork::all_upgrades_active(); setup::ScenarioBuilder::new_load_1153_saplingcb_regtest_chain(®test_network) .await .child_process_handler @@ -1551,7 +1494,7 @@ pub mod scenarios { LightClient, LightClient, ) { - let regtest_network = zingoconfig::RegtestNetwork::all_upgrades_active(); + let regtest_network = crate::config::RegtestNetwork::all_upgrades_active(); let mut sb = setup::ScenarioBuilder::new_load_1153_saplingcb_regtest_chain(®test_network) .await; @@ -1576,7 +1519,7 @@ pub mod scenarios { LightClient, LightClient, ) { - let regtest_network = zingoconfig::RegtestNetwork::all_upgrades_active(); + let regtest_network = crate::config::RegtestNetwork::all_upgrades_active(); let mut sb = setup::ScenarioBuilder::new_load_1153_saplingcb_regtest_chain(®test_network) .await; diff --git a/zingo-testutils/src/assertions.rs b/zingolib/src/testutils/assertions.rs similarity index 81% rename from zingo-testutils/src/assertions.rs rename to zingolib/src/testutils/assertions.rs index 7a5041812b..975de37604 100644 --- a/zingo-testutils/src/assertions.rs +++ b/zingolib/src/testutils/assertions.rs @@ -5,15 +5,16 @@ use nonempty::NonEmpty; use zcash_client_backend::proposal::Proposal; use zcash_primitives::transaction::TxId; +use crate::{lightclient::LightClient, wallet::notes::query::OutputQuery}; use zingo_status::confirmation_status::ConfirmationStatus; -use zingolib::{lightclient::LightClient, wallet::notes::query::OutputQuery}; /// currently checks: /// 1. len of txids == num steps /// 2. the txid is stored in the records_by_ids database /// 3. if the fee from the calculate_transaction_fee matches the sum of the per-step fees -/// this currently fails for any broadcast but not confirmed transaction: it seems like get_transaction_fee does not recognize pending spends -/// returns the total fee for the transfer +/// this currently fails for any broadcast but not confirmed transaction: it seems like +/// get_transaction_fee does not recognize pending spends returns the total fee for the +/// transfer pub async fn assert_record_fee_and_status( client: &LightClient, proposal: &Proposal, @@ -31,12 +32,19 @@ pub async fn assert_record_fee_and_status( assert_eq!(proposal.steps().len(), txids.len()); let mut total_fee = 0; for (i, step) in proposal.steps().iter().enumerate() { - let record = records.get(&txids[i]).expect("sender must recognize txid"); + let record = records.get(&txids[i]).unwrap_or_else(|| { + panic!( + "sender must recognize txid.\nExpected {}\nRecognised: {:?}", + txids[i], + records.0.values().collect::>() + ) + }); // does this record match this step? // we can check that it has the expected status assert_eq!(record.status, expected_status); // may fail in uncertain ways if used on a transaction we dont have an OutgoingViewingKey for let recorded_fee = records.calculate_transaction_fee(record).unwrap(); + assert_eq!(recorded_fee, step.balance().fee_required().into_u64()); total_fee += recorded_fee; diff --git a/zingo-testutils/src/chain_generics.rs b/zingolib/src/testutils/chain_generics.rs similarity index 96% rename from zingo-testutils/src/chain_generics.rs rename to zingolib/src/testutils/chain_generics.rs index 5359bbc95f..3beba2d675 100644 --- a/zingo-testutils/src/chain_generics.rs +++ b/zingolib/src/testutils/chain_generics.rs @@ -6,12 +6,12 @@ //! darkside known issues: //! - transparent //! - txids - +//! //! libtonode known issues: //! - mempool pub mod conduct_chain; +pub mod live_chain; pub mod fixtures; - pub mod with_assertions; diff --git a/zingo-testutils/src/chain_generics/conduct_chain.rs b/zingolib/src/testutils/chain_generics/conduct_chain.rs similarity index 68% rename from zingo-testutils/src/chain_generics/conduct_chain.rs rename to zingolib/src/testutils/chain_generics/conduct_chain.rs index 5084546b6a..1287583805 100644 --- a/zingo-testutils/src/chain_generics/conduct_chain.rs +++ b/zingolib/src/testutils/chain_generics/conduct_chain.rs @@ -3,8 +3,9 @@ //! lib-to-node, which links a lightserver to a zcashd in regtest mode. see `impl ConductChain for LibtoNode //! darkside, a mode for the lightserver which mocks zcashd. search 'impl ConductChain for DarksideScenario -use crate::{get_base_address_macro, lightclient::from_inputs}; -use zingolib::lightclient::LightClient; +use crate::get_base_address_macro; +use crate::testutils::lightclient::from_inputs; +use crate::{lightclient::LightClient, wallet::LightWallet}; #[allow(async_fn_in_trait)] #[allow(opaque_hidden_inferred_bound)] @@ -16,8 +17,33 @@ pub trait ConductChain { async fn setup() -> Self; /// builds a faucet (funded from mining) async fn create_faucet(&mut self) -> LightClient; + + /// sets server parameters + fn zingo_config(&mut self) -> crate::config::ZingoConfig; + /// builds an empty client - async fn create_client(&mut self) -> LightClient; + async fn create_client(&mut self) -> LightClient { + let mut zingo_config = self.zingo_config(); + zingo_config.accept_server_txids = true; + LightClient::create_from_wallet_base_async( + crate::wallet::WalletBase::FreshEntropy, + &zingo_config, + 0, + false, + ) + .await + .unwrap() + } + + /// loads a client from bytes + async fn load_client(&mut self, data: &[u8]) -> LightClient { + let mut zingo_config = self.zingo_config(); + zingo_config.accept_server_txids = true; + + LightClient::create_from_wallet_async(LightWallet::unsafe_from_buffer_testnet(data).await) + .await + .unwrap() + } /// moves the chain tip forward, creating 1 new block /// and confirming transactions that were received by the server diff --git a/zingo-testutils/src/chain_generics/fixtures.rs b/zingolib/src/testutils/chain_generics/fixtures.rs similarity index 79% rename from zingo-testutils/src/chain_generics/fixtures.rs rename to zingolib/src/testutils/chain_generics/fixtures.rs index 35799858d6..a17a44eb90 100644 --- a/zingo-testutils/src/chain_generics/fixtures.rs +++ b/zingolib/src/testutils/chain_generics/fixtures.rs @@ -10,16 +10,18 @@ use zcash_client_backend::ShieldedProtocol::Orchard; use zcash_client_backend::ShieldedProtocol::Sapling; use zcash_primitives::transaction::fees::zip317::MARGINAL_FEE; -use zingolib::lightclient::LightClient; -use zingolib::wallet::notes::query::OutputSpendStatusQuery; -use zingolib::wallet::notes::{query::OutputPoolQuery, OutputInterface}; -use zingolib::wallet::{data::summaries::ValueTransferKind, notes::query::OutputQuery}; - -use crate::chain_generics::conduct_chain::ConductChain; -use crate::chain_generics::with_assertions; -use crate::fee_tables; -use crate::lightclient::from_inputs; -use crate::lightclient::get_base_address; +use crate::lightclient::LightClient; +use crate::wallet::data::summaries::SelfSendValueTransfer; +use crate::wallet::data::summaries::SentValueTransfer; +use crate::wallet::notes::query::OutputSpendStatusQuery; +use crate::wallet::notes::{query::OutputPoolQuery, OutputInterface}; +use crate::wallet::{data::summaries::ValueTransferKind, notes::query::OutputQuery}; + +use crate::testutils::chain_generics::conduct_chain::ConductChain; +use crate::testutils::chain_generics::with_assertions; +use crate::testutils::fee_tables; +use crate::testutils::lightclient::from_inputs; +use crate::testutils::lightclient::get_base_address; /// Fixture for testing various vt transactions pub async fn create_various_value_transfers() @@ -62,11 +64,13 @@ where ); assert_eq!( sender.value_transfers().await.0[1].kind(), - ValueTransferKind::Sent + ValueTransferKind::Sent(SentValueTransfer::Send) ); assert_eq!( sender.value_transfers().await.0[2].kind(), - ValueTransferKind::MemoToSelf + ValueTransferKind::Sent(SentValueTransfer::SendToSelf( + SelfSendValueTransfer::MemoToSelf + )) ); assert_eq!(recipient.value_transfers().await.0.len(), 1); assert_eq!( @@ -84,14 +88,14 @@ where assert_eq!(sender.value_transfers().await.0.len(), 4); assert_eq!( sender.value_transfers().await.0[3].kind(), - ValueTransferKind::SendToSelf + ValueTransferKind::Sent(SentValueTransfer::SendToSelf(SelfSendValueTransfer::Basic)) ); with_assertions::propose_shield_bump_sync(&mut environment, &sender, false).await; assert_eq!(sender.value_transfers().await.0.len(), 5); assert_eq!( sender.value_transfers().await.0[4].kind(), - ValueTransferKind::Shield + ValueTransferKind::Sent(SentValueTransfer::SendToSelf(SelfSendValueTransfer::Shield)) ); } /// runs a send-to-receiver and receives it in a chain-generic context @@ -287,7 +291,7 @@ where let spent_orchard_outputs: Vec<_> = secondary_outputs .iter() .filter(|o| matches!(o.pool_type(), Shielded(Orchard))) - .filter(|o| o.is_spent()) + .filter(|o| o.is_spent_confirmed()) .collect(); assert_eq!(spent_orchard_outputs.len(), 1); } @@ -487,7 +491,7 @@ where let spent_sapling_outputs: Vec<_> = all_outputs .iter() .filter(|o| matches!(o.pool_type(), Shielded(Sapling))) - .filter(|o| o.is_spent()) + .filter(|o| o.is_spent_confirmed()) .collect(); assert_eq!( spent_sapling_outputs.len(), @@ -513,23 +517,19 @@ where .await; let tertiary = environment.create_client().await; - let expected_fee = fee_tables::one_to_one(shpool, pool, true); - // assert_eq!( - // secondary - // .propose_send_all(tertiary, - // get_base_address(tertiary, pool)) - // .await - // .into_u64(), - // 0 - // ); + let expected_fee = fee_tables::one_to_one(Some(shpool), pool, true); + let ref_primary: Arc = Arc::new(primary); + let ref_secondary: Arc = Arc::new(secondary); let ref_tertiary: Arc = Arc::new(tertiary); // mempool monitor - let check_mempool = false; + let check_mempool = !cfg!(feature = "ci"); if check_mempool { - LightClient::start_mempool_monitor(ref_tertiary.clone()); - dbg!("mm started"); + for lightclient in [&ref_primary, &ref_secondary, &ref_tertiary] { + LightClient::start_mempool_monitor(lightclient.clone()); + dbg!("mm started"); + } tokio::time::sleep(std::time::Duration::from_secs(5)).await; } @@ -537,10 +537,104 @@ where expected_fee, with_assertions::propose_send_bump_sync_all_recipients( &mut environment, - &secondary, + &ref_secondary, vec![(&ref_tertiary, pool, 100_000 - expected_fee, None)], check_mempool, ) .await ); } + +/// the simplest test that sends from a specific shielded pool to another specific pool. error variant. +pub async fn shpool_to_pool_insufficient_error( + shpool: ShieldedProtocol, + pool: PoolType, + underflow_amount: u64, +) where + CC: ConductChain, +{ + let mut environment = CC::setup().await; + + let primary = environment.fund_client_orchard(1_000_000).await; + let secondary = environment.create_client().await; + + let expected_fee = fee_tables::one_to_one(Some(shpool), pool, true); + let secondary_fund = 100_000 + expected_fee - underflow_amount; + with_assertions::propose_send_bump_sync_all_recipients( + &mut environment, + &primary, + vec![(&secondary, Shielded(shpool), secondary_fund, None)], + false, + ) + .await; + + let tertiary = environment.create_client().await; + + let ref_secondary: Arc = Arc::new(secondary); + let ref_tertiary: Arc = Arc::new(tertiary); + + let tertiary_fund = 100_000; + assert_eq!( + from_inputs::propose( + &ref_secondary, + vec![( + ref_tertiary + .wallet + .get_first_address(pool) + .unwrap() + .as_str(), + tertiary_fund, + None, + )], + ) + .await + .unwrap_err() + .to_string(), + format!( + "Insufficient balance (have {}, need {} including fee)", + secondary_fund, + tertiary_fund + expected_fee + ) + ); +} + +/// the simplest test that sends from a specific shielded pool to another specific pool. also known as simpool. +pub async fn to_pool_unfunded_error(pool: PoolType, try_amount: u64) +where + CC: ConductChain, +{ + let mut environment = CC::setup().await; + + let secondary = environment.create_client().await; + let tertiary = environment.create_client().await; + + let ref_secondary: Arc = Arc::new(secondary); + let ref_tertiary: Arc = Arc::new(tertiary); + + ref_secondary.do_sync(false).await.unwrap(); + + let expected_fee = fee_tables::one_to_one(None, pool, true); + + assert_eq!( + from_inputs::propose( + &ref_secondary, + vec![( + ref_tertiary + .wallet + .get_first_address(pool) + .unwrap() + .as_str(), + try_amount, + None, + )], + ) + .await + .unwrap_err() + .to_string(), + format!( + "Insufficient balance (have {}, need {} including fee)", + 0, + try_amount + expected_fee + ) + ); +} diff --git a/zingolib/src/testutils/chain_generics/live_chain.rs b/zingolib/src/testutils/chain_generics/live_chain.rs new file mode 100644 index 0000000000..b25d21d3d6 --- /dev/null +++ b/zingolib/src/testutils/chain_generics/live_chain.rs @@ -0,0 +1,32 @@ +//! implementation of conduct chain for live chains + +use crate::lightclient::LightClient; + +use super::conduct_chain::ConductChain; + +/// this is essentially a placeholder. +/// allows using existing ChainGeneric functions with TestNet wallets +pub struct LiveChain; + +impl ConductChain for LiveChain { + async fn setup() -> Self { + Self {} + } + + async fn create_faucet(&mut self) -> LightClient { + unimplemented!() + } + + fn zingo_config(&mut self) -> crate::config::ZingoConfig { + todo!() + } + + async fn bump_chain(&mut self) { + // average block time is 75 seconds. we do this twice here to insist on a new block + tokio::time::sleep(std::time::Duration::from_secs(150)).await; + } + + fn get_chain_height(&mut self) -> u32 { + unimplemented!() + } +} diff --git a/zingo-testutils/src/chain_generics/with_assertions.rs b/zingolib/src/testutils/chain_generics/with_assertions.rs similarity index 73% rename from zingo-testutils/src/chain_generics/with_assertions.rs rename to zingolib/src/testutils/chain_generics/with_assertions.rs index 024daaba8a..c3f0265fcf 100644 --- a/zingo-testutils/src/chain_generics/with_assertions.rs +++ b/zingolib/src/testutils/chain_generics/with_assertions.rs @@ -1,9 +1,9 @@ //! lightclient functions with added assertions. used for tests. +use crate::lightclient::LightClient; use zcash_client_backend::PoolType; -use zingolib::lightclient::LightClient; -use crate::{ +use crate::testutils::{ assertions::{assert_recipient_total_lte_to_proposal_total, assert_record_fee_and_status}, chain_generics::conduct_chain::ConductChain, lightclient::{from_inputs, get_base_address}, @@ -43,14 +43,20 @@ where .await .unwrap(); - let send_height = environment.get_chain_height() + 1; + let send_height = sender + .wallet + .get_target_height_and_anchor_offset() + .await + .expect("sender has a target height") + .0; // digesting the calculated transaction + // this step happens after transaction is recorded locally, but before learning anything about whether the server accepted it let recorded_fee = assert_record_fee_and_status( sender, &proposal, &txids, - ConfirmationStatus::Pending(send_height.into()), + ConfirmationStatus::Transmitted(send_height.into()), ) .await; @@ -59,11 +65,16 @@ where if test_mempool { // mempool scan shows the same sender.do_sync(false).await.unwrap(); + + // let the mempool monitor get a chance + // to listen + tokio::time::sleep(std::time::Duration::from_secs(6)).await; + assert_record_fee_and_status( sender, &proposal, &txids, - ConfirmationStatus::Pending(send_height.into()), + ConfirmationStatus::Mempool(send_height.into()), ) .await; @@ -71,13 +82,20 @@ where for (recipient, _, _, _) in sends.clone() { if send_ua_id != recipient.do_addresses().await[0]["address"].clone() { recipient.do_sync(false).await.unwrap(); - assert_record_fee_and_status( - recipient, - &proposal, - &txids, - ConfirmationStatus::Pending(send_height.into()), - ) - .await; + let records = &recipient + .wallet + .transaction_context + .transaction_metadata_set + .read() + .await + .transaction_records_by_id; + for txid in &txids { + let record = records.get(txid).expect("recipient must recognize txid"); + assert_eq!( + record.status, + ConfirmationStatus::Mempool(send_height.into()), + ) + } } } } @@ -115,7 +133,13 @@ where { let proposal = client.propose_shield().await.unwrap(); - let send_height = environment.get_chain_height() + 1; + let send_height = client + .wallet + .get_target_height_and_anchor_offset() + .await + .expect("sender has a target height") + .0; + let txids = client .complete_and_broadcast_stored_proposal() .await @@ -126,7 +150,7 @@ where client, &proposal, &txids, - ConfirmationStatus::Pending(send_height.into()), + ConfirmationStatus::Transmitted(send_height.into()), ) .await; @@ -137,7 +161,7 @@ where client, &proposal, &txids, - ConfirmationStatus::Pending(send_height.into()), + ConfirmationStatus::Mempool(send_height.into()), ) .await; } diff --git a/zingo-testutils/src/fee_tables.rs b/zingolib/src/testutils/fee_tables.rs similarity index 89% rename from zingo-testutils/src/fee_tables.rs rename to zingolib/src/testutils/fee_tables.rs index 553f36781c..64bd055d0e 100644 --- a/zingo-testutils/src/fee_tables.rs +++ b/zingolib/src/testutils/fee_tables.rs @@ -13,7 +13,11 @@ use zcash_primitives::transaction::fees::zip317::MARGINAL_FEE; /// estimates a fee based on the zip317 protocol rules /// -pub fn one_to_one(source_protocol: ShieldedProtocol, target_pool: PoolType, change: bool) -> u64 { +pub fn one_to_one( + source_protocol: Option, + target_pool: PoolType, + change: bool, +) -> u64 { let transparent_inputs = 0; let mut transparent_outputs = 0; let mut sapling_inputs = 0; @@ -21,8 +25,9 @@ pub fn one_to_one(source_protocol: ShieldedProtocol, target_pool: PoolType, chan let mut orchard_inputs = 0; let mut orchard_outputs = 0; match source_protocol { - Sapling => sapling_inputs += 1, - Orchard => orchard_inputs += 1, + Some(Sapling) => sapling_inputs += 1, + Some(Orchard) => orchard_inputs += 1, + _ => {} } match target_pool { Transparent => transparent_outputs += 1, diff --git a/zingo-testutils/src/grpc_proxy.rs b/zingolib/src/testutils/grpc_proxy.rs similarity index 98% rename from zingo-testutils/src/grpc_proxy.rs rename to zingolib/src/testutils/grpc_proxy.rs index d3a6ba4c34..df7b7a81cc 100644 --- a/zingo-testutils/src/grpc_proxy.rs +++ b/zingolib/src/testutils/grpc_proxy.rs @@ -15,7 +15,7 @@ use zcash_client_backend::proto::{ }, }; -use crate::port_to_localhost_uri; +use super::port_to_localhost_uri; macro_rules! define_grpc_passthrough { (fn @@ -186,7 +186,7 @@ impl CompactTxStreamer for ProxyServer { 'life0: 'async_trait, Self: 'async_trait, { - todo!("this isn't expected to be called. Please implement this if you need it") + unimplemented!("this isn't expected to be called. Please implement this if you need it") } #[doc = "Server streaming response type for the GetMempoolTx method."] diff --git a/zingo-testutils/src/interrupts.rs b/zingolib/src/testutils/interrupts.rs similarity index 89% rename from zingo-testutils/src/interrupts.rs rename to zingolib/src/testutils/interrupts.rs index 2d4a2b10a3..d2c3565021 100644 --- a/zingo-testutils/src/interrupts.rs +++ b/zingolib/src/testutils/interrupts.rs @@ -1,6 +1,6 @@ //! TODO: Add Mod Description Here! -use zingolib::lightclient::LightClient; +use crate::lightclient::LightClient; /// TODO: Add Doc Comment Here! pub async fn sync_with_timeout_millis(lightclient: &LightClient, timeout: u64) -> Result<(), ()> { diff --git a/zingo-testutils/src/lightclient.rs b/zingolib/src/testutils/lightclient.rs similarity index 73% rename from zingo-testutils/src/lightclient.rs rename to zingolib/src/testutils/lightclient.rs index ca0b05a884..7a9c4ae79d 100644 --- a/zingo-testutils/src/lightclient.rs +++ b/zingolib/src/testutils/lightclient.rs @@ -1,7 +1,7 @@ //! This mod is mostly to take inputs, raw data amd comvert it into lightclient actions //! (obvisouly) in a test environment. +use crate::{error::ZingoLibError, lightclient::LightClient}; use zcash_client_backend::{PoolType, ShieldedProtocol}; -use zingolib::{error::ZingoLibError, lightclient::LightClient}; /// Create a lightclient from the buffer of another pub async fn new_client_from_save_buffer( @@ -36,14 +36,14 @@ pub async fn get_fees_paid_by_client(client: &LightClient) -> u64 { /// Helpers to provide raw_receivers to lightclients for send and shield, etc. pub mod from_inputs { - use zingolib::lightclient::{send::send_with_proposal::QuickSendError, LightClient}; + use crate::lightclient::{send::send_with_proposal::QuickSendError, LightClient}; /// Panics if the address, amount or memo conversion fails. pub async fn quick_send( - quick_sender: &zingolib::lightclient::LightClient, + quick_sender: &crate::lightclient::LightClient, raw_receivers: Vec<(&str, u64, Option<&str>)>, ) -> Result, QuickSendError> { - let request = transaction_request_from_send_inputs(quick_sender, raw_receivers) + let request = transaction_request_from_send_inputs(raw_receivers) .expect("should be able to create a transaction request as receivers are valid."); quick_sender.quick_send(request).await } @@ -51,36 +51,33 @@ pub mod from_inputs { /// Panics if the address, amount or memo conversion fails. pub fn receivers_from_send_inputs( raw_receivers: Vec<(&str, u64, Option<&str>)>, - chain: &zingoconfig::ChainType, - ) -> zingolib::data::receivers::Receivers { + ) -> crate::data::receivers::Receivers { raw_receivers .into_iter() .map(|(address, amount, memo)| { - let recipient_address = - zingolib::utils::conversion::address_from_str(address, chain) - .expect("should be a valid address"); - let amount = zingolib::utils::conversion::zatoshis_from_u64(amount) + let recipient_address = crate::utils::conversion::address_from_str(address) + .expect("should be a valid address"); + let amount = crate::utils::conversion::zatoshis_from_u64(amount) .expect("should be inside the range of valid zatoshis"); let memo = memo.map(|memo| { - zingolib::wallet::utils::interpret_memo_string(memo.to_string()) + crate::wallet::utils::interpret_memo_string(memo.to_string()) .expect("should be able to interpret memo") }); - zingolib::data::receivers::Receiver::new(recipient_address, amount, memo) + crate::data::receivers::Receiver::new(recipient_address, amount, memo) }) .collect() } /// Creates a [`zcash_client_backend::zip321::TransactionRequest`] from rust primitives for simplified test writing. pub fn transaction_request_from_send_inputs( - requester: &zingolib::lightclient::LightClient, raw_receivers: Vec<(&str, u64, Option<&str>)>, ) -> Result< zcash_client_backend::zip321::TransactionRequest, zcash_client_backend::zip321::Zip321Error, > { - let receivers = receivers_from_send_inputs(raw_receivers, &requester.config().chain); - zingolib::data::receivers::transaction_request_from_receivers(receivers) + let receivers = receivers_from_send_inputs(raw_receivers); + crate::data::receivers::transaction_request_from_receivers(receivers) } /// Panics if the address, amount or memo conversion fails. @@ -88,10 +85,10 @@ pub mod from_inputs { proposer: &LightClient, raw_receivers: Vec<(&str, u64, Option<&str>)>, ) -> Result< - zingolib::data::proposal::ProportionalFeeProposal, - zingolib::lightclient::propose::ProposeSendError, + crate::data::proposal::ProportionalFeeProposal, + crate::wallet::propose::ProposeSendError, > { - let request = transaction_request_from_send_inputs(proposer, raw_receivers) + let request = transaction_request_from_send_inputs(raw_receivers) .expect("should be able to create a transaction request as receivers are valid."); proposer.propose_send(request).await } diff --git a/zingo-testutils/src/macros.rs b/zingolib/src/testutils/macros.rs similarity index 97% rename from zingo-testutils/src/macros.rs rename to zingolib/src/testutils/macros.rs index cd3794f43b..62e00bd93e 100644 --- a/zingo-testutils/src/macros.rs +++ b/zingolib/src/testutils/macros.rs @@ -26,6 +26,8 @@ macro_rules! get_base_address_macro { #[macro_export] macro_rules! check_client_balances { ($client:ident, o: $orchard:tt s: $sapling:tt t: $transparent:tt) => { + use zingolib::wallet::data::summaries::TransactionSummaryInterface as _; + let balance = $client.do_balance().await; assert_eq!( balance.orchard_balance.unwrap(), diff --git a/zingo-testutils/src/paths.rs b/zingolib/src/testutils/paths.rs similarity index 100% rename from zingo-testutils/src/paths.rs rename to zingolib/src/testutils/paths.rs diff --git a/zingo-testutils/src/regtest.rs b/zingolib/src/testutils/regtest.rs similarity index 98% rename from zingo-testutils/src/regtest.rs rename to zingolib/src/testutils/regtest.rs index 2a75f7a499..e6cac46155 100644 --- a/zingo-testutils/src/regtest.rs +++ b/zingolib/src/testutils/regtest.rs @@ -203,10 +203,11 @@ pub fn launch_lightwalletd( lightwalletd_child } +#[cfg(not(feature = "zaino-test"))] fn write_zcash_conf(location: &PathBuf) { // This is the only data we need to supply *to* the zcashd, the other files are created by zcashd and lightwalletd use std::io::Write; - let conf_bytes: &'static [u8] = include_bytes!("../regtest/conf/zcash.conf"); + let conf_bytes: &'static [u8] = include_bytes!("regtest/conf/zcash.conf"); File::create(location) .unwrap() .write_all(conf_bytes) @@ -220,7 +221,10 @@ impl RegtestManager { let confs_dir = regtest_dir.join("conf"); let zcashd_config = confs_dir.join("zcash.conf"); std::fs::create_dir_all(&confs_dir).expect("Couldn't create dir."); - write_zcash_conf(&zcashd_config); + #[cfg(not(feature = "zaino-test"))] + { + write_zcash_conf(&zcashd_config); + } let bin_dir = super::paths::get_bin_dir(); std::fs::create_dir_all(&bin_dir).expect("Couldn't create dir."); let cli_bin = bin_dir.join("zcash-cli"); diff --git a/zingo-testutils/regtest/bin/.gitignore b/zingolib/src/testutils/regtest/bin/.gitignore similarity index 100% rename from zingo-testutils/regtest/bin/.gitignore rename to zingolib/src/testutils/regtest/bin/.gitignore diff --git a/zingo-testutils/regtest/bin/.gitkeep b/zingolib/src/testutils/regtest/bin/.gitkeep similarity index 100% rename from zingo-testutils/regtest/bin/.gitkeep rename to zingolib/src/testutils/regtest/bin/.gitkeep diff --git a/zingo-testutils/regtest/conf/lightwalletd.yml b/zingolib/src/testutils/regtest/conf/lightwalletd.yml similarity index 100% rename from zingo-testutils/regtest/conf/lightwalletd.yml rename to zingolib/src/testutils/regtest/conf/lightwalletd.yml diff --git a/zingo-testutils/regtest/conf/zcash.conf b/zingolib/src/testutils/regtest/conf/zcash.conf similarity index 100% rename from zingo-testutils/regtest/conf/zcash.conf rename to zingolib/src/testutils/regtest/conf/zcash.conf diff --git a/zingo-testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/.lock b/zingolib/src/testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/.lock similarity index 100% rename from zingo-testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/.lock rename to zingolib/src/testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/.lock diff --git a/zingo-testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/banlist.dat b/zingolib/src/testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/banlist.dat similarity index 100% rename from zingo-testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/banlist.dat rename to zingolib/src/testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/banlist.dat diff --git a/zingo-testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/blocks/blk00000.dat b/zingolib/src/testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/blocks/blk00000.dat similarity index 100% rename from zingo-testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/blocks/blk00000.dat rename to zingolib/src/testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/blocks/blk00000.dat diff --git a/zingo-testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/blocks/index/000005.ldb b/zingolib/src/testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/blocks/index/000005.ldb similarity index 100% rename from zingo-testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/blocks/index/000005.ldb rename to zingolib/src/testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/blocks/index/000005.ldb diff --git a/zingo-testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/blocks/index/000006.log b/zingolib/src/testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/blocks/index/000006.log similarity index 100% rename from zingo-testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/blocks/index/000006.log rename to zingolib/src/testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/blocks/index/000006.log diff --git a/zingo-testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/blocks/index/CURRENT b/zingolib/src/testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/blocks/index/CURRENT similarity index 100% rename from zingo-testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/blocks/index/CURRENT rename to zingolib/src/testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/blocks/index/CURRENT diff --git a/zingo-testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/blocks/index/LOCK b/zingolib/src/testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/blocks/index/LOCK similarity index 100% rename from zingo-testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/blocks/index/LOCK rename to zingolib/src/testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/blocks/index/LOCK diff --git a/zingo-testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/blocks/index/MANIFEST-000004 b/zingolib/src/testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/blocks/index/MANIFEST-000004 similarity index 100% rename from zingo-testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/blocks/index/MANIFEST-000004 rename to zingolib/src/testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/blocks/index/MANIFEST-000004 diff --git a/zingo-testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/blocks/rev00000.dat b/zingolib/src/testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/blocks/rev00000.dat similarity index 100% rename from zingo-testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/blocks/rev00000.dat rename to zingolib/src/testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/blocks/rev00000.dat diff --git a/zingo-testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/chainstate/000005.ldb b/zingolib/src/testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/chainstate/000005.ldb similarity index 100% rename from zingo-testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/chainstate/000005.ldb rename to zingolib/src/testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/chainstate/000005.ldb diff --git a/zingo-testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/chainstate/000006.log b/zingolib/src/testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/chainstate/000006.log similarity index 100% rename from zingo-testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/chainstate/000006.log rename to zingolib/src/testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/chainstate/000006.log diff --git a/zingo-testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/chainstate/CURRENT b/zingolib/src/testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/chainstate/CURRENT similarity index 100% rename from zingo-testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/chainstate/CURRENT rename to zingolib/src/testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/chainstate/CURRENT diff --git a/zingo-testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/chainstate/LOCK b/zingolib/src/testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/chainstate/LOCK similarity index 100% rename from zingo-testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/chainstate/LOCK rename to zingolib/src/testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/chainstate/LOCK diff --git a/zingo-testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/chainstate/MANIFEST-000004 b/zingolib/src/testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/chainstate/MANIFEST-000004 similarity index 100% rename from zingo-testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/chainstate/MANIFEST-000004 rename to zingolib/src/testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/chainstate/MANIFEST-000004 diff --git a/zingo-testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/db.log b/zingolib/src/testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/db.log similarity index 100% rename from zingo-testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/db.log rename to zingolib/src/testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/db.log diff --git a/zingo-testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/fee_estimates.dat b/zingolib/src/testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/fee_estimates.dat similarity index 100% rename from zingo-testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/fee_estimates.dat rename to zingolib/src/testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/fee_estimates.dat diff --git a/zingo-testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/peers.dat b/zingolib/src/testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/peers.dat similarity index 100% rename from zingo-testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/peers.dat rename to zingolib/src/testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/peers.dat diff --git a/zingo-testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/wallet.dat b/zingolib/src/testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/wallet.dat similarity index 100% rename from zingo-testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/wallet.dat rename to zingolib/src/testutils/regtest/data/chain_cache/blocks_1153/zcashd/regtest/wallet.dat diff --git a/zingo-testutils/regtest/data/lightwalletd/.gitkeep b/zingolib/src/testutils/regtest/data/lightwalletd/.gitkeep similarity index 100% rename from zingo-testutils/regtest/data/lightwalletd/.gitkeep rename to zingolib/src/testutils/regtest/data/lightwalletd/.gitkeep diff --git a/zingo-testutils/regtest/data/regtestvectors/regtest/.lock b/zingolib/src/testutils/regtest/data/regtestvectors/regtest/.lock similarity index 100% rename from zingo-testutils/regtest/data/regtestvectors/regtest/.lock rename to zingolib/src/testutils/regtest/data/regtestvectors/regtest/.lock diff --git a/zingo-testutils/regtest/data/regtestvectors/regtest/banlist.dat b/zingolib/src/testutils/regtest/data/regtestvectors/regtest/banlist.dat similarity index 100% rename from zingo-testutils/regtest/data/regtestvectors/regtest/banlist.dat rename to zingolib/src/testutils/regtest/data/regtestvectors/regtest/banlist.dat diff --git a/zingo-testutils/regtest/data/regtestvectors/regtest/blocks/blk00000.dat b/zingolib/src/testutils/regtest/data/regtestvectors/regtest/blocks/blk00000.dat similarity index 100% rename from zingo-testutils/regtest/data/regtestvectors/regtest/blocks/blk00000.dat rename to zingolib/src/testutils/regtest/data/regtestvectors/regtest/blocks/blk00000.dat diff --git a/zingo-testutils/regtest/data/regtestvectors/regtest/blocks/index/000005.ldb b/zingolib/src/testutils/regtest/data/regtestvectors/regtest/blocks/index/000005.ldb similarity index 100% rename from zingo-testutils/regtest/data/regtestvectors/regtest/blocks/index/000005.ldb rename to zingolib/src/testutils/regtest/data/regtestvectors/regtest/blocks/index/000005.ldb diff --git a/zingo-testutils/regtest/data/regtestvectors/regtest/blocks/index/000008.ldb b/zingolib/src/testutils/regtest/data/regtestvectors/regtest/blocks/index/000008.ldb similarity index 100% rename from zingo-testutils/regtest/data/regtestvectors/regtest/blocks/index/000008.ldb rename to zingolib/src/testutils/regtest/data/regtestvectors/regtest/blocks/index/000008.ldb diff --git a/zingo-testutils/regtest/data/regtestvectors/regtest/blocks/index/000009.log b/zingolib/src/testutils/regtest/data/regtestvectors/regtest/blocks/index/000009.log similarity index 100% rename from zingo-testutils/regtest/data/regtestvectors/regtest/blocks/index/000009.log rename to zingolib/src/testutils/regtest/data/regtestvectors/regtest/blocks/index/000009.log diff --git a/zingo-testutils/regtest/data/regtestvectors/regtest/blocks/index/CURRENT b/zingolib/src/testutils/regtest/data/regtestvectors/regtest/blocks/index/CURRENT similarity index 100% rename from zingo-testutils/regtest/data/regtestvectors/regtest/blocks/index/CURRENT rename to zingolib/src/testutils/regtest/data/regtestvectors/regtest/blocks/index/CURRENT diff --git a/zingo-testutils/regtest/data/regtestvectors/regtest/blocks/index/LOCK b/zingolib/src/testutils/regtest/data/regtestvectors/regtest/blocks/index/LOCK similarity index 100% rename from zingo-testutils/regtest/data/regtestvectors/regtest/blocks/index/LOCK rename to zingolib/src/testutils/regtest/data/regtestvectors/regtest/blocks/index/LOCK diff --git a/zingo-testutils/regtest/data/regtestvectors/regtest/blocks/index/LOG b/zingolib/src/testutils/regtest/data/regtestvectors/regtest/blocks/index/LOG similarity index 100% rename from zingo-testutils/regtest/data/regtestvectors/regtest/blocks/index/LOG rename to zingolib/src/testutils/regtest/data/regtestvectors/regtest/blocks/index/LOG diff --git a/zingo-testutils/regtest/data/regtestvectors/regtest/blocks/index/LOG.old b/zingolib/src/testutils/regtest/data/regtestvectors/regtest/blocks/index/LOG.old similarity index 100% rename from zingo-testutils/regtest/data/regtestvectors/regtest/blocks/index/LOG.old rename to zingolib/src/testutils/regtest/data/regtestvectors/regtest/blocks/index/LOG.old diff --git a/zingo-testutils/regtest/data/regtestvectors/regtest/blocks/index/MANIFEST-000007 b/zingolib/src/testutils/regtest/data/regtestvectors/regtest/blocks/index/MANIFEST-000007 similarity index 100% rename from zingo-testutils/regtest/data/regtestvectors/regtest/blocks/index/MANIFEST-000007 rename to zingolib/src/testutils/regtest/data/regtestvectors/regtest/blocks/index/MANIFEST-000007 diff --git a/zingo-testutils/regtest/data/regtestvectors/regtest/blocks/rev00000.dat b/zingolib/src/testutils/regtest/data/regtestvectors/regtest/blocks/rev00000.dat similarity index 100% rename from zingo-testutils/regtest/data/regtestvectors/regtest/blocks/rev00000.dat rename to zingolib/src/testutils/regtest/data/regtestvectors/regtest/blocks/rev00000.dat diff --git a/zingo-testutils/regtest/data/regtestvectors/regtest/chainstate/000005.ldb b/zingolib/src/testutils/regtest/data/regtestvectors/regtest/chainstate/000005.ldb similarity index 100% rename from zingo-testutils/regtest/data/regtestvectors/regtest/chainstate/000005.ldb rename to zingolib/src/testutils/regtest/data/regtestvectors/regtest/chainstate/000005.ldb diff --git a/zingo-testutils/regtest/data/regtestvectors/regtest/chainstate/000008.ldb b/zingolib/src/testutils/regtest/data/regtestvectors/regtest/chainstate/000008.ldb similarity index 100% rename from zingo-testutils/regtest/data/regtestvectors/regtest/chainstate/000008.ldb rename to zingolib/src/testutils/regtest/data/regtestvectors/regtest/chainstate/000008.ldb diff --git a/zingo-testutils/regtest/data/regtestvectors/regtest/chainstate/000009.log b/zingolib/src/testutils/regtest/data/regtestvectors/regtest/chainstate/000009.log similarity index 100% rename from zingo-testutils/regtest/data/regtestvectors/regtest/chainstate/000009.log rename to zingolib/src/testutils/regtest/data/regtestvectors/regtest/chainstate/000009.log diff --git a/zingo-testutils/regtest/data/regtestvectors/regtest/chainstate/CURRENT b/zingolib/src/testutils/regtest/data/regtestvectors/regtest/chainstate/CURRENT similarity index 100% rename from zingo-testutils/regtest/data/regtestvectors/regtest/chainstate/CURRENT rename to zingolib/src/testutils/regtest/data/regtestvectors/regtest/chainstate/CURRENT diff --git a/zingo-testutils/regtest/data/regtestvectors/regtest/chainstate/LOCK b/zingolib/src/testutils/regtest/data/regtestvectors/regtest/chainstate/LOCK similarity index 100% rename from zingo-testutils/regtest/data/regtestvectors/regtest/chainstate/LOCK rename to zingolib/src/testutils/regtest/data/regtestvectors/regtest/chainstate/LOCK diff --git a/zingo-testutils/regtest/data/regtestvectors/regtest/chainstate/LOG b/zingolib/src/testutils/regtest/data/regtestvectors/regtest/chainstate/LOG similarity index 100% rename from zingo-testutils/regtest/data/regtestvectors/regtest/chainstate/LOG rename to zingolib/src/testutils/regtest/data/regtestvectors/regtest/chainstate/LOG diff --git a/zingo-testutils/regtest/data/regtestvectors/regtest/chainstate/LOG.old b/zingolib/src/testutils/regtest/data/regtestvectors/regtest/chainstate/LOG.old similarity index 100% rename from zingo-testutils/regtest/data/regtestvectors/regtest/chainstate/LOG.old rename to zingolib/src/testutils/regtest/data/regtestvectors/regtest/chainstate/LOG.old diff --git a/zingo-testutils/regtest/data/regtestvectors/regtest/chainstate/MANIFEST-000007 b/zingolib/src/testutils/regtest/data/regtestvectors/regtest/chainstate/MANIFEST-000007 similarity index 100% rename from zingo-testutils/regtest/data/regtestvectors/regtest/chainstate/MANIFEST-000007 rename to zingolib/src/testutils/regtest/data/regtestvectors/regtest/chainstate/MANIFEST-000007 diff --git a/zingo-testutils/regtest/data/regtestvectors/regtest/database/log.0000000001 b/zingolib/src/testutils/regtest/data/regtestvectors/regtest/database/log.0000000001 similarity index 100% rename from zingo-testutils/regtest/data/regtestvectors/regtest/database/log.0000000001 rename to zingolib/src/testutils/regtest/data/regtestvectors/regtest/database/log.0000000001 diff --git a/zingo-testutils/regtest/data/regtestvectors/regtest/db.log b/zingolib/src/testutils/regtest/data/regtestvectors/regtest/db.log similarity index 100% rename from zingo-testutils/regtest/data/regtestvectors/regtest/db.log rename to zingolib/src/testutils/regtest/data/regtestvectors/regtest/db.log diff --git a/zingo-testutils/regtest/data/regtestvectors/regtest/fee_estimates.dat b/zingolib/src/testutils/regtest/data/regtestvectors/regtest/fee_estimates.dat similarity index 100% rename from zingo-testutils/regtest/data/regtestvectors/regtest/fee_estimates.dat rename to zingolib/src/testutils/regtest/data/regtestvectors/regtest/fee_estimates.dat diff --git a/zingo-testutils/regtest/data/regtestvectors/regtest/peers.dat b/zingolib/src/testutils/regtest/data/regtestvectors/regtest/peers.dat similarity index 100% rename from zingo-testutils/regtest/data/regtestvectors/regtest/peers.dat rename to zingolib/src/testutils/regtest/data/regtestvectors/regtest/peers.dat diff --git a/zingo-testutils/regtest/data/regtestvectors/regtest/wallet.dat b/zingolib/src/testutils/regtest/data/regtestvectors/regtest/wallet.dat similarity index 100% rename from zingo-testutils/regtest/data/regtestvectors/regtest/wallet.dat rename to zingolib/src/testutils/regtest/data/regtestvectors/regtest/wallet.dat diff --git a/zingo-testutils/regtest/data/zcashd/.gitkeep b/zingolib/src/testutils/regtest/data/zcashd/.gitkeep similarity index 100% rename from zingo-testutils/regtest/data/zcashd/.gitkeep rename to zingolib/src/testutils/regtest/data/zcashd/.gitkeep diff --git a/zingo-testutils/regtest/logs/.gitignore b/zingolib/src/testutils/regtest/logs/.gitignore similarity index 100% rename from zingo-testutils/regtest/logs/.gitignore rename to zingolib/src/testutils/regtest/logs/.gitignore diff --git a/zingo-testutils/regtest/logs/.gitkeep b/zingolib/src/testutils/regtest/logs/.gitkeep similarity index 100% rename from zingo-testutils/regtest/logs/.gitkeep rename to zingolib/src/testutils/regtest/logs/.gitkeep diff --git a/zingo-testutils/regtest/logs/lightwalletd/.gitkeep b/zingolib/src/testutils/regtest/logs/lightwalletd/.gitkeep similarity index 100% rename from zingo-testutils/regtest/logs/lightwalletd/.gitkeep rename to zingolib/src/testutils/regtest/logs/lightwalletd/.gitkeep diff --git a/zingo-testutils/regtest/logs/zcashd/.gitkeep b/zingolib/src/testutils/regtest/logs/zcashd/.gitkeep similarity index 100% rename from zingo-testutils/regtest/logs/zcashd/.gitkeep rename to zingolib/src/testutils/regtest/logs/zcashd/.gitkeep diff --git a/zingo-testvectors/src/lib.rs b/zingolib/src/testvectors.rs similarity index 93% rename from zingo-testvectors/src/lib.rs rename to zingolib/src/testvectors.rs index c70874d21c..03b4ec5c67 100644 --- a/zingo-testvectors/src/lib.rs +++ b/zingolib/src/testvectors.rs @@ -1,17 +1,20 @@ -//! A publishable crate that provides access to the full dataset ZingoLabs uses to test -//! zingolib. It's available as a re-export from zingolib. +//! A publishable mod that provides access to the full dataset ZingoLabs uses to test +//! zingolib. It's available under the "testvectors" feature gate from zingolib. //! TODO: 1. specify feature gates for re-export access //! TODO: 2. after 1. is resolved add a doc-test + +/// The number of blocks most tests start with. pub const BASE_HEIGHT: u32 = 3; /// TODO: Add Mod Description Here! pub mod seeds { + /// TODO: Add Doc Comment Here! pub const DARKSIDE_SEED: &str = "still champion voice habit trend flight survey between bitter process artefact blind carbon truly provide dizzy crush flush breeze blouse charge solid fish spread"; #[test] fn validate_seeds() { - let abandon_art_seed = zcash_primitives::zip339::Mnemonic::from_entropy([0; 32]) + let abandon_art_seed = bip0039::Mnemonic::::from_entropy([0; 32]) .unwrap() .to_string(); assert_eq!(ABANDON_ART_SEED, abandon_art_seed); @@ -54,6 +57,7 @@ pub mod block_rewards { /// Burn-to regtest address generated by `zcash-cli getnewaddress` pub const EXT_TADDR: &str = "tmJTBtMwPU96XteSiP89xDz1WARNgRddEHq"; +/// A test txid pub const TEST_TXID: &str = "d5eaac5563f8bc1a0406588e05953977ad768d02f1cf8449e9d7d9cc8de3801c"; /// TODO: Add Doc Comment Here! @@ -65,7 +69,7 @@ pub mod config_template_fillers { /// TODO: Add Doc Comment Here! pub fn basic( rpcport: &str, - regtest_network: &zingoconfig::RegtestNetwork, + regtest_network: &crate::config::RegtestNetwork, extra: &str, ) -> String { let overwinter_activation_height = regtest_network @@ -125,7 +129,7 @@ listen=0 pub fn funded( mineraddress: &str, rpcport: &str, - regtest_network: &zingoconfig::RegtestNetwork, + regtest_network: &crate::config::RegtestNetwork, ) -> String { basic(rpcport, regtest_network, &format!("\ @@ -138,7 +142,7 @@ minetolocalwallet=0 # This is set to false so that we can mine to a wallet, othe #[test] fn funded_zcashd_conf() { - let regtest_network = zingoconfig::RegtestNetwork::new(1, 2, 3, 4, 5, 6); + let regtest_network = crate::config::RegtestNetwork::new(1, 2, 3, 4, 5, 6); assert_eq!( funded( super::super::REG_Z_ADDR_FROM_ABANDONART, diff --git a/zingo-testvectors/CHANGELOG.md b/zingolib/src/testvectors/CHANGELOG.md similarity index 100% rename from zingo-testvectors/CHANGELOG.md rename to zingolib/src/testvectors/CHANGELOG.md diff --git a/zingo-testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/.lock b/zingolib/src/testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/.lock similarity index 100% rename from zingo-testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/.lock rename to zingolib/src/testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/.lock diff --git a/zingo-testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/banlist.dat b/zingolib/src/testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/banlist.dat similarity index 100% rename from zingo-testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/banlist.dat rename to zingolib/src/testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/banlist.dat diff --git a/zingo-testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/blocks/blk00000.dat b/zingolib/src/testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/blocks/blk00000.dat similarity index 100% rename from zingo-testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/blocks/blk00000.dat rename to zingolib/src/testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/blocks/blk00000.dat diff --git a/zingo-testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/blocks/index/000005.ldb b/zingolib/src/testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/blocks/index/000005.ldb similarity index 100% rename from zingo-testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/blocks/index/000005.ldb rename to zingolib/src/testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/blocks/index/000005.ldb diff --git a/zingo-testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/blocks/index/000006.log b/zingolib/src/testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/blocks/index/000006.log similarity index 100% rename from zingo-testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/blocks/index/000006.log rename to zingolib/src/testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/blocks/index/000006.log diff --git a/zingo-testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/blocks/index/CURRENT b/zingolib/src/testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/blocks/index/CURRENT similarity index 100% rename from zingo-testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/blocks/index/CURRENT rename to zingolib/src/testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/blocks/index/CURRENT diff --git a/zingo-testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/blocks/index/LOCK b/zingolib/src/testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/blocks/index/LOCK similarity index 100% rename from zingo-testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/blocks/index/LOCK rename to zingolib/src/testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/blocks/index/LOCK diff --git a/zingo-testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/blocks/index/LOG b/zingolib/src/testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/blocks/index/LOG similarity index 100% rename from zingo-testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/blocks/index/LOG rename to zingolib/src/testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/blocks/index/LOG diff --git a/zingo-testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/blocks/index/LOG.old b/zingolib/src/testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/blocks/index/LOG.old similarity index 100% rename from zingo-testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/blocks/index/LOG.old rename to zingolib/src/testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/blocks/index/LOG.old diff --git a/zingo-testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/blocks/index/MANIFEST-000004 b/zingolib/src/testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/blocks/index/MANIFEST-000004 similarity index 100% rename from zingo-testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/blocks/index/MANIFEST-000004 rename to zingolib/src/testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/blocks/index/MANIFEST-000004 diff --git a/zingo-testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/blocks/rev00000.dat b/zingolib/src/testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/blocks/rev00000.dat similarity index 100% rename from zingo-testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/blocks/rev00000.dat rename to zingolib/src/testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/blocks/rev00000.dat diff --git a/zingo-testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/chainstate/000005.ldb b/zingolib/src/testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/chainstate/000005.ldb similarity index 100% rename from zingo-testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/chainstate/000005.ldb rename to zingolib/src/testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/chainstate/000005.ldb diff --git a/zingo-testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/chainstate/000006.log b/zingolib/src/testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/chainstate/000006.log similarity index 100% rename from zingo-testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/chainstate/000006.log rename to zingolib/src/testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/chainstate/000006.log diff --git a/zingo-testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/chainstate/CURRENT b/zingolib/src/testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/chainstate/CURRENT similarity index 100% rename from zingo-testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/chainstate/CURRENT rename to zingolib/src/testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/chainstate/CURRENT diff --git a/zingo-testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/chainstate/LOCK b/zingolib/src/testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/chainstate/LOCK similarity index 100% rename from zingo-testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/chainstate/LOCK rename to zingolib/src/testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/chainstate/LOCK diff --git a/zingo-testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/chainstate/LOG b/zingolib/src/testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/chainstate/LOG similarity index 100% rename from zingo-testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/chainstate/LOG rename to zingolib/src/testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/chainstate/LOG diff --git a/zingo-testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/chainstate/LOG.old b/zingolib/src/testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/chainstate/LOG.old similarity index 100% rename from zingo-testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/chainstate/LOG.old rename to zingolib/src/testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/chainstate/LOG.old diff --git a/zingo-testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/chainstate/MANIFEST-000004 b/zingolib/src/testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/chainstate/MANIFEST-000004 similarity index 100% rename from zingo-testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/chainstate/MANIFEST-000004 rename to zingolib/src/testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/chainstate/MANIFEST-000004 diff --git a/zingo-testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/db.log b/zingolib/src/testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/db.log similarity index 100% rename from zingo-testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/db.log rename to zingolib/src/testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/db.log diff --git a/zingo-testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/fee_estimates.dat b/zingolib/src/testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/fee_estimates.dat similarity index 100% rename from zingo-testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/fee_estimates.dat rename to zingolib/src/testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/fee_estimates.dat diff --git a/zingo-testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/peers.dat b/zingolib/src/testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/peers.dat similarity index 100% rename from zingo-testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/peers.dat rename to zingolib/src/testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/peers.dat diff --git a/zingo-testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/wallet.dat b/zingolib/src/testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/wallet.dat similarity index 100% rename from zingo-testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/wallet.dat rename to zingolib/src/testvectors/old_wallet_reorg_test_wallet/zcashd/regtest/wallet.dat diff --git a/zingolib/src/utils.rs b/zingolib/src/utils.rs index 63ed8d96bf..212b26ee64 100644 --- a/zingolib/src/utils.rs +++ b/zingolib/src/utils.rs @@ -3,6 +3,7 @@ pub mod conversion; pub mod error; +#[cfg(any(test, feature = "test-elevation"))] macro_rules! build_method { ($name:ident, $localtype:ty) => { #[doc = "Set the $name field of the builder."] @@ -12,7 +13,7 @@ macro_rules! build_method { } }; } -#[cfg(test)] // temporary test gate as no production builders use this macros yet +#[cfg(any(test, feature = "test-elevation"))] macro_rules! build_method_push { ($name:ident, $localtype:ty) => { #[doc = "Push a $ty to the builder."] @@ -22,7 +23,7 @@ macro_rules! build_method_push { } }; } -#[cfg(test)] // temporary test gate as no production builders use this macros yet +#[cfg(any(test, feature = "test-elevation"))] macro_rules! build_push_list { ($name:ident, $builder:ident, $struct:ident) => { for i in &$builder.$name { @@ -31,8 +32,56 @@ macro_rules! build_push_list { }; } +#[cfg(any(test, feature = "test-elevation"))] pub(crate) use build_method; -#[cfg(test)] +#[cfg(any(test, feature = "test-elevation"))] pub(crate) use build_method_push; -#[cfg(test)] +#[cfg(any(test, feature = "test-elevation"))] pub(crate) use build_push_list; +use zcash_primitives::consensus::NetworkConstants; + +/// this mod exists to allow the use statement without cluttering the parent mod +pub mod txid { + use log::error; + use zcash_primitives::transaction::TxId; + + /// used when the server reports a string txid + pub fn compare_txid_to_string( + txid: TxId, + reported_txid_string: String, + prefer_reported: bool, + ) -> TxId { + match crate::utils::conversion::txid_from_hex_encoded_str(reported_txid_string.as_str()) { + Ok(reported_txid) => { + if txid != reported_txid { + // happens during darkside tests + error!( + "served txid {} does not match calulated txid {}", + reported_txid, txid, + ); + }; + if prefer_reported { + reported_txid + } else { + txid + } + } + Err(e) => { + error!("server returned invalid txid {}", e); + txid + } + } + } +} + +/// Take a P2PKH taddr and interpret it as a tex addr +pub fn interpret_taddr_as_tex_addr( + taddr_bytes: [u8; 20], + p: &impl zcash_primitives::consensus::Parameters, +) -> String { + bech32::encode::( + bech32::Hrp::parse_unchecked(p.network_type().hrp_tex_address()), + &taddr_bytes, + ) + .unwrap() +} diff --git a/zingolib/src/utils/conversion.rs b/zingolib/src/utils/conversion.rs index fda1ae752b..42882f68d5 100644 --- a/zingolib/src/utils/conversion.rs +++ b/zingolib/src/utils/conversion.rs @@ -2,11 +2,9 @@ use thiserror::Error; -use zcash_client_backend::address::Address; +use zcash_address::ZcashAddress; use zcash_primitives::transaction::{components::amount::NonNegativeAmount, TxId}; -use zingoconfig::ChainType; - use super::error::ConversionError; #[allow(missing_docs)] // error types document themselves @@ -29,10 +27,9 @@ pub fn txid_from_hex_encoded_str(txid: &str) -> Result Result { - Address::decode(chain, address) - .ok_or_else(|| ConversionError::InvalidAddress(address.to_string())) +/// Convert a &str to a ZcashAddress +pub fn address_from_str(address: &str) -> Result { + Ok(ZcashAddress::try_from_encoded(address)?) } /// Convert a valid u64 into Zatoshis. diff --git a/zingolib/src/utils/error.rs b/zingolib/src/utils/error.rs index becd2a7d6f..873e7b66fe 100644 --- a/zingolib/src/utils/error.rs +++ b/zingolib/src/utils/error.rs @@ -3,27 +3,25 @@ use std::fmt; /// The error type for conversion errors. -#[derive(Debug, Clone, PartialEq)] +#[derive(thiserror::Error, Debug, Clone, PartialEq)] pub enum ConversionError { /// Failed to decode hex DecodeHexFailed(hex::FromHexError), /// Invalid string length InvalidStringLength, /// Invalid recipient address - InvalidAddress(String), + InvalidAddress(#[from] zcash_address::ParseError), /// Amount is outside the valid range of zatoshis OutsideValidRange, } -impl std::error::Error for ConversionError {} - impl fmt::Display for ConversionError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { ConversionError::DecodeHexFailed(e) => write!(f, "failed to decode hex. {}", e), ConversionError::InvalidStringLength => write!(f, "invalid string length"), - ConversionError::InvalidAddress(address) => { - write!(f, "invalid recipient address. {}", address) + ConversionError::InvalidAddress(e) => { + write!(f, "invalid recipient address. {}", e) } ConversionError::OutsideValidRange => { write!(f, "amount is outside the valid range of zatoshis") diff --git a/zingolib/src/wallet.rs b/zingolib/src/wallet.rs index cb7caaa82a..76133036cb 100644 --- a/zingolib/src/wallet.rs +++ b/zingolib/src/wallet.rs @@ -3,14 +3,26 @@ //! TODO: Add Mod Description Here use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; +use error::KeyError; +use getset::{Getters, MutGetters}; +use zcash_keys::keys::UnifiedFullViewingKey; +#[cfg(feature = "sync")] +use zcash_primitives::consensus::BlockHeight; use zcash_primitives::memo::Memo; use log::{info, warn}; use rand::rngs::OsRng; use rand::Rng; -use sapling_crypto::zip32::DiversifiableFullViewingKey; +#[cfg(feature = "sync")] +use zingo_sync::{ + primitives::{NullifierMap, SyncState, WalletBlock}, + witness::ShardTrees, +}; +use bip0039::Mnemonic; +#[cfg(feature = "sync")] +use std::collections::BTreeMap; use std::{ cmp, io::{self, Error, ErrorKind, Read, Write}, @@ -18,20 +30,18 @@ use std::{ time::SystemTime, }; use tokio::sync::RwLock; -use zcash_primitives::zip339::Mnemonic; +use crate::config::ZingoConfig; use zcash_client_backend::proto::service::TreeState; use zcash_encoding::Optional; -use zingoconfig::ZingoConfig; -use self::keys::unified::Fvk as _; use self::keys::unified::WalletCapability; use self::{ data::{BlockData, WalletZecPriceInfo}, message::Message, transaction_context::TransactionContext, - tx_map_and_maybe_trees::TxMapAndMaybeTrees, + tx_map::TxMap, }; pub mod data; @@ -43,15 +53,19 @@ pub mod traits; pub mod transaction_context; pub mod transaction_record; pub mod transaction_records_by_id; -pub mod tx_map_and_maybe_trees; +pub mod tx_map; pub mod utils; //these mods contain pieces of the impl LightWallet pub mod describe; pub mod disk; +pub mod propose; pub mod send; pub mod witnesses; +#[cfg(feature = "sync")] +pub mod sync; + pub(crate) use send::SendProgress; /// TODO: Add Doc Comment Here! @@ -172,7 +186,8 @@ impl WalletBase { } } -/// TODO: Add Doc Comment Here! +/// In-memory wallet data struct +#[derive(Getters, MutGetters)] pub struct LightWallet { // The block at which this wallet was born. Rescans // will start from here. @@ -184,7 +199,7 @@ pub struct LightWallet { mnemonic: Option<(Mnemonic, u32)>, /// The last 100 blocks, used if something gets re-orged - pub blocks: Arc>>, + pub last_100_blocks: Arc>>, /// Wallet options pub wallet_options: Arc>, @@ -201,6 +216,26 @@ pub struct LightWallet { /// Local state needed to submit (compact)block-requests to the proxy /// and interpret responses pub transaction_context: TransactionContext, + + /// Wallet compact blocks + #[cfg(feature = "sync")] + #[getset(get = "pub", get_mut = "pub")] + wallet_blocks: BTreeMap, + + /// Nullifier map + #[cfg(feature = "sync")] + #[getset(get = "pub", get_mut = "pub")] + nullifier_map: NullifierMap, + + /// Shard trees + #[cfg(feature = "sync")] + #[getset(get = "pub", get_mut = "pub")] + shard_trees: ShardTrees, + + /// Sync state + #[cfg(feature = "sync")] + #[getset(get = "pub", get_mut = "pub")] + sync_state: SyncState, } impl LightWallet { @@ -222,7 +257,7 @@ impl LightWallet { /// After this, the wallet's initial state will need to be set /// and the wallet will need to be rescanned pub async fn clear_all(&self) { - self.blocks.write().await.clear(); + self.last_100_blocks.write().await.clear(); self.transaction_context .transaction_metadata_set .write() @@ -232,10 +267,18 @@ impl LightWallet { ///TODO: Make this work for orchard too pub async fn decrypt_message(&self, enc: Vec) -> Result { - let sapling_ivk = DiversifiableFullViewingKey::try_from(&*self.wallet_capability())? - .derive_ivk::(); + let ufvk: UnifiedFullViewingKey = + match self.wallet_capability().unified_key_store().try_into() { + Ok(ufvk) => ufvk, + Err(e) => return Err(e.to_string()), + }; + let sapling_ivk = if let Some(ivk) = ufvk.sapling() { + ivk.to_external_ivk().prepare() + } else { + return Err(KeyError::NoViewCapability.to_string()); + }; - if let Ok(msg) = Message::decrypt(&enc, &sapling_ivk.ivk) { + if let Ok(msg) = Message::decrypt(&enc, &sapling_ivk) { // If decryption succeeded for this IVK, return the decrypted memo and the matched address return Ok(msg); } @@ -286,7 +329,7 @@ impl LightWallet { ); } WalletBase::MnemonicPhraseAndIndex(phrase, position) => { - let mnemonic = Mnemonic::from_phrase(phrase) + let mnemonic = Mnemonic::::from_phrase(phrase) .and_then(|m| Mnemonic::from_entropy(m.entropy())) .map_err(|e| { Error::new( @@ -331,25 +374,32 @@ impl LightWallet { } }; - if let Err(e) = wc.new_address(wc.can_view()) { + if let Err(e) = wc.new_address(wc.can_view(), false) { return Err(io::Error::new( io::ErrorKind::InvalidData, format!("could not create initial address: {e}"), )); }; - let transaction_metadata_set = if wc.can_spend_from_all_pools() { - Arc::new(RwLock::new(TxMapAndMaybeTrees::new_with_witness_trees( + let transaction_metadata_set = if wc.unified_key_store().is_spending_key() { + Arc::new(RwLock::new(TxMap::new_with_witness_trees( wc.transparent_child_addresses().clone(), + wc.transparent_child_ephemeral_addresses().clone(), + wc.ephemeral_ivk().map_err(|e| { + Error::new( + ErrorKind::InvalidData, + format!("Error with transparent key: {e}"), + ) + })?, ))) } else { - Arc::new(RwLock::new(TxMapAndMaybeTrees::new_treeless( + Arc::new(RwLock::new(TxMap::new_treeless( wc.transparent_child_addresses().clone(), ))) }; let transaction_context = TransactionContext::new(&config, Arc::new(wc), transaction_metadata_set); Ok(Self { - blocks: Arc::new(RwLock::new(vec![])), + last_100_blocks: Arc::new(RwLock::new(vec![])), mnemonic, wallet_options: Arc::new(RwLock::new(WalletOptions::default())), birthday: AtomicU64::new(height), @@ -357,12 +407,20 @@ impl LightWallet { send_progress: Arc::new(RwLock::new(SendProgress::new(0))), price: Arc::new(RwLock::new(WalletZecPriceInfo::default())), transaction_context, + #[cfg(feature = "sync")] + wallet_blocks: BTreeMap::new(), + #[cfg(feature = "sync")] + nullifier_map: zingo_sync::primitives::NullifierMap::new(), + #[cfg(feature = "sync")] + shard_trees: zingo_sync::witness::ShardTrees::new(), + #[cfg(feature = "sync")] + sync_state: zingo_sync::primitives::SyncState::new(), }) } /// TODO: Add Doc Comment Here! pub async fn set_blocks(&self, new_blocks: Vec) { - let mut blocks = self.blocks.write().await; + let mut blocks = self.last_100_blocks.write().await; blocks.clear(); blocks.extend_from_slice(&new_blocks[..]); } @@ -374,7 +432,7 @@ impl LightWallet { /// TODO: Add Doc Comment Here! pub async fn set_initial_block(&self, height: u64, hash: &str, _sapling_tree: &str) -> bool { - let mut blocks = self.blocks.write().await; + let mut blocks = self.last_100_blocks.write().await; if !blocks.is_empty() { return false; } diff --git a/zingolib/src/wallet/data.rs b/zingolib/src/wallet/data.rs index b478c949b6..41af97d7a3 100644 --- a/zingolib/src/wallet/data.rs +++ b/zingolib/src/wallet/data.rs @@ -11,12 +11,14 @@ use prost::Message; use std::convert::TryFrom; use std::io::{self, Read, Write}; -use zcash_client_backend::proto::compact_formats::CompactBlock; +use zcash_client_backend::{ + proto::compact_formats::CompactBlock, wallet::TransparentAddressMetadata, +}; use zcash_encoding::{Optional, Vector}; -use zcash_primitives::memo::MemoBytes; use zcash_primitives::merkle_tree::{read_commitment_tree, write_commitment_tree}; +use zcash_primitives::{legacy::TransparentAddress, memo::MemoBytes}; use zcash_primitives::{memo::Memo, transaction::TxId}; pub use crate::wallet::transaction_record::TransactionRecord; // TODO: is this necessary? can we import this directly where its used? @@ -389,7 +391,8 @@ impl OutgoingTxData { } } -struct OutgoingTxDataSummaries(Vec); +/// Wraps a vec of outgoing transaction datas for the implementation of std::fmt::Display +pub struct OutgoingTxDataSummaries(Vec); impl std::fmt::Display for OutgoingTxDataSummaries { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -446,11 +449,14 @@ pub mod finsight { } } -/// TODO: Add Mod Description Here! +/// A mod designed for conveniently displaying information to the user or converting to JSON to pass through an FFI. +/// A "snapshot" of the state of the items in the wallet at the time the summary was constructed. +/// Not to be used for internal logic in the system. pub mod summaries { + use crate::config::ChainType; use chrono::DateTime; use json::JsonValue; - use zcash_primitives::{consensus::BlockHeight, transaction::TxId}; + use zcash_primitives::{consensus::BlockHeight, memo::Memo, transaction::TxId}; use zingo_status::confirmation_status::ConfirmationStatus; use crate::{ @@ -458,11 +464,13 @@ pub mod summaries { utils::build_method, wallet::{ data::OutgoingTxDataSummaries, + notes::OutputInterface as _, transaction_record::{SendType, TransactionKind}, + transaction_records_by_id::TransactionRecordsById, }, }; - use super::OutgoingTxData; + use super::{OutgoingTxData, TransactionRecord}; /// A value transfer is a note group abstraction. /// A group of all notes sent to a specific address in a transaction. @@ -482,47 +490,47 @@ pub mod summaries { } impl ValueTransfer { - /// TODO: doc comment + /// Gets txid pub fn txid(&self) -> TxId { self.txid } - /// TODO: doc comment + /// Gets datetime pub fn datetime(&self) -> u64 { self.datetime } - /// TODO: doc comment + /// Gets confirmation status pub fn status(&self) -> ConfirmationStatus { self.status } - /// TODO: doc comment + /// Gets blockheight pub fn blockheight(&self) -> BlockHeight { self.blockheight } - /// TODO: doc comment + /// Gets transaction fee pub fn transaction_fee(&self) -> Option { self.transaction_fee } - /// TODO: doc comment + /// Gets zec price in USD pub fn zec_price(&self) -> Option { self.zec_price } - /// TODO: doc comment + /// Gets value transfer kind pub fn kind(&self) -> ValueTransferKind { self.kind } - /// TODO: doc comment + /// Gets value pub fn value(&self) -> u64 { self.value } - /// TODO: doc comment + /// Gets recipient address pub fn recipient_address(&self) -> Option<&str> { self.recipient_address.as_deref() } - /// TODO: doc comment + /// Gets pool received pub fn pool_received(&self) -> Option<&str> { self.pool_received.as_deref() } - /// TODO: doc comment + /// Gets memos pub fn memos(&self) -> Vec<&str> { self.memos.iter().map(|s| s.as_str()).collect() } @@ -630,7 +638,7 @@ pub mod summaries { pub struct ValueTransfers(pub Vec); impl ValueTransfers { - /// TODO: doc comment + /// Creates a new ValueTransfer pub fn new(value_transfers: Vec) -> Self { ValueTransfers(value_transfers) } @@ -659,7 +667,7 @@ pub mod summaries { } } - /// TODO: doc comment + /// Builds ValueTransfer from builder pub struct ValueTransferBuilder { txid: Option, datetime: Option, @@ -675,7 +683,7 @@ pub mod summaries { } impl ValueTransferBuilder { - /// TODO: doc comment + /// Creates a new ValueTransfer builder pub fn new() -> ValueTransferBuilder { ValueTransferBuilder { txid: None, @@ -704,7 +712,7 @@ pub mod summaries { build_method!(pool_received, Option); build_method!(memos, Vec); - /// TODO: doc comment + /// Builds a ValueTransfer from builder pub fn build(&self) -> Result { Ok(ValueTransfer { txid: self @@ -756,35 +764,143 @@ pub mod summaries { /// Variants of within transaction outputs grouped by receiver /// non_exhaustive to permit expanding to include an /// Deshield variant fo sending to transparent - #[non_exhaustive] #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum ValueTransferKind { /// The recipient is different than this creator - Sent, + Sent(SentValueTransfer), + /// The wallet capability is receiving funds in a transaction + /// that was created by a different capability + Received, + } + /// There are 2 kinds of sent value to-other and to-self + #[derive(Clone, Copy, PartialEq, Eq, Debug)] + pub enum SentValueTransfer { + /// Transaction is sending funds to recipient other than the creator + Send, + /// The recipient is the creator and the transaction has no recipients that are not the creator + SendToSelf(SelfSendValueTransfer), + } + /// There are 4 kinds of self sends (so far) + #[non_exhaustive] + #[derive(Clone, Copy, PartialEq, Eq, Debug)] + pub enum SelfSendValueTransfer { + /// Explicit memo-less value sent to self + Basic, /// The recipient is the creator and this is a shield transaction Shield, - /// The recipient is the creator and the transaction has no recipients that are not the creator - SendToSelf, /// The recipient is the creator and is receiving at least 1 note with a TEXT memo MemoToSelf, - /// The wallet capability is receiving funds in a transaction - /// that was created by a different capability - Received, + /// The recipient is an ephemeral 320 address + Ephemeral320, } impl std::fmt::Display for ValueTransferKind { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { - ValueTransferKind::Sent => write!(f, "sent"), - ValueTransferKind::Shield => write!(f, "shield"), - ValueTransferKind::SendToSelf => write!(f, "send-to-self"), - ValueTransferKind::MemoToSelf => write!(f, "memo-to-self"), ValueTransferKind::Received => write!(f, "received"), + ValueTransferKind::Sent(sent) => match sent { + SentValueTransfer::Send => write!(f, "sent"), + SentValueTransfer::SendToSelf(selfsend) => match selfsend { + SelfSendValueTransfer::Basic => write!(f, "basic"), + SelfSendValueTransfer::Shield => write!(f, "shield"), + SelfSendValueTransfer::MemoToSelf => write!(f, "memo-to-self"), + SelfSendValueTransfer::Ephemeral320 => write!(f, "ephemeral-320-tex"), + }, + }, } } } - /// TODO: doc comment + /// Basic transaction summary interface + pub trait TransactionSummaryInterface { + /// Gets txid + fn txid(&self) -> TxId; + /// Gets datetime + fn datetime(&self) -> u64; + /// Gets confirmation status + fn status(&self) -> ConfirmationStatus; + /// Gets blockheight + fn blockheight(&self) -> BlockHeight; + /// Gets transaction kind + fn kind(&self) -> TransactionKind; + /// Gets value + fn value(&self) -> u64; + /// Gets fee + fn fee(&self) -> Option; + /// Gets zec price in USD + fn zec_price(&self) -> Option; + /// Gets slice of orchard note summaries + fn orchard_notes(&self) -> &[OrchardNoteSummary]; + /// Gets slice of sapling note summaries + fn sapling_notes(&self) -> &[SaplingNoteSummary]; + /// Gets slice of transparent coin summaries + fn transparent_coins(&self) -> &[TransparentCoinSummary]; + /// Gets slice of outgoing transaction data + fn outgoing_tx_data(&self) -> &[OutgoingTxData]; + /// Depending on the relationship of this capability to the + /// receiver capability, assign polarity to value transferred. + /// Returns None if fields expecting Some(_) are None + fn balance_delta(&self) -> Option { + match self.kind() { + TransactionKind::Sent(SendType::Send) => { + self.fee().map(|fee| -((self.value() + fee) as i64)) + } + TransactionKind::Sent(SendType::Shield) + | TransactionKind::Sent(SendType::SendToSelf) => { + self.fee().map(|fee| -(fee as i64)) + } + TransactionKind::Received => Some(self.value() as i64), + } + } + /// Prepares the fields in the summary for display + fn prepare_for_display( + &self, + ) -> ( + String, + String, + String, + OrchardNoteSummaries, + SaplingNoteSummaries, + TransparentCoinSummaries, + OutgoingTxDataSummaries, + ) { + let datetime = if let Some(dt) = DateTime::from_timestamp(self.datetime() as i64, 0) { + format!("{}", dt) + } else { + "not available".to_string() + }; + let fee = if let Some(f) = self.fee() { + f.to_string() + } else { + "not available".to_string() + }; + let zec_price = if let Some(price) = self.zec_price() { + price.to_string() + } else { + "not available".to_string() + }; + let orchard_notes = OrchardNoteSummaries(self.orchard_notes().to_vec()); + let sapling_notes = SaplingNoteSummaries(self.sapling_notes().to_vec()); + let transparent_coins = TransparentCoinSummaries(self.transparent_coins().to_vec()); + let outgoing_tx_data_summaries = + OutgoingTxDataSummaries(self.outgoing_tx_data().to_vec()); + + ( + datetime, + fee, + zec_price, + orchard_notes, + sapling_notes, + transparent_coins, + outgoing_tx_data_summaries, + ) + } + } + + /// Transaction summary. + /// A struct designed for conveniently displaying information to the user or converting to JSON to pass through an FFI. + /// A "snapshot" of the state of a transaction in the wallet at the time the summary was constructed. + /// Not to be used for internal logic in the system. #[derive(Clone, PartialEq, Debug)] pub struct TransactionSummary { txid: TxId, @@ -801,94 +917,56 @@ pub mod summaries { outgoing_tx_data: Vec, } - impl TransactionSummary { - /// TODO: doc comment - pub fn txid(&self) -> TxId { + impl TransactionSummaryInterface for TransactionSummary { + fn txid(&self) -> TxId { self.txid } - /// TODO: doc comment - pub fn datetime(&self) -> u64 { + fn datetime(&self) -> u64 { self.datetime } - /// TODO: doc comment - pub fn status(&self) -> ConfirmationStatus { + fn status(&self) -> ConfirmationStatus { self.status } - /// TODO: doc comment - pub fn blockheight(&self) -> BlockHeight { + fn blockheight(&self) -> BlockHeight { self.blockheight } - /// TODO: doc comment - pub fn kind(&self) -> TransactionKind { + fn kind(&self) -> TransactionKind { self.kind } - /// TODO: doc comment - pub fn value(&self) -> u64 { + fn value(&self) -> u64 { self.value } - /// TODO: doc comment - pub fn fee(&self) -> Option { + fn fee(&self) -> Option { self.fee } - /// TODO: doc comment - pub fn zec_price(&self) -> Option { + fn zec_price(&self) -> Option { self.zec_price } - /// TODO: doc comment - pub fn orchard_notes(&self) -> &[OrchardNoteSummary] { + fn orchard_notes(&self) -> &[OrchardNoteSummary] { &self.orchard_notes } - /// TODO: doc comment - pub fn sapling_notes(&self) -> &[SaplingNoteSummary] { + fn sapling_notes(&self) -> &[SaplingNoteSummary] { &self.sapling_notes } - /// TODO: doc comment - pub fn transparent_coins(&self) -> &[TransparentCoinSummary] { + fn transparent_coins(&self) -> &[TransparentCoinSummary] { &self.transparent_coins } - /// TODO: doc comment - pub fn outgoing_tx_data(&self) -> &[OutgoingTxData] { + fn outgoing_tx_data(&self) -> &[OutgoingTxData] { &self.outgoing_tx_data } - - /// Depending on the relationship of this capability to the - /// receiver capability, assign polarity to value transferred. - /// Returns None if fields expecting Som(_) are None - pub fn balance_delta(&self) -> Option { - match self.kind { - TransactionKind::Sent(SendType::Send) => { - self.fee().map(|fee| -((self.value() + fee) as i64)) - } - TransactionKind::Sent(SendType::Shield) - | TransactionKind::Sent(SendType::SendToSelf) => { - self.fee().map(|fee| -(fee as i64)) - } - TransactionKind::Received => Some(self.value() as i64), - } - } } impl std::fmt::Display for TransactionSummary { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let datetime = if let Some(dt) = DateTime::from_timestamp(self.datetime as i64, 0) { - format!("{}", dt) - } else { - "not available".to_string() - }; - let fee = if let Some(f) = self.fee { - f.to_string() - } else { - "not available".to_string() - }; - let zec_price = if let Some(price) = self.zec_price { - price.to_string() - } else { - "not available".to_string() - }; - let orchard_notes = OrchardNoteSummaries(self.orchard_notes.clone()); - let sapling_notes = SaplingNoteSummaries(self.sapling_notes.clone()); - let transparent_coins = TransparentCoinSummaries(self.transparent_coins.clone()); - let outgoing_tx_data_summaries = OutgoingTxDataSummaries(self.outgoing_tx_data.clone()); + let ( + datetime, + fee, + zec_price, + orchard_notes, + sapling_notes, + transparent_coins, + outgoing_tx_data_summaries, + ) = self.prepare_for_display(); write!( f, "{{ @@ -940,12 +1018,12 @@ pub mod summaries { } } - /// Summary of transactions + /// Wraps a vec of transaction summaries for the implementation of std::fmt::Display #[derive(PartialEq, Debug)] pub struct TransactionSummaries(pub Vec); impl TransactionSummaries { - /// TODO: doc comment + /// Creates a new TransactionSummaries struct pub fn new(transaction_summaries: Vec) -> Self { TransactionSummaries(transaction_summaries) } @@ -953,7 +1031,7 @@ pub mod summaries { pub fn iter(&self) -> std::slice::Iter { self.0.iter() } - /// Total fees captures by these summaries + /// Total fees captured by these summaries pub fn paid_fees(&self) -> u64 { self.iter().filter_map(|summary| summary.fee()).sum() } @@ -985,7 +1063,7 @@ pub mod summaries { } } - /// TODO: doc comment + /// Builds TransactionSummary from builder pub struct TransactionSummaryBuilder { txid: Option, datetime: Option, @@ -1002,7 +1080,7 @@ pub mod summaries { } impl TransactionSummaryBuilder { - /// TODO: doc comment + /// Creates a new TransactionSummary builder pub fn new() -> TransactionSummaryBuilder { TransactionSummaryBuilder { txid: None, @@ -1033,7 +1111,7 @@ pub mod summaries { build_method!(transparent_coins, Vec); build_method!(outgoing_tx_data, Vec); - /// TODO: doc comment + /// Builds a TransactionSummary from builder pub fn build(&self) -> Result { Ok(TransactionSummary { txid: self @@ -1086,43 +1164,352 @@ pub mod summaries { } } - /// TODO: doc comment + /// Detailed transaction summary. + /// A struct designed for conveniently displaying information to the user or converting to JSON to pass through an FFI. + /// A "snapshot" of the state of a transaction in the wallet at the time the summary was constructed. + /// Not to be used for internal logic in the system. + #[derive(Clone, PartialEq, Debug)] + pub struct DetailedTransactionSummary { + txid: TxId, + datetime: u64, + status: ConfirmationStatus, + blockheight: BlockHeight, + kind: TransactionKind, + value: u64, + fee: Option, + zec_price: Option, + orchard_nullifiers: Vec, + sapling_nullifiers: Vec, + orchard_notes: Vec, + sapling_notes: Vec, + transparent_coins: Vec, + outgoing_tx_data: Vec, + } + + impl DetailedTransactionSummary { + /// Gets orchard nullifiers + pub fn orchard_nullifiers(&self) -> Vec<&str> { + self.orchard_nullifiers.iter().map(|n| n.as_str()).collect() + } + /// Gets sapling nullifiers + pub fn sapling_nullifiers(&self) -> Vec<&str> { + self.sapling_nullifiers.iter().map(|n| n.as_str()).collect() + } + } + + impl TransactionSummaryInterface for DetailedTransactionSummary { + fn txid(&self) -> TxId { + self.txid + } + fn datetime(&self) -> u64 { + self.datetime + } + fn status(&self) -> ConfirmationStatus { + self.status + } + fn blockheight(&self) -> BlockHeight { + self.blockheight + } + fn kind(&self) -> TransactionKind { + self.kind + } + fn value(&self) -> u64 { + self.value + } + fn fee(&self) -> Option { + self.fee + } + fn zec_price(&self) -> Option { + self.zec_price + } + fn orchard_notes(&self) -> &[OrchardNoteSummary] { + &self.orchard_notes + } + fn sapling_notes(&self) -> &[SaplingNoteSummary] { + &self.sapling_notes + } + fn transparent_coins(&self) -> &[TransparentCoinSummary] { + &self.transparent_coins + } + fn outgoing_tx_data(&self) -> &[OutgoingTxData] { + &self.outgoing_tx_data + } + } + + impl std::fmt::Display for DetailedTransactionSummary { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let ( + datetime, + fee, + zec_price, + orchard_notes, + sapling_notes, + transparent_coins, + outgoing_tx_data_summaries, + ) = self.prepare_for_display(); + let orchard_nullifier_summaries = + OrchardNullifierSummaries(self.orchard_nullifiers.clone()); + let sapling_nullifier_summaries = + SaplingNullifierSummaries(self.sapling_nullifiers.clone()); + write!( + f, + "{{ + txid: {} + datetime: {} + status: {} + blockheight: {} + kind: {} + value: {} + fee: {} + zec price: {} + orchard_nullifiers: {} + sapling_nullifiers: {} + orchard notes: {} + sapling notes: {} + transparent coins: {} + outgoing data: {} +}}", + self.txid, + datetime, + self.status, + u64::from(self.blockheight), + self.kind, + self.value, + fee, + zec_price, + orchard_notes, + sapling_notes, + transparent_coins, + outgoing_tx_data_summaries, + orchard_nullifier_summaries, + sapling_nullifier_summaries, + ) + } + } + + impl From for JsonValue { + fn from(transaction: DetailedTransactionSummary) -> Self { + json::object! { + "txid" => transaction.txid.to_string(), + "datetime" => transaction.datetime, + "status" => transaction.status.to_string(), + "blockheight" => u64::from(transaction.blockheight), + "kind" => transaction.kind.to_string(), + "value" => transaction.value, + "fee" => transaction.fee, + "zec_price" => transaction.zec_price, + "orchard_nullifiers" => JsonValue::from(transaction.orchard_nullifiers), + "sapling_nullifiers" => JsonValue::from(transaction.sapling_nullifiers), + "orchard_notes" => JsonValue::from(transaction.orchard_notes), + "sapling_notes" => JsonValue::from(transaction.sapling_notes), + "transparent_coins" => JsonValue::from(transaction.transparent_coins), + "outgoing_tx_data" => JsonValue::from(transaction.outgoing_tx_data), + } + } + } + + /// Wraps a vec of detailed transaction summaries for the implementation of std::fmt::Display + #[derive(PartialEq, Debug)] + pub struct DetailedTransactionSummaries(pub Vec); + + impl DetailedTransactionSummaries { + /// Creates a new Detailedtransactionsummaries struct + pub fn new(transaction_summaries: Vec) -> Self { + DetailedTransactionSummaries(transaction_summaries) + } + /// Implicitly dispatch to the wrapped data + pub fn iter(&self) -> std::slice::Iter { + self.0.iter() + } + /// Total fees captured by these summaries + pub fn paid_fees(&self) -> u64 { + self.iter().filter_map(|summary| summary.fee()).sum() + } + /// A Vec of the txids + pub fn txids(&self) -> Vec { + self.iter().map(|summary| summary.txid()).collect() + } + } + + impl std::fmt::Display for DetailedTransactionSummaries { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for transaction_summary in &self.0 { + write!(f, "\n{}", transaction_summary)?; + } + Ok(()) + } + } + + impl From for JsonValue { + fn from(transaction_summaries: DetailedTransactionSummaries) -> Self { + let transaction_summaries: Vec = transaction_summaries + .0 + .into_iter() + .map(JsonValue::from) + .collect(); + json::object! { + "detailed_transaction_summaries" => transaction_summaries + } + } + } + + /// Builder for DetailedTransactionSummary + pub struct DetailedTransactionSummaryBuilder { + txid: Option, + datetime: Option, + status: Option, + blockheight: Option, + kind: Option, + value: Option, + fee: Option>, + zec_price: Option>, + orchard_notes: Option>, + sapling_notes: Option>, + transparent_coins: Option>, + outgoing_tx_data: Option>, + orchard_nullifiers: Option>, + sapling_nullifiers: Option>, + } + + impl DetailedTransactionSummaryBuilder { + /// Creates a new DetailedTransactionSummary builder + pub fn new() -> DetailedTransactionSummaryBuilder { + DetailedTransactionSummaryBuilder { + txid: None, + datetime: None, + status: None, + blockheight: None, + kind: None, + value: None, + fee: None, + zec_price: None, + orchard_notes: None, + sapling_notes: None, + transparent_coins: None, + outgoing_tx_data: None, + orchard_nullifiers: None, + sapling_nullifiers: None, + } + } + + build_method!(txid, TxId); + build_method!(datetime, u64); + build_method!(status, ConfirmationStatus); + build_method!(blockheight, BlockHeight); + build_method!(kind, TransactionKind); + build_method!(value, u64); + build_method!(fee, Option); + build_method!(zec_price, Option); + build_method!(orchard_notes, Vec); + build_method!(sapling_notes, Vec); + build_method!(transparent_coins, Vec); + build_method!(outgoing_tx_data, Vec); + build_method!(orchard_nullifiers, Vec); + build_method!(sapling_nullifiers, Vec); + + /// Builds DetailedTransactionSummary from builder + pub fn build(&self) -> Result { + Ok(DetailedTransactionSummary { + txid: self + .txid + .ok_or(BuildError::MissingField("txid".to_string()))?, + datetime: self + .datetime + .ok_or(BuildError::MissingField("datetime".to_string()))?, + status: self + .status + .ok_or(BuildError::MissingField("status".to_string()))?, + blockheight: self + .blockheight + .ok_or(BuildError::MissingField("blockheight".to_string()))?, + kind: self + .kind + .ok_or(BuildError::MissingField("kind".to_string()))?, + value: self + .value + .ok_or(BuildError::MissingField("value".to_string()))?, + fee: self + .fee + .ok_or(BuildError::MissingField("fee".to_string()))?, + zec_price: self + .zec_price + .ok_or(BuildError::MissingField("zec_price".to_string()))?, + orchard_notes: self + .orchard_notes + .clone() + .ok_or(BuildError::MissingField("orchard_notes".to_string()))?, + sapling_notes: self + .sapling_notes + .clone() + .ok_or(BuildError::MissingField("sapling_notes".to_string()))?, + transparent_coins: self + .transparent_coins + .clone() + .ok_or(BuildError::MissingField("transparent_coins".to_string()))?, + outgoing_tx_data: self + .outgoing_tx_data + .clone() + .ok_or(BuildError::MissingField("outgoing_tx_data".to_string()))?, + orchard_nullifiers: self + .orchard_nullifiers + .clone() + .ok_or(BuildError::MissingField("orchard_nullifiers".to_string()))?, + sapling_nullifiers: self + .sapling_nullifiers + .clone() + .ok_or(BuildError::MissingField("sapling_nullifiers".to_string()))?, + }) + } + } + + impl Default for DetailedTransactionSummaryBuilder { + fn default() -> Self { + Self::new() + } + } + + /// Orchard note summary. + /// A struct designed for conveniently displaying information to the user or converting to JSON to pass through an FFI. + /// A "snapshot" of the state of the output in the wallet at the time the summary was constructed. + /// Not to be used for internal logic in the system. #[derive(Clone, PartialEq, Debug)] pub struct OrchardNoteSummary { value: u64, - spend_status: SpendStatus, + spend_summary: SpendSummary, output_index: Option, memo: Option, } impl OrchardNoteSummary { - /// TODO: doc comment + /// Creates an OrchardNoteSummary from parts pub fn from_parts( value: u64, - spend_status: SpendStatus, + spend_status: SpendSummary, output_index: Option, memo: Option, ) -> Self { OrchardNoteSummary { value, - spend_status, + spend_summary: spend_status, output_index, memo, } } - /// TODO: doc comment + /// Gets value pub fn value(&self) -> u64 { self.value } - /// TODO: doc comment - pub fn spend_status(&self) -> SpendStatus { - self.spend_status + + /// Gets spend status + pub fn spend_summary(&self) -> SpendSummary { + self.spend_summary } - /// TODO: doc comment + + /// Gets output index pub fn output_index(&self) -> Option { self.output_index } - /// TODO: doc comment + /// Gets memo pub fn memo(&self) -> Option<&str> { self.memo.as_deref() } @@ -1148,7 +1535,7 @@ pub mod summaries { output index: {} memo: {} }}", - self.value, self.spend_status, output_index, memo, + self.value, self.spend_summary, output_index, memo, ) } } @@ -1157,14 +1544,15 @@ pub mod summaries { fn from(note: OrchardNoteSummary) -> Self { json::object! { "value" => note.value, - "spend_status" => note.spend_status.to_string(), + "spend_status" => note.spend_summary.to_string(), "output_index" => note.output_index, "memo" => note.memo, } } } - struct OrchardNoteSummaries(Vec); + /// Wraps a vec of orchard note summaries for the implementation of std::fmt::Display + pub struct OrchardNoteSummaries(Vec); impl std::fmt::Display for OrchardNoteSummaries { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -1175,43 +1563,48 @@ pub mod summaries { } } - /// TODO: doc comment + /// Sapling note summary. + /// A struct designed for conveniently displaying information to the user or converting to JSON to pass through an FFI. + /// A "snapshot" of the state of the output in the wallet at the time the summary was constructed. + /// Not to be used for internal logic in the system. #[derive(Clone, PartialEq, Debug)] pub struct SaplingNoteSummary { value: u64, - spend_status: SpendStatus, + spend_summary: SpendSummary, output_index: Option, memo: Option, } impl SaplingNoteSummary { - /// TODO: doc comment + /// Creates a SaplingNoteSummary from parts pub fn from_parts( value: u64, - spend_status: SpendStatus, + spend_status: SpendSummary, output_index: Option, memo: Option, ) -> Self { SaplingNoteSummary { value, - spend_status, + spend_summary: spend_status, output_index, memo, } } - /// TODO: doc comment + /// Gets value pub fn value(&self) -> u64 { self.value } - /// TODO: doc comment - pub fn spend_status(&self) -> SpendStatus { - self.spend_status + + /// Gets spend status + pub fn spend_summary(&self) -> SpendSummary { + self.spend_summary } - /// TODO: doc comment + + /// Gets output index pub fn output_index(&self) -> Option { self.output_index } - /// TODO: doc comment + /// Gets memo pub fn memo(&self) -> Option<&str> { self.memo.as_deref() } @@ -1237,7 +1630,7 @@ pub mod summaries { output index: {} memo: {} }}", - self.value, self.spend_status, output_index, memo, + self.value, self.spend_summary, output_index, memo, ) } } @@ -1246,14 +1639,15 @@ pub mod summaries { fn from(note: SaplingNoteSummary) -> Self { json::object! { "value" => note.value, - "spend_status" => note.spend_status.to_string(), + "spend_status" => note.spend_summary.to_string(), "output_index" => note.output_index, "memo" => note.memo, } } } - struct SaplingNoteSummaries(Vec); + /// Wraps a vec of sapling note summaries for the implementation of std::fmt::Display + pub struct SaplingNoteSummaries(Vec); impl std::fmt::Display for SaplingNoteSummaries { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -1264,32 +1658,37 @@ pub mod summaries { } } - /// TODO: doc comment + /// Transparent coin summary. + /// A struct designed for conveniently displaying information to the user or converting to JSON to pass through an FFI. + /// A "snapshot" of the state of the output in the wallet at the time the summary was constructed. + /// Not to be used for internal logic in the system. #[derive(Clone, PartialEq, Debug)] pub struct TransparentCoinSummary { value: u64, - spend_status: SpendStatus, + spend_summary: SpendSummary, output_index: u64, } impl TransparentCoinSummary { - /// TODO: doc comment - pub fn from_parts(value: u64, spend_status: SpendStatus, output_index: u64) -> Self { + /// Creates a SaplingNoteSummary from parts + pub fn from_parts(value: u64, spend_status: SpendSummary, output_index: u64) -> Self { TransparentCoinSummary { value, - spend_status, + spend_summary: spend_status, output_index, } } - /// TODO: doc comment + /// Gets value pub fn value(&self) -> u64 { self.value } - /// TODO: doc comment - pub fn spend_status(&self) -> SpendStatus { - self.spend_status + + /// Gets spend status + pub fn spend_summary(&self) -> SpendSummary { + self.spend_summary } - /// TODO: doc comment + + /// Gets output index pub fn output_index(&self) -> u64 { self.output_index } @@ -1304,7 +1703,7 @@ pub mod summaries { spend status: {} output index: {} }}", - self.value, self.spend_status, self.output_index, + self.value, self.spend_summary, self.output_index, ) } } @@ -1312,13 +1711,14 @@ pub mod summaries { fn from(note: TransparentCoinSummary) -> Self { json::object! { "value" => note.value, - "spend_status" => note.spend_status.to_string(), + "spend_status" => note.spend_summary.to_string(), "output_index" => note.output_index, } } } - struct TransparentCoinSummaries(Vec); + /// Wraps a vec of transparent coin summaries for the implementation of std::fmt::Display + pub struct TransparentCoinSummaries(Vec); impl std::fmt::Display for TransparentCoinSummaries { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -1329,26 +1729,158 @@ pub mod summaries { } } - /// TODO: doc comment + /// Wraps a vec of orchard nullifier summaries for the implementation of std::fmt::Display + pub struct OrchardNullifierSummaries(Vec); + + impl std::fmt::Display for OrchardNullifierSummaries { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for nullifier in &self.0 { + write!(f, "\n{}", nullifier)?; + } + Ok(()) + } + } + + /// Wraps a vec of sapling nullifier summaries for the implementation of std::fmt::Display + struct SaplingNullifierSummaries(Vec); + + impl std::fmt::Display for SaplingNullifierSummaries { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for nullifier in &self.0 { + write!(f, "\n{}", nullifier)?; + } + Ok(()) + } + } + + /// Spend status of an output #[derive(Clone, Copy, PartialEq, Debug)] - pub enum SpendStatus { - /// TODO: doc comment + pub enum SpendSummary { + /// Output is not spent. Unspent, - /// TODO: doc comment + /// Output is pending spent. + /// The transaction consuming this output has been transmitted. + TransmittedSpent(TxId), + /// Output is pending spent. + /// The transaction consuming this output has been detected in the mempool. + MempoolSpent(TxId), + /// Output is spent. + /// The transaction consuming this output is confirmed. Spent(TxId), - /// TODO: doc comment - PendingSpent(TxId), } - impl std::fmt::Display for SpendStatus { + impl SpendSummary { + /// converts the interface spend to a SpendSummary + pub fn from_spend(spend: &Option<(TxId, ConfirmationStatus)>) -> Self { + match spend { + Some((txid, ConfirmationStatus::Transmitted(_))) => { + SpendSummary::TransmittedSpent(*txid) + } + + Some((txid, ConfirmationStatus::Mempool(_))) => SpendSummary::MempoolSpent(*txid), + Some((txid, ConfirmationStatus::Confirmed(_))) => SpendSummary::Spent(*txid), + _ => SpendSummary::Unspent, + } + } + } + + impl std::fmt::Display for SpendSummary { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - SpendStatus::Unspent => write!(f, "unspent"), - SpendStatus::Spent(txid) => write!(f, "spent in {}", txid), - SpendStatus::PendingSpent(txid) => write!(f, "pending spent in {}", txid), + SpendSummary::Unspent => write!(f, "unspent"), + SpendSummary::TransmittedSpent(txid) => write!(f, "transmitted: spent in {}", txid), + SpendSummary::Spent(txid) => write!(f, "confirmed: spent in {}", txid), + SpendSummary::MempoolSpent(txid) => write!(f, "mempool: spent in {}", txid), } } } + + pub(crate) fn basic_transaction_summary_parts( + transaction_record: &TransactionRecord, + transaction_records: &TransactionRecordsById, + chain: &ChainType, + ) -> ( + TransactionKind, + u64, + Option, + Vec, + Vec, + Vec, + ) { + let kind = transaction_records.transaction_kind(transaction_record, chain); + let value = match kind { + TransactionKind::Received + | TransactionKind::Sent(SendType::Shield) + | TransactionKind::Sent(SendType::SendToSelf) => { + transaction_record.total_value_received() + } + TransactionKind::Sent(SendType::Send) => transaction_record.value_outgoing(), + }; + let fee = transaction_records + .calculate_transaction_fee(transaction_record) + .ok(); + let orchard_notes = transaction_record + .orchard_notes + .iter() + .map(|output| { + let spend_summary = SpendSummary::from_spend(output.spending_tx_status()); + + let memo = if let Some(Memo::Text(memo_text)) = &output.memo { + Some(memo_text.to_string()) + } else { + None + }; + + OrchardNoteSummary::from_parts( + output.value(), + spend_summary, + output.output_index, + memo, + ) + }) + .collect::>(); + let sapling_notes = transaction_record + .sapling_notes + .iter() + .map(|output| { + let spend_summary = SpendSummary::from_spend(output.spending_tx_status()); + + let memo = if let Some(Memo::Text(memo_text)) = &output.memo { + Some(memo_text.to_string()) + } else { + None + }; + + SaplingNoteSummary::from_parts( + output.value(), + spend_summary, + output.output_index, + memo, + ) + }) + .collect::>(); + let transparent_coins = transaction_record + .transparent_outputs + .iter() + .map(|output| { + let spend_summary = SpendSummary::from_spend(output.spending_tx_status()); + + TransparentCoinSummary::from_parts( + output.value(), + spend_summary, + output.output_index, + ) + }) + .collect::>(); + ( + kind, + value, + fee, + orchard_notes, + sapling_notes, + transparent_coins, + ) + } } /// Convenience wrapper for primitive @@ -1462,6 +1994,30 @@ impl WalletZecPriceInfo { } } +/// Generate a new ephemeral transparent address, +/// for use in a send to a TEX address. +pub fn new_persistent_ephemeral_address( + transparent_child_ephemeral_addresses: &append_only_vec::AppendOnlyVec<( + TransparentAddress, + TransparentAddressMetadata, + )>, + + transparent_ephemeral_ivk: &zcash_primitives::legacy::keys::EphemeralIvk, +) -> Result< + ( + zcash_primitives::legacy::TransparentAddress, + zcash_client_backend::wallet::TransparentAddressMetadata, + ), + super::error::KeyError, +> { + let (ephemeral_address, metadata) = super::keys::unified::WalletCapability::ephemeral_address( + transparent_ephemeral_ivk, + transparent_child_ephemeral_addresses.len() as u32, + )?; + transparent_child_ephemeral_addresses.push((ephemeral_address, metadata.clone())); + Ok((ephemeral_address, metadata)) +} + #[test] fn read_write_empty_sapling_tree() { let mut buffer = Vec::new(); @@ -1477,7 +2033,7 @@ fn read_write_empty_sapling_tree() { ) } -#[cfg(test)] +#[cfg(any(test, feature = "test-elevation"))] pub(crate) mod mocks { use zcash_primitives::memo::Memo; diff --git a/zingolib/src/wallet/describe.rs b/zingolib/src/wallet/describe.rs index d144f24d93..83cf5a49d4 100644 --- a/zingolib/src/wallet/describe.rs +++ b/zingolib/src/wallet/describe.rs @@ -9,7 +9,8 @@ use zcash_primitives::transaction::fees::zip317::MARGINAL_FEE; use std::{cmp, sync::Arc}; use tokio::sync::RwLock; -use zcash_primitives::zip339::Mnemonic; + +use bip0039::Mnemonic; use zcash_note_encryption::Domain; @@ -21,22 +22,32 @@ use crate::wallet::notes::ShieldedNoteInterface; use crate::wallet::traits::Diversifiable as _; use crate::wallet::error::BalanceError; -use crate::wallet::keys::unified::{Capability, WalletCapability}; +use crate::wallet::keys::unified::WalletCapability; use crate::wallet::notes::TransparentOutput; use crate::wallet::traits::DomainWalletExt; use crate::wallet::traits::Recipient; use crate::wallet::LightWallet; -use crate::wallet::{data::BlockData, tx_map_and_maybe_trees::TxMapAndMaybeTrees}; +use crate::wallet::{data::BlockData, tx_map::TxMap}; + +use super::keys::unified::UnifiedKeyStore; impl LightWallet { + /// returns Some seed phrase for the wallet. + /// if wallet does not have a seed phrase, returns None + pub async fn get_seed_phrase(&self) -> Option { + self.mnemonic() + .map(|(mnemonic, _)| mnemonic.phrase().to_string()) + } // Core shielded_balance function, other public methods dispatch specific sets of filters to this // method for processing. - // This methods ensures that None is returned in the case of a missing view capability + /// Returns the sum of unspent notes recorded by the wallet + /// with optional filtering. + /// This method ensures that None is returned in the case of a missing view capability. #[allow(clippy::type_complexity)] - async fn get_filtered_balance( + pub async fn get_filtered_balance( &self, - filters: &[Box bool + '_>], + filter_function: Box bool + '_>, ) -> Option where D: DomainWalletExt, @@ -44,17 +55,17 @@ impl LightWallet { ::Recipient: Recipient, { // For the moment we encode lack of view capability as None - match D::SHIELDED_PROTOCOL { - ShieldedProtocol::Sapling => { - if !self.wallet_capability().sapling.can_view() { - return None; + match self.wallet_capability().unified_key_store() { + UnifiedKeyStore::Spend(_) => (), + UnifiedKeyStore::View(ufvk) => match D::SHIELDED_PROTOCOL { + ShieldedProtocol::Sapling => { + ufvk.sapling()?; } - } - ShieldedProtocol::Orchard => { - if !self.wallet_capability().orchard.can_view() { - return None; + ShieldedProtocol::Orchard => { + ufvk.orchard()?; } - } + }, + UnifiedKeyStore::Empty => return None, } Some( self.transaction_context @@ -67,13 +78,11 @@ impl LightWallet { let mut selected_notes: Box> = Box::new(D::WalletNote::transaction_metadata_notes(transaction).iter()); // All filters in iterator are applied, by this loop - for filtering_fn in filters { - selected_notes = - Box::new(selected_notes.filter(|nnmd| filtering_fn(nnmd, transaction))) - } + selected_notes = + Box::new(selected_notes.filter(|nnmd| filter_function(nnmd, transaction))); selected_notes .map(|notedata| { - if notedata.spent().is_none() && notedata.pending_spent().is_none() { + if notedata.spending_tx_status().is_none() { ::value(notedata) } else { 0 @@ -91,26 +100,29 @@ impl LightWallet { ::Recipient: Recipient, ::Note: PartialEq + Clone, { - if let Capability::Spend(_) = self.wallet_capability().orchard { + if let UnifiedKeyStore::Spend(_) = self.wallet_capability().unified_key_store() { self.confirmed_balance::().await } else { None } } /// Sums the transparent balance (unspent) - pub async fn tbalance(&self) -> Option { - if self.wallet_capability().transparent.can_view() { - Some( - self.get_utxos() - .await - .iter() - .filter(|transparent_output| transparent_output.is_unspent()) - .map(|utxo| utxo.value) - .sum::(), - ) - } else { - None + pub async fn get_transparent_balance(&self) -> Option { + match self.wallet_capability().unified_key_store() { + UnifiedKeyStore::Spend(_) => (), + UnifiedKeyStore::View(ufvk) => { + ufvk.transparent()?; + } + UnifiedKeyStore::Empty => return None, } + Some( + self.get_utxos() + .await + .iter() + .filter(|transparent_output| transparent_output.is_unspent()) + .map(|utxo| utxo.value) + .sum::(), + ) } /// On chain balance @@ -120,11 +132,11 @@ impl LightWallet { ::Note: PartialEq + Clone, { #[allow(clippy::type_complexity)] - let filters: &[Box bool>] = &[ - Box::new(|_, transaction| transaction.status.is_confirmed()), - Box::new(|nnmd, _| !nnmd.pending_receipt()), - ]; - self.get_filtered_balance::(filters).await + let filter_function: Box bool> = + Box::new(|nnmd, transaction| { + transaction.status.is_confirmed() && !nnmd.pending_receipt() + }); + self.get_filtered_balance::(filter_function).await } /// The amount in pending notes, not yet on chain pub async fn pending_balance(&self) -> Option @@ -132,7 +144,7 @@ impl LightWallet { ::Recipient: Recipient, ::Note: PartialEq + Clone, { - self.get_filtered_balance::(&[Box::new(|note, _| note.pending_receipt())]) + self.get_filtered_balance::(Box::new(|note, _| note.pending_receipt())) .await } @@ -144,12 +156,13 @@ impl LightWallet { ::Note: PartialEq + Clone, { #[allow(clippy::type_complexity)] - let filters: &[Box bool>] = &[ - Box::new(|_, transaction| transaction.status.is_confirmed()), - Box::new(|note, _| !note.pending_receipt()), - Box::new(|note, _| note.value() >= MARGINAL_FEE.into_u64()), - ]; - self.get_filtered_balance::(filters).await + let filter_function: Box bool> = + Box::new(|note, transaction| { + transaction.status.is_confirmed() + && !note.pending_receipt() + && note.value() > MARGINAL_FEE.into_u64() + }); + self.get_filtered_balance::(filter_function).await } /// Returns total balance of all shielded pools excluding any notes with value less than marginal fee @@ -174,7 +187,7 @@ impl LightWallet { } /// TODO: Add Doc Comment Here! pub(crate) fn note_address( - network: &zingoconfig::ChainType, + network: &crate::config::ChainType, note: &D::WalletNote, wallet_capability: &WalletCapability, ) -> String @@ -182,7 +195,7 @@ impl LightWallet { ::Recipient: Recipient, ::Note: PartialEq + Clone, { - D::wc_to_fvk(wallet_capability).expect("to get fvk from wc") + D::unified_key_store_to_fvk(wallet_capability.unified_key_store()).expect("to get fvk from the unified key store") .diversified_address(*note.diversifier()) .and_then(|address| { D::ua_from_contained_receiver(wallet_capability, &address) @@ -216,7 +229,7 @@ impl LightWallet { /// Return a copy of the blocks currently in the wallet, needed to process possible reorgs pub async fn get_blocks(&self) -> Vec { - self.blocks.read().await.iter().cloned().collect() + self.last_100_blocks.read().await.iter().cloned().collect() } /// Get the first block that this wallet has a transaction in. This is often used as the wallet's "birthday" @@ -242,7 +255,7 @@ impl LightWallet { )) } - /// Get all (unspent) utxos. pending spent utxos are included + /// Get all (unspent) utxos. pub async fn get_utxos(&self) -> Vec { self.transaction_context .transaction_metadata_set @@ -254,7 +267,7 @@ impl LightWallet { transaction .transparent_outputs .iter() - .filter(|utxo| !utxo.is_spent()) + .filter(|utxo| !utxo.is_spent_confirmed()) }) .cloned() .collect::>() @@ -262,7 +275,7 @@ impl LightWallet { /// TODO: Add Doc Comment Here! pub async fn last_synced_hash(&self) -> String { - self.blocks + self.last_100_blocks .read() .await .first() @@ -273,7 +286,7 @@ impl LightWallet { /// TODO: How do we know that 'sapling_activation_height - 1' is only returned /// when it should be? When should it be? pub async fn last_synced_height(&self) -> u64 { - self.blocks + self.last_100_blocks .read() .await .first() @@ -282,30 +295,112 @@ impl LightWallet { } /// TODO: Add Doc Comment Here! - pub fn transactions(&self) -> Arc> { + pub fn transactions(&self) -> Arc> { self.transaction_context.transaction_metadata_set.clone() } + + /// lists the transparent addresses known by the wallet. + pub fn get_transparent_addresses(&self) -> Vec { + self.wallet_capability() + .transparent_child_addresses() + .iter() + .map(|(_index, sk)| *sk) + .collect::>() + } } -#[cfg(test)] -mod tests { +#[cfg(any(test, feature = "test-elevation"))] +mod test { + + use zcash_client_backend::PoolType; + use zcash_client_backend::ShieldedProtocol; + use zcash_primitives::consensus::NetworkConstants as _; + + use crate::wallet::LightWallet; + + /// these functions have clearer typing than + /// the production functions using json that could be upgraded soon + impl LightWallet { + #[allow(clippy::result_unit_err)] + /// gets a UnifiedAddress, the first of the wallet. + /// zingolib includes derivations of further addresses. + /// ZingoMobile uses one address. + pub fn get_first_ua(&self) -> Result { + Ok(self + .wallet_capability() + .addresses() + .iter() + .next() + .ok_or(())? + .clone()) + } + + #[allow(clippy::result_unit_err)] + /// UnifiedAddress type is not a string. to process it into a string requires chain date. + pub fn encode_ua_as_pool( + &self, + ua: &zcash_keys::address::UnifiedAddress, + pool: PoolType, + ) -> Result { + match pool { + PoolType::Transparent => ua + .transparent() + .map(|taddr| { + crate::wallet::keys::address_from_pubkeyhash( + &self.transaction_context.config, + *taddr, + ) + }) + .ok_or(()), + PoolType::Shielded(ShieldedProtocol::Sapling) => ua + .sapling() + .map(|z_addr| { + zcash_keys::encoding::encode_payment_address( + self.transaction_context + .config + .chain + .hrp_sapling_payment_address(), + z_addr, + ) + }) + .ok_or(()), + PoolType::Shielded(ShieldedProtocol::Orchard) => { + Ok(ua.encode(&self.transaction_context.config.chain)) + } + } + } + + #[allow(clippy::result_unit_err)] + /// gets a string address for the wallet, based on pooltype + pub fn get_first_address(&self, pool: PoolType) -> Result { + let ua = self.get_first_ua()?; + self.encode_ua_as_pool(&ua, pool) + } + } + + #[cfg(test)] use orchard::note_encryption::OrchardDomain; + #[cfg(test)] use sapling_crypto::note_encryption::SaplingDomain; - + #[cfg(test)] use zingo_status::confirmation_status::ConfirmationStatus; - use zingoconfig::ZingoConfigBuilder; - - use crate::{ - mocks::{orchard_note::OrchardCryptoNoteBuilder, SaplingCryptoNoteBuilder}, - wallet::{ - notes::{ - orchard::mocks::OrchardNoteBuilder, sapling::mocks::SaplingNoteBuilder, - transparent::mocks::TransparentOutputBuilder, - }, - transaction_record::mocks::TransactionRecordBuilder, - LightWallet, WalletBase, - }, - }; + + #[cfg(test)] + use crate::config::ZingoConfigBuilder; + #[cfg(test)] + use crate::mocks::orchard_note::OrchardCryptoNoteBuilder; + #[cfg(test)] + use crate::mocks::SaplingCryptoNoteBuilder; + #[cfg(test)] + use crate::wallet::notes::orchard::mocks::OrchardNoteBuilder; + #[cfg(test)] + use crate::wallet::notes::sapling::mocks::SaplingNoteBuilder; + #[cfg(test)] + use crate::wallet::notes::transparent::mocks::TransparentOutputBuilder; + #[cfg(test)] + use crate::wallet::transaction_record::mocks::TransactionRecordBuilder; + #[cfg(test)] + use crate::wallet::WalletBase; #[tokio::test] async fn confirmed_balance_excluding_dust() { @@ -335,7 +430,7 @@ mod tests { OrchardNoteBuilder::default() .note( OrchardCryptoNoteBuilder::default() - .value(orchard::value::NoteValue::from_raw(5_000)) + .value(orchard::value::NoteValue::from_raw(5_001)) .clone(), ) .clone(), @@ -350,8 +445,8 @@ mod tests { .clone(), ) .build(); - let pending_tx_record = TransactionRecordBuilder::default() - .status(ConfirmationStatus::Pending(95.into())) + let mempool_tx_record = TransactionRecordBuilder::default() + .status(ConfirmationStatus::Mempool(95.into())) .transparent_outputs(TransparentOutputBuilder::default()) .sapling_notes(SaplingNoteBuilder::default()) .orchard_notes(OrchardNoteBuilder::default()) @@ -367,7 +462,7 @@ mod tests { .insert_transaction_record(confirmed_tx_record); tx_map .transaction_records_by_id - .insert_transaction_record(pending_tx_record); + .insert_transaction_record(mempool_tx_record); } assert_eq!( @@ -380,7 +475,7 @@ mod tests { wallet .confirmed_balance_excluding_dust::() .await, - Some(1_605_000) + Some(1_605_001) ); } } diff --git a/zingolib/src/wallet/disk.rs b/zingolib/src/wallet/disk.rs index 152bb4cd24..717c52ed69 100644 --- a/zingolib/src/wallet/disk.rs +++ b/zingolib/src/wallet/disk.rs @@ -2,38 +2,45 @@ use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; use log::{error, info}; +use zcash_keys::keys::UnifiedSpendingKey; +use zip32::AccountId; use std::{ io::{self, Error, ErrorKind, Read, Write}, sync::{atomic::AtomicU64, Arc}, }; + +#[cfg(feature = "sync")] +use std::collections::BTreeMap; use tokio::sync::RwLock; -use zcash_primitives::zip339::Mnemonic; + +use bip0039::Mnemonic; use zcash_client_backend::proto::service::TreeState; use zcash_encoding::{Optional, Vector}; use zcash_primitives::consensus::BlockHeight; -use zingoconfig::ZingoConfig; +use crate::{config::ZingoConfig, wallet::keys::unified::UnifiedKeyStore}; use crate::wallet::traits::ReadableWriteable; use crate::wallet::WalletOptions; use crate::wallet::{utils, SendProgress}; -use super::keys::unified::{Capability, WalletCapability}; +use super::keys::unified::WalletCapability; use super::LightWallet; use super::{ data::{BlockData, WalletZecPriceInfo}, transaction_context::TransactionContext, - tx_map_and_maybe_trees::TxMapAndMaybeTrees, + tx_map::TxMap, }; impl LightWallet { - /// TODO: Add Doc Comment Here! + /// Changes in version 30: + /// - New WalletCapability version (v4) which implements read/write for ephemeral addresses pub const fn serialized_version() -> u64 { - 28 + 30 } /// TODO: Add Doc Comment Here! @@ -42,9 +49,13 @@ impl LightWallet { writer.write_u64::(Self::serialized_version())?; // Write all the keys - self.transaction_context.key.write(&mut writer)?; + self.transaction_context + .key + .write(&mut writer, self.transaction_context.config.chain)?; - Vector::write(&mut writer, &self.blocks.read().await, |w, b| b.write(w))?; + Vector::write(&mut writer, &self.last_100_blocks.read().await, |w, b| { + b.write(w) + })?; self.transaction_context .transaction_metadata_set @@ -113,23 +124,7 @@ impl LightWallet { } info!("Reading wallet version {}", external_version); - let wallet_capability = WalletCapability::read(&mut reader, ())?; - info!("Keys in this wallet:"); - match &wallet_capability.orchard { - Capability::None => (), - Capability::View(_) => info!(" - Orchard Full Viewing Key"), - Capability::Spend(_) => info!(" - Orchard Spending Key"), - }; - match &wallet_capability.sapling { - Capability::None => (), - Capability::View(_) => info!(" - Sapling Extended Full Viewing Key"), - Capability::Spend(_) => info!(" - Sapling Extended Spending Key"), - }; - match &wallet_capability.transparent { - Capability::None => (), - Capability::View(_) => info!(" - transparent extended public key"), - Capability::Spend(_) => info!(" - transparent extended private key"), - }; + let mut wallet_capability = WalletCapability::read(&mut reader, config.chain)?; let mut blocks = Vector::read(&mut reader, |r| BlockData::read(r))?; if external_version <= 14 { @@ -139,9 +134,9 @@ impl LightWallet { } let transactions = if external_version <= 14 { - TxMapAndMaybeTrees::read_old(&mut reader, &wallet_capability) + TxMap::read_old(&mut reader, &wallet_capability) } else { - TxMapAndMaybeTrees::read(&mut reader, &wallet_capability) + TxMap::read(&mut reader, &wallet_capability) }?; let chain_name = utils::read_string(&mut reader)?; @@ -190,12 +185,6 @@ impl LightWallet { WalletZecPriceInfo::read(&mut reader)? }; - let transaction_context = TransactionContext::new( - config, - Arc::new(wallet_capability), - Arc::new(RwLock::new(transactions)), - ); - let _orchard_anchor_height_pairs = if external_version == 25 { Vector::read(&mut reader, |r| { let mut anchor_bytes = [0; 32]; @@ -227,8 +216,74 @@ impl LightWallet { None }; + // Derive unified spending key from seed and overide temporary USK if wallet is pre v29. + // + // UnifiedSpendingKey is initially incomplete for old wallet versions. + // This is due to the legacy transparent extended private key (ExtendedPrivKey) not containing all information required for BIP0032. + // There is also the issue that the legacy transparent private key is derived an extra level to the external scope. + if external_version < 29 { + if let Some(mnemonic) = mnemonic.as_ref() { + wallet_capability.set_unified_key_store(UnifiedKeyStore::Spend(Box::new( + UnifiedSpendingKey::from_seed( + &config.chain, + &mnemonic.0.to_seed(""), + AccountId::ZERO, + ) + .map_err(|e| { + Error::new( + ErrorKind::InvalidData, + format!( + "Failed to derive unified spending key from stored seed bytes. {}", + e + ), + ) + })?, + ))); + } else if let UnifiedKeyStore::Spend(_) = wallet_capability.unified_key_store() { + return Err(io::Error::new( + ErrorKind::Other, + "loading from legacy spending keys with no seed phrase to recover", + )); + } + } + + info!("Keys in this wallet:"); + match wallet_capability.unified_key_store() { + UnifiedKeyStore::Spend(_) => { + info!(" - orchard spending key"); + info!(" - sapling extended spending key"); + info!(" - transparent extended private key"); + } + UnifiedKeyStore::View(ufvk) => { + if ufvk.orchard().is_some() { + info!(" - orchard full viewing key"); + } + if ufvk.sapling().is_some() { + info!(" - sapling diversifiable full viewing key"); + } + if ufvk.transparent().is_some() { + info!(" - transparent extended public key"); + } + } + UnifiedKeyStore::Empty => info!(" - no keys found"), + } + + // this initialization combines two types of data + let transaction_context = TransactionContext::new( + // Config data could be used differently based on the circumstances + // hardcoded? + // entered at init by user? + // stored on disk in a separate location and connected by a descendant library (such as zingo-mobile)? + config, + // Saveable Arc data + // - Arcs allow access between threads. + // - This data is loaded from the wallet file and but needs multithreaded access during sync. + Arc::new(wallet_capability), + Arc::new(RwLock::new(transactions)), + ); + let lw = Self { - blocks: Arc::new(RwLock::new(blocks)), + last_100_blocks: Arc::new(RwLock::new(blocks)), mnemonic, wallet_options: Arc::new(RwLock::new(wallet_options)), birthday: AtomicU64::new(birthday), @@ -236,8 +291,19 @@ impl LightWallet { send_progress: Arc::new(RwLock::new(SendProgress::new(0))), price: Arc::new(RwLock::new(price)), transaction_context, + #[cfg(feature = "sync")] + wallet_blocks: BTreeMap::new(), + #[cfg(feature = "sync")] + nullifier_map: zingo_sync::primitives::NullifierMap::new(), + #[cfg(feature = "sync")] + shard_trees: zingo_sync::witness::ShardTrees::new(), + #[cfg(feature = "sync")] + sync_state: zingo_sync::primitives::SyncState::new(), }; Ok(lw) } } + +#[cfg(any(test, feature = "test-elevation"))] +pub mod testing; diff --git a/zingolib/src/wallet/disk/testing.rs b/zingolib/src/wallet/disk/testing.rs new file mode 100644 index 0000000000..536c635229 --- /dev/null +++ b/zingolib/src/wallet/disk/testing.rs @@ -0,0 +1,108 @@ +//! functionality for testing the save and load functions of LightWallet. +//! do not compile test-elevation feature for production. + +use bip0039::Mnemonic; +use zcash_keys::keys::{Era, UnifiedSpendingKey}; + +use crate::wallet::keys::unified::UnifiedKeyStore; + +use super::LightWallet; + +impl LightWallet { + /// connects a wallet to a local regtest node. + pub async fn unsafe_from_buffer_regtest(data: &[u8]) -> Self { + // this step starts a TestEnvironment and picks a new port! + let lightwalletd_uri = + crate::testutils::scenarios::setup::TestEnvironmentGenerator::new(None) + .get_lightwalletd_uri(); + let config = crate::config::load_clientconfig( + lightwalletd_uri, + None, + crate::config::ChainType::Regtest(crate::config::RegtestNetwork::all_upgrades_active()), + true, + ) + .unwrap(); + Self::read_internal(data, &config) + .await + .map_err(|e| format!("Cannot deserialize LightWallet file!: {}", e)) + .unwrap() + } + /// parses a wallet as an testnet wallet, aimed at a default testnet server + pub async fn unsafe_from_buffer_testnet(data: &[u8]) -> Self { + let config = crate::config::ZingoConfig::create_testnet(); + Self::read_internal(data, &config) + .await + .map_err(|e| format!("Cannot deserialize LightWallet file!: {}", e)) + .unwrap() + } + /// parses a wallet as an testnet wallet, aimed at a default testnet server + pub async fn unsafe_from_buffer_mainnet(data: &[u8]) -> Self { + let config = crate::config::ZingoConfig::create_mainnet(); + Self::read_internal(data, &config) + .await + .map_err(|e| format!("Cannot deserialize LightWallet file!: {}", e)) + .unwrap() + } +} + +// async fn assert_test_wallet(case: examples::LegacyWalletCase) { +// let wallet = LightWallet::load_example_wallet(case).await; +// } + +/// example wallets +/// including from different versions of the software. +pub mod examples; + +/// tests +#[cfg(test)] +pub mod tests; + +// test helper functions + +/// asserts that a fresh capability generated with the seed matches the extant capability, which also can export the seed +pub async fn assert_wallet_capability_matches_seed( + wallet: &LightWallet, + expected_seed_phrase: String, +) { + let actual_seed_phrase = wallet.get_seed_phrase().await.unwrap(); + assert_eq!(expected_seed_phrase, actual_seed_phrase); + + let expected_mnemonic = ( + Mnemonic::::from_phrase(expected_seed_phrase).unwrap(), + 0, + ); + + let expected_wc = crate::wallet::keys::unified::WalletCapability::new_from_phrase( + &wallet.transaction_context.config, + &expected_mnemonic.0, + expected_mnemonic.1, + ) + .unwrap(); + let wc = wallet.wallet_capability(); + + // Compare USK + let UnifiedKeyStore::Spend(usk) = &wc.unified_key_store() else { + panic!("Expected Unified Spending Key"); + }; + assert_eq!( + usk.to_bytes(Era::Orchard), + UnifiedSpendingKey::try_from(expected_wc.unified_key_store()) + .unwrap() + .to_bytes(Era::Orchard) + ); +} + +/// basically does what it says on the tin +pub async fn assert_wallet_capability_contains_n_triple_pool_receivers( + wallet: &LightWallet, + expected_num_addresses: usize, +) { + let wc = wallet.wallet_capability(); + + assert_eq!(wc.addresses().len(), expected_num_addresses); + for addr in wc.addresses().iter() { + assert!(addr.orchard().is_some()); + assert!(addr.sapling().is_some()); + assert!(addr.transparent().is_some()); + } +} diff --git a/zingolib/src/wallet/disk/testing/examples.rs b/zingolib/src/wallet/disk/testing/examples.rs new file mode 100644 index 0000000000..df0c9441eb --- /dev/null +++ b/zingolib/src/wallet/disk/testing/examples.rs @@ -0,0 +1,329 @@ +use zcash_client_backend::PoolType; +use zcash_client_backend::ShieldedProtocol; + +use super::super::LightWallet; + +/// ExampleWalletNetworkCase sorts first by Network, then seed, then last saved version. +/// It is public so that any consumer can select and load any example wallet. +#[non_exhaustive] +#[derive(Clone)] +pub enum ExampleWalletNetwork { + /// / + Regtest(ExampleRegtestWalletSeed), + /// / + Testnet(ExampleTestnetWalletSeed), + /// / + Mainnet(ExampleMainnetWalletSeed), +} + +/// / +#[non_exhaustive] +#[derive(Clone)] +pub enum ExampleMainnetWalletSeed { + /// this is a mainnet wallet originally called missing_data_test + VTFCORFBCBPCTCFUPMEGMWBP(ExampleVTFCORFBCBPCTCFUPMEGMWBPVersion), + /// empty mainnet wallet + HHCCLALTPCCKCSSLPCNETBLR(ExampleHHCCLALTPCCKCSSLPCNETBLRVersion), +} +/// / +#[non_exhaustive] +#[derive(Clone)] +pub enum ExampleVTFCORFBCBPCTCFUPMEGMWBPVersion { + /// wallet was last saved in this serialization version + V28, +} +/// / +#[non_exhaustive] +#[derive(Clone)] +pub enum ExampleHHCCLALTPCCKCSSLPCNETBLRVersion { + /// wallet was last saved in this serialization version + Gf0aaf9347, +} +/// / +#[non_exhaustive] +#[derive(Clone)] +pub enum ExampleTestnetWalletSeed { + /// this testnet wallet was generated at the beginning of serialization v28 by fluidvanadium + CBBHRWIILGBRABABSSHSMTPR(ExampleCBBHRWIILGBRABABSSHSMTPRVersion), + /// This is a testnet seed, generated by fluidvanadium at the beginning of owls (this example wallet enumeration) + MSKMGDBHOTBPETCJWCSPGOPP(ExampleMSKMGDBHOTBPETCJWCSPGOPPVersion), +} +/// / +#[non_exhaustive] +#[derive(Clone)] +pub enum ExampleMSKMGDBHOTBPETCJWCSPGOPPVersion { + /// wallet was last saved by the code in this commit + Gab72a38b, + /// this wallet was synced in this version. does it have a bunch of taz scattered around different addresses? + G93738061a, + /// NU6 added to zingolib re-allows testnet tests by this commit + Ga74fed621, +} +/// A testnet wallet initiated with +/// --seed "chimney better bulb horror rebuild whisper improve intact letter giraffe brave rib appear bulk aim burst snap salt hill sad merge tennis phrase raise" +/// with 3 addresses containing all receivers. +/// including orchard and sapling transactions +#[non_exhaustive] +#[derive(Clone)] +pub enum ExampleCBBHRWIILGBRABABSSHSMTPRVersion { + /// wallet was last saved in this serialization version + V26, + /// wallet was last saved in this serialization version + V27, + /// wallet was last saved in this serialization version + V28, + /// wallet was last saved at this commit + G2f3830058, +} +/// / +#[non_exhaustive] +#[derive(Clone)] +pub enum ExampleRegtestWalletSeed { + /// this is a regtest wallet originally called old_wallet_reorg_test_wallet + HMVASMUVWMSSVICHCARBPOCT(ExampleHMVASMUVWMSSVICHCARBPOCTVersion), + /// this is a regtest wallet originally called v26/sap_only + AAAAAAAAAAAAAAAAAAAAAAAA(ExampleAAAAAAAAAAAAAAAAAAAAAAAAVersion), + /// another regtest wallet + AADAALACAADAALACAADAALAC(ExampleAADAALACAADAALACAADAALACVersion), +} +/// / +#[non_exhaustive] +#[derive(Clone)] +pub enum ExampleHMVASMUVWMSSVICHCARBPOCTVersion { + /// wallet was last saved in this serialization version + V27, +} +/// / +#[non_exhaustive] +#[derive(Clone)] +pub enum ExampleAAAAAAAAAAAAAAAAAAAAAAAAVersion { + /// wallet was last saved in this serialization version + V26, +} +/// / +#[non_exhaustive] +#[derive(Clone)] +pub enum ExampleAADAALACAADAALACAADAALACVersion { + /// existing receivers? + OrchAndSapl, + /// existing receivers? + OrchOnly, +} + +impl ExampleWalletNetwork { + /// loads test wallets + /// this function can be improved by a macro. even better would be to derive directly from the enum. + /// loads any one of the test wallets included in the examples + pub async fn load_example_wallet(&self) -> LightWallet { + match self { + ExampleWalletNetwork::Regtest(example_regt_seed) => match example_regt_seed { + ExampleRegtestWalletSeed::HMVASMUVWMSSVICHCARBPOCT( + ExampleHMVASMUVWMSSVICHCARBPOCTVersion::V27, + ) => { + LightWallet::unsafe_from_buffer_regtest(include_bytes!( + "examples/regtest/hmvasmuvwmssvichcarbpoct/v27/zingo-wallet.dat" + )) + .await + } + ExampleRegtestWalletSeed::AAAAAAAAAAAAAAAAAAAAAAAA( + ExampleAAAAAAAAAAAAAAAAAAAAAAAAVersion::V26, + ) => { + LightWallet::unsafe_from_buffer_regtest(include_bytes!( + "examples/regtest/aaaaaaaaaaaaaaaaaaaaaaaa/v26/zingo-wallet.dat" + )) + .await + } + ExampleRegtestWalletSeed::AADAALACAADAALACAADAALAC( + ExampleAADAALACAADAALACAADAALACVersion::OrchAndSapl, + ) => { + LightWallet::unsafe_from_buffer_regtest(include_bytes!( + "examples/regtest/aadaalacaadaalacaadaalac/orch_and_sapl/zingo-wallet.dat" + )) + .await + } + ExampleRegtestWalletSeed::AADAALACAADAALACAADAALAC( + ExampleAADAALACAADAALACAADAALACVersion::OrchOnly, + ) => { + LightWallet::unsafe_from_buffer_regtest(include_bytes!( + "examples/regtest/aadaalacaadaalacaadaalac/orch_only/zingo-wallet.dat" + )) + .await + } + }, + ExampleWalletNetwork::Testnet(example_testnet_seed) => match example_testnet_seed { + ExampleTestnetWalletSeed::CBBHRWIILGBRABABSSHSMTPR( + ExampleCBBHRWIILGBRABABSSHSMTPRVersion::V26, + ) => { + LightWallet::unsafe_from_buffer_testnet(include_bytes!( + "examples/testnet/cbbhrwiilgbrababsshsmtpr/v26/zingo-wallet.dat" + )) + .await + } + ExampleTestnetWalletSeed::CBBHRWIILGBRABABSSHSMTPR( + ExampleCBBHRWIILGBRABABSSHSMTPRVersion::V27, + ) => { + LightWallet::unsafe_from_buffer_testnet(include_bytes!( + "examples/testnet/cbbhrwiilgbrababsshsmtpr/v27/zingo-wallet.dat" + )) + .await + } + ExampleTestnetWalletSeed::CBBHRWIILGBRABABSSHSMTPR( + ExampleCBBHRWIILGBRABABSSHSMTPRVersion::V28, + ) => { + LightWallet::unsafe_from_buffer_testnet(include_bytes!( + "examples/testnet/cbbhrwiilgbrababsshsmtpr/v28/zingo-wallet.dat" + )) + .await + } + ExampleTestnetWalletSeed::CBBHRWIILGBRABABSSHSMTPR( + ExampleCBBHRWIILGBRABABSSHSMTPRVersion::G2f3830058, + ) => { + LightWallet::unsafe_from_buffer_testnet(include_bytes!( + "examples/testnet/cbbhrwiilgbrababsshsmtpr/G2f3830058/zingo-wallet.dat" + )) + .await + } + ExampleTestnetWalletSeed::MSKMGDBHOTBPETCJWCSPGOPP( + ExampleMSKMGDBHOTBPETCJWCSPGOPPVersion::Gab72a38b, + ) => { + LightWallet::unsafe_from_buffer_testnet(include_bytes!( + "examples/testnet/mskmgdbhotbpetcjwcspgopp/Gab72a38b/zingo-wallet.dat" + )) + .await + } + ExampleTestnetWalletSeed::MSKMGDBHOTBPETCJWCSPGOPP( + ExampleMSKMGDBHOTBPETCJWCSPGOPPVersion::G93738061a, + ) => { + LightWallet::unsafe_from_buffer_testnet(include_bytes!( + "examples/testnet/mskmgdbhotbpetcjwcspgopp/G93738061a/zingo-wallet.dat" + )) + .await + } + ExampleTestnetWalletSeed::MSKMGDBHOTBPETCJWCSPGOPP( + ExampleMSKMGDBHOTBPETCJWCSPGOPPVersion::Ga74fed621, + ) => { + LightWallet::unsafe_from_buffer_testnet(include_bytes!( + "examples/testnet/mskmgdbhotbpetcjwcspgopp/Ga74fed621/zingo-wallet.dat" + )) + .await + } + }, + ExampleWalletNetwork::Mainnet(example_mainnet_seed) => match example_mainnet_seed { + ExampleMainnetWalletSeed::VTFCORFBCBPCTCFUPMEGMWBP( + ExampleVTFCORFBCBPCTCFUPMEGMWBPVersion::V28, + ) => { + LightWallet::unsafe_from_buffer_mainnet(include_bytes!( + "examples/mainnet/vtfcorfbcbpctcfupmegmwbp/v28/zingo-wallet.dat" + )) + .await + } + ExampleMainnetWalletSeed::HHCCLALTPCCKCSSLPCNETBLR( + ExampleHHCCLALTPCCKCSSLPCNETBLRVersion::Gf0aaf9347, + ) => { + LightWallet::unsafe_from_buffer_mainnet(include_bytes!( + "examples/mainnet/hhcclaltpcckcsslpcnetblr/gf0aaf9347/zingo-wallet.dat" + )) + .await + } + }, + } + } + /// picks the seed (or ufvk) string associated with an example wallet + pub fn example_wallet_base(&self) -> String { + match self { + ExampleWalletNetwork::Regtest(example_regt_seed) => match example_regt_seed { + ExampleRegtestWalletSeed::HMVASMUVWMSSVICHCARBPOCT(_) => { + crate::testvectors::seeds::HOSPITAL_MUSEUM_SEED.to_string() + }, + ExampleRegtestWalletSeed::AAAAAAAAAAAAAAAAAAAAAAAA(_) => { + crate::testvectors::seeds::ABANDON_ART_SEED.to_string() + }, + ExampleRegtestWalletSeed::AADAALACAADAALACAADAALAC(_) => { + "absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic avoid letter advice comic".to_string() + } + }, + ExampleWalletNetwork::Testnet(example_testnet_seed) => match example_testnet_seed { + ExampleTestnetWalletSeed::CBBHRWIILGBRABABSSHSMTPR( + _, + ) => crate::testvectors::seeds::CHIMNEY_BETTER_SEED.to_string(), + ExampleTestnetWalletSeed::MSKMGDBHOTBPETCJWCSPGOPP( + _, + ) => "mobile shuffle keen mother globe desk bless hub oil town begin potato explain table crawl just wild click spring pottery gasp often pill plug".to_string() + }, + ExampleWalletNetwork::Mainnet(example_mainnet_seed) => match example_mainnet_seed { + ExampleMainnetWalletSeed::VTFCORFBCBPCTCFUPMEGMWBP( + _, + ) => "village target fun course orange release female brain cruise birth pet copy trouble common fitness unfold panther man enjoy genuine merry write bulb pledge".to_string(), + ExampleMainnetWalletSeed::HHCCLALTPCCKCSSLPCNETBLR( + _, + ) => "hotel humor crunch crack language awkward lunar term priority critic cushion keep coin sketch soap laugh pretty cement noodle enjoy trip bicycle list return".to_string() + } + } + } + /// picks the first receiver associated with an example wallet + pub fn example_wallet_address(&self, pool: PoolType) -> String { + match pool { + PoolType::Transparent => match self { + ExampleWalletNetwork::Regtest(example_regt_seed) => match example_regt_seed { + ExampleRegtestWalletSeed::HMVASMUVWMSSVICHCARBPOCT(_) => {"tmFLszfkjgim4zoUMAXpuohnFBAKy99rr2i".to_string()}, + ExampleRegtestWalletSeed::AAAAAAAAAAAAAAAAAAAAAAAA(_) => {"tmBsTi2xWTjUdEXnuTceL7fecEQKeWaPDJd".to_string()}, + ExampleRegtestWalletSeed::AADAALACAADAALACAADAALAC(_) => {"tmS9nbexug7uT8x1cMTLP1ABEyKXpMjR5F1".to_string()}, + }, + ExampleWalletNetwork::Testnet(example_test_seed) => match example_test_seed { + ExampleTestnetWalletSeed::CBBHRWIILGBRABABSSHSMTPR(_) => {"tmYd5GP6JxUxTUcz98NLPumEotvaMPaXytz".to_string()}, + ExampleTestnetWalletSeed::MSKMGDBHOTBPETCJWCSPGOPP(_) => {"tmEVmDAnveCakZkvV4a6FT1TfYApTv937E7".to_string()}, + }, + ExampleWalletNetwork::Mainnet(example_mainn_seed) => match example_mainn_seed { + ExampleMainnetWalletSeed::VTFCORFBCBPCTCFUPMEGMWBP(_) => {"t1P8tQtYFLR7TWsqtauc71RGQdqqwfFBbb4".to_string()}, + ExampleMainnetWalletSeed::HHCCLALTPCCKCSSLPCNETBLR(_) => {"t1XnsupYhvhSDSFJ4nzZ2kADhLMR22wg35y".to_string()}, + } + }, + PoolType::Shielded(ShieldedProtocol::Sapling) => match self { + ExampleWalletNetwork::Regtest(example_regt_seed) => match example_regt_seed { + ExampleRegtestWalletSeed::HMVASMUVWMSSVICHCARBPOCT(_) => {"zregtestsapling1fkc26vpg566hgnx33n5uvgye4neuxt4358k68atnx78l5tg2dewdycesmr4m5pn56ffzsa7lyj6".to_string()}, + ExampleRegtestWalletSeed::AAAAAAAAAAAAAAAAAAAAAAAA(_) => {"zregtestsapling1fmq2ufux3gm0v8qf7x585wj56le4wjfsqsj27zprjghntrerntggg507hxh2ydcdkn7sx8kya7p".to_string()}, + ExampleRegtestWalletSeed::AADAALACAADAALACAADAALAC(_) => {"zregtestsapling1lhjvuj4s3ghhccnjaefdzuwp3h3mfluz6tm8h0dsq2ym3f77zsv0wrrszpmaqlezm3kt6ajdvlw".to_string()}, + }, + ExampleWalletNetwork::Testnet(example_test_seed) => match example_test_seed { + ExampleTestnetWalletSeed::CBBHRWIILGBRABABSSHSMTPR(_) => {"ztestsapling1etnl5s47cqves0g5hk2dx5824rme4xv4aeauwzp4d6ys3qxykt5sw5rnaqh9syxry8vgxu60uhj".to_string()}, + ExampleTestnetWalletSeed::MSKMGDBHOTBPETCJWCSPGOPP(_) => {"ztestsapling1h8l5mzlwhmqmd9x7ehquayqckzg6jwa6955f3w9mnkn5p5yfhqy04yz6yjrqfcztxx05xlh3prq".to_string()}, + } + ExampleWalletNetwork::Mainnet(example_test_seed) => match example_test_seed { + ExampleMainnetWalletSeed::VTFCORFBCBPCTCFUPMEGMWBP(_) => {"zs1kgdrzfe6xuq3tg64vnezp3duyp43u7wcpgduqcpwz9wsnfqm4cecafu9qkmpsjtqxzf27n34z9k".to_string()}, + ExampleMainnetWalletSeed::HHCCLALTPCCKCSSLPCNETBLR(_) => {"zs1zgffhwsnh7efu4auv8ql9egteangyferp28rv8r7hmu76u0ee8mthcpflx575emx2zygqcuedzn".to_string()}, + } + }, + PoolType::Shielded(ShieldedProtocol::Orchard) => match self { + ExampleWalletNetwork::Regtest(example_regt_seed) => match example_regt_seed { + ExampleRegtestWalletSeed::HMVASMUVWMSSVICHCARBPOCT(_) => { + "uregtest1wdukkmv5p5n824e8ytnc3m6m77v9vwwl7hcpj0wangf6z23f9x0fnaen625dxgn8cgp67vzw6swuar6uwp3nqywfvvkuqrhdjffxjfg644uthqazrtxhrgwac0a6ujzgwp8y9cwthjeayq8r0q6786yugzzyt9vevxn7peujlw8kp3vf6d8p4fvvpd8qd5p7xt2uagelmtf3vl6w3u8".to_string() + }, + ExampleRegtestWalletSeed::AAAAAAAAAAAAAAAAAAAAAAAA(_) => { + "uregtest1zkuzfv5m3yhv2j4fmvq5rjurkxenxyq8r7h4daun2zkznrjaa8ra8asgdm8wwgwjvlwwrxx7347r8w0ee6dqyw4rufw4wg9djwcr6frzkezmdw6dud3wsm99eany5r8wgsctlxquu009nzd6hsme2tcsk0v3sgjvxa70er7h27z5epr67p5q767s2z5gt88paru56mxpm6pwz0cu35m".to_string() + }, + ExampleRegtestWalletSeed::AADAALACAADAALACAADAALAC(_) => { + "uregtest1qtqr46fwkhmdn336uuyvvxyrv0l7trgc0z9clpryx6vtladnpyt4wvq99p59f4rcyuvpmmd0hm4k5vv6j8edj6n8ltk45sdkptlk7rtzlm4uup4laq8ka8vtxzqemj3yhk6hqhuypupzryhv66w65lah9ms03xa8nref7gux2zzhjnfanxnnrnwscmz6szv2ghrurhu3jsqdx25y2yh".to_string() + }, + }, + ExampleWalletNetwork::Testnet(example_test_seed) => match example_test_seed { + ExampleTestnetWalletSeed::CBBHRWIILGBRABABSSHSMTPR(_) => { + "utest17wwv8nuvdnpjsxtu6ndz6grys5x8wphcwtzmg75wkx607c7cue9qz5kfraqzc7k9dfscmylazj4nkwazjj26s9rhyjxm0dcqm837ykgh2suv0at9eegndh3kvtfjwp3hhhcgk55y9d2ys56zkw8aaamcrv9cy0alj0ndvd0wll4gxhrk9y4yy9q9yg8yssrencl63uznqnkv7mk3w05".to_string() + }, + + ExampleTestnetWalletSeed::MSKMGDBHOTBPETCJWCSPGOPP(_) => { + "utest19zd9laj93deq4lkay48xcfyh0tjec786x6yrng38fp6zusgm0c84h3el99fngh8eks4kxv020r2h2njku6pf69anpqmjq5c3suzcjtlyhvpse0aqje09la48xk6a2cnm822s2yhuzfr47pp4dla9rakdk90g0cee070z57d3trqk87wwj4swz6uf6ts6p5z6lep3xyvueuvt7392tww".to_string() + }, + + }, + ExampleWalletNetwork::Mainnet(example_test_seed) => match example_test_seed { + ExampleMainnetWalletSeed::VTFCORFBCBPCTCFUPMEGMWBP(_) => { + "u1n5zgv8c9px4hfmq7cr9f9t0av6q9nj5dwca9w0z9jxegut65gxs2y4qnx7ppng6k2hyt0asyycqrywalzyasxu2302xt4spfqnkh25nevr3h9exc3clh9tfpr5hyhc9dwee50l0cxm7ajun5xs9ycqhlw8rd39jql8z5zlv9hw4q8azcgpv04dez5547geuvyh8pfzezpw52cg2qknm".to_string() + }, + ExampleMainnetWalletSeed::HHCCLALTPCCKCSSLPCNETBLR(_) => { + "u14lrpa0myuh5ax8dtyaj64jddk8m80nk2wgd3sjlu7g3ejwxs3qkfj5hntakjte8ena3qnk40ht0ats5ad0lcwhjtn9hak6733fdf33fhkl7umgqy2vtcfmhmca9pjdlrsz68euuw06swnl9uzzpadmvztd50xen4ruw738t995x7mhdcx3mjv7eh5hntgtvhtv6vgp9l885eqg6xpm8".to_string() + }, + }, + }, + } + } +} diff --git a/zingolib/src/wallet/disk/testing/examples/mainnet/hhcclaltpcckcsslpcnetblr/gf0aaf9347/zingo-wallet.dat b/zingolib/src/wallet/disk/testing/examples/mainnet/hhcclaltpcckcsslpcnetblr/gf0aaf9347/zingo-wallet.dat new file mode 100644 index 0000000000..97d61e5f45 Binary files /dev/null and b/zingolib/src/wallet/disk/testing/examples/mainnet/hhcclaltpcckcsslpcnetblr/gf0aaf9347/zingo-wallet.dat differ diff --git a/libtonode-tests/tests/data/wallets/missing_data_test/zingo-wallet.dat b/zingolib/src/wallet/disk/testing/examples/mainnet/vtfcorfbcbpctcfupmegmwbp/v28/zingo-wallet.dat similarity index 100% rename from libtonode-tests/tests/data/wallets/missing_data_test/zingo-wallet.dat rename to zingolib/src/wallet/disk/testing/examples/mainnet/vtfcorfbcbpctcfupmegmwbp/v28/zingo-wallet.dat diff --git a/libtonode-tests/tests/data/wallets/v26/202302_release/regtest/sap_only/zingo-wallet.dat b/zingolib/src/wallet/disk/testing/examples/regtest/aaaaaaaaaaaaaaaaaaaaaaaa/v26/zingo-wallet.dat similarity index 100% rename from libtonode-tests/tests/data/wallets/v26/202302_release/regtest/sap_only/zingo-wallet.dat rename to zingolib/src/wallet/disk/testing/examples/regtest/aaaaaaaaaaaaaaaaaaaaaaaa/v26/zingo-wallet.dat diff --git a/libtonode-tests/tests/data/wallets/v26/202302_release/regtest/orch_and_sapl/zingo-wallet.dat b/zingolib/src/wallet/disk/testing/examples/regtest/aadaalacaadaalacaadaalac/orch_and_sapl/zingo-wallet.dat similarity index 100% rename from libtonode-tests/tests/data/wallets/v26/202302_release/regtest/orch_and_sapl/zingo-wallet.dat rename to zingolib/src/wallet/disk/testing/examples/regtest/aadaalacaadaalacaadaalac/orch_and_sapl/zingo-wallet.dat diff --git a/libtonode-tests/tests/data/wallets/v26/202302_release/regtest/orch_only/zingo-wallet.dat b/zingolib/src/wallet/disk/testing/examples/regtest/aadaalacaadaalacaadaalac/orch_only/zingo-wallet.dat similarity index 100% rename from libtonode-tests/tests/data/wallets/v26/202302_release/regtest/orch_only/zingo-wallet.dat rename to zingolib/src/wallet/disk/testing/examples/regtest/aadaalacaadaalacaadaalac/orch_only/zingo-wallet.dat diff --git a/zingo-testvectors/old_wallet_reorg_test_wallet/zingo-wallet.dat b/zingolib/src/wallet/disk/testing/examples/regtest/hmvasmuvwmssvichcarbpoct/v27/zingo-wallet.dat similarity index 100% rename from zingo-testvectors/old_wallet_reorg_test_wallet/zingo-wallet.dat rename to zingolib/src/wallet/disk/testing/examples/regtest/hmvasmuvwmssvichcarbpoct/v27/zingo-wallet.dat diff --git a/zingolib/src/wallet/disk/testing/examples/testnet/cbbhrwiilgbrababsshsmtpr/G2f3830058/zingo-wallet.dat b/zingolib/src/wallet/disk/testing/examples/testnet/cbbhrwiilgbrababsshsmtpr/G2f3830058/zingo-wallet.dat new file mode 100644 index 0000000000..851c287d35 Binary files /dev/null and b/zingolib/src/wallet/disk/testing/examples/testnet/cbbhrwiilgbrababsshsmtpr/G2f3830058/zingo-wallet.dat differ diff --git a/libtonode-tests/tests/zingo-wallet-v26.dat b/zingolib/src/wallet/disk/testing/examples/testnet/cbbhrwiilgbrababsshsmtpr/v26/zingo-wallet.dat similarity index 100% rename from libtonode-tests/tests/zingo-wallet-v26.dat rename to zingolib/src/wallet/disk/testing/examples/testnet/cbbhrwiilgbrababsshsmtpr/v26/zingo-wallet.dat diff --git a/libtonode-tests/tests/zingo-wallet-v26-2.dat b/zingolib/src/wallet/disk/testing/examples/testnet/cbbhrwiilgbrababsshsmtpr/v27/zingo-wallet.dat similarity index 100% rename from libtonode-tests/tests/zingo-wallet-v26-2.dat rename to zingolib/src/wallet/disk/testing/examples/testnet/cbbhrwiilgbrababsshsmtpr/v27/zingo-wallet.dat diff --git a/libtonode-tests/tests/zingo-wallet-v28.dat b/zingolib/src/wallet/disk/testing/examples/testnet/cbbhrwiilgbrababsshsmtpr/v28/zingo-wallet.dat similarity index 100% rename from libtonode-tests/tests/zingo-wallet-v28.dat rename to zingolib/src/wallet/disk/testing/examples/testnet/cbbhrwiilgbrababsshsmtpr/v28/zingo-wallet.dat diff --git a/zingolib/src/wallet/disk/testing/examples/testnet/mskmgdbhotbpetcjwcspgopp/G93738061a/zingo-wallet.dat b/zingolib/src/wallet/disk/testing/examples/testnet/mskmgdbhotbpetcjwcspgopp/G93738061a/zingo-wallet.dat new file mode 100644 index 0000000000..e62910cc22 Binary files /dev/null and b/zingolib/src/wallet/disk/testing/examples/testnet/mskmgdbhotbpetcjwcspgopp/G93738061a/zingo-wallet.dat differ diff --git a/zingolib/src/wallet/disk/testing/examples/testnet/mskmgdbhotbpetcjwcspgopp/Ga74fed621/zingo-wallet.dat b/zingolib/src/wallet/disk/testing/examples/testnet/mskmgdbhotbpetcjwcspgopp/Ga74fed621/zingo-wallet.dat new file mode 100644 index 0000000000..b9481078d1 Binary files /dev/null and b/zingolib/src/wallet/disk/testing/examples/testnet/mskmgdbhotbpetcjwcspgopp/Ga74fed621/zingo-wallet.dat differ diff --git a/zingolib/src/wallet/disk/testing/examples/testnet/mskmgdbhotbpetcjwcspgopp/Gab72a38b/zingo-wallet.dat b/zingolib/src/wallet/disk/testing/examples/testnet/mskmgdbhotbpetcjwcspgopp/Gab72a38b/zingo-wallet.dat new file mode 100644 index 0000000000..15bb72c4d3 Binary files /dev/null and b/zingolib/src/wallet/disk/testing/examples/testnet/mskmgdbhotbpetcjwcspgopp/Gab72a38b/zingo-wallet.dat differ diff --git a/zingolib/src/wallet/disk/testing/tests.rs b/zingolib/src/wallet/disk/testing/tests.rs new file mode 100644 index 0000000000..a1ca35ded6 --- /dev/null +++ b/zingolib/src/wallet/disk/testing/tests.rs @@ -0,0 +1,308 @@ +use bip0039::Mnemonic; + +use zcash_client_backend::PoolType; +use zcash_client_backend::ShieldedProtocol; +use zcash_keys::keys::Era; + +use crate::lightclient::LightClient; +use crate::wallet::keys::unified::UnifiedKeyStore; + +use super::super::LightWallet; +use super::assert_wallet_capability_matches_seed; + +use super::examples::ExampleWalletNetwork; +use super::examples::ExampleWalletNetwork::Mainnet; +use super::examples::ExampleWalletNetwork::Regtest; +use super::examples::ExampleWalletNetwork::Testnet; + +use super::examples::ExampleMainnetWalletSeed::HHCCLALTPCCKCSSLPCNETBLR; +use super::examples::ExampleMainnetWalletSeed::VTFCORFBCBPCTCFUPMEGMWBP; +use super::examples::ExampleRegtestWalletSeed::AAAAAAAAAAAAAAAAAAAAAAAA; +use super::examples::ExampleRegtestWalletSeed::AADAALACAADAALACAADAALAC; +use super::examples::ExampleRegtestWalletSeed::HMVASMUVWMSSVICHCARBPOCT; +use super::examples::ExampleTestnetWalletSeed::CBBHRWIILGBRABABSSHSMTPR; +use super::examples::ExampleTestnetWalletSeed::MSKMGDBHOTBPETCJWCSPGOPP; + +use super::examples::ExampleAAAAAAAAAAAAAAAAAAAAAAAAVersion; +use super::examples::ExampleAADAALACAADAALACAADAALACVersion; +use super::examples::ExampleCBBHRWIILGBRABABSSHSMTPRVersion; +use super::examples::ExampleHHCCLALTPCCKCSSLPCNETBLRVersion; +use super::examples::ExampleHMVASMUVWMSSVICHCARBPOCTVersion; +use super::examples::ExampleMSKMGDBHOTBPETCJWCSPGOPPVersion; +use super::examples::ExampleVTFCORFBCBPCTCFUPMEGMWBPVersion; + +// moving toward completeness: each of these tests should assert everything known about the LightWallet without network. + +impl ExampleWalletNetwork { + /// this is enough data to restore wallet from! thus, it is the bronze test for backward compatibility + async fn load_example_wallet_with_verification(&self) -> LightWallet { + let wallet = self.load_example_wallet().await; + assert_wallet_capability_matches_seed(&wallet, self.example_wallet_base()).await; + for pool in [ + PoolType::Transparent, + PoolType::Shielded(ShieldedProtocol::Sapling), + PoolType::Shielded(ShieldedProtocol::Orchard), + ] { + assert_eq!( + wallet + .get_first_address(pool) + .expect("can find the first address"), + self.example_wallet_address(pool) + ); + } + wallet + } +} + +#[tokio::test] +async fn verify_example_wallet_regtest_aaaaaaaaaaaaaaaaaaaaaaaa_v26() { + Regtest(AAAAAAAAAAAAAAAAAAAAAAAA( + ExampleAAAAAAAAAAAAAAAAAAAAAAAAVersion::V26, + )) + .load_example_wallet_with_verification() + .await; +} +#[tokio::test] +async fn verify_example_wallet_regtest_aadaalacaadaalacaadaalac_orch_and_sapl() { + Regtest(AADAALACAADAALACAADAALAC( + ExampleAADAALACAADAALACAADAALACVersion::OrchAndSapl, + )) + .load_example_wallet_with_verification() + .await; +} +#[tokio::test] +async fn verify_example_wallet_regtest_aadaalacaadaalacaadaalac_orch_only() { + Regtest(AADAALACAADAALACAADAALAC( + ExampleAADAALACAADAALACAADAALACVersion::OrchOnly, + )) + .load_example_wallet_with_verification() + .await; +} +#[tokio::test] +async fn verify_example_wallet_regtest_hmvasmuvwmssvichcarbpoct_v27() { + Regtest(HMVASMUVWMSSVICHCARBPOCT( + ExampleHMVASMUVWMSSVICHCARBPOCTVersion::V27, + )) + .load_example_wallet_with_verification() + .await; +} +/// unlike other, more basic tests, this test also checks number of addresses and balance +#[tokio::test] +async fn verify_example_wallet_testnet_cbbhrwiilgbrababsshsmtpr_v26() { + let wallet = Testnet(CBBHRWIILGBRABABSSHSMTPR( + ExampleCBBHRWIILGBRABABSSHSMTPRVersion::V26, + )) + .load_example_wallet_with_verification() + .await; + + loaded_wallet_assert( + wallet, + crate::testvectors::seeds::CHIMNEY_BETTER_SEED.to_string(), + 0, + 3, + ) + .await; +} +/// unlike other, more basic tests, this test also checks number of addresses and balance +#[ignore = "test proves note has no index bug is a breaker"] +#[tokio::test] +async fn verify_example_wallet_testnet_cbbhrwiilgbrababsshsmtpr_v27() { + let wallet = Testnet(CBBHRWIILGBRABABSSHSMTPR( + ExampleCBBHRWIILGBRABABSSHSMTPRVersion::V27, + )) + .load_example_wallet_with_verification() + .await; + + loaded_wallet_assert( + wallet, + crate::testvectors::seeds::CHIMNEY_BETTER_SEED.to_string(), + 10177826, + 1, + ) + .await; +} +#[tokio::test] +async fn verify_example_wallet_testnet_cbbhrwiilgbrababsshsmtpr_v28() { + Testnet(CBBHRWIILGBRABABSSHSMTPR( + ExampleCBBHRWIILGBRABABSSHSMTPRVersion::V28, + )) + .load_example_wallet_with_verification() + .await; +} +#[tokio::test] +async fn verify_example_wallet_testnet_cbbhrwiilgbrababsshsmtpr_g2f3830058() { + Testnet(CBBHRWIILGBRABABSSHSMTPR( + ExampleCBBHRWIILGBRABABSSHSMTPRVersion::G2f3830058, + )) + .load_example_wallet_with_verification() + .await; +} +#[tokio::test] +async fn verify_example_wallet_testnet_mskmgdbhotbpetcjwcspgopp_gab72a38b() { + Testnet(MSKMGDBHOTBPETCJWCSPGOPP( + ExampleMSKMGDBHOTBPETCJWCSPGOPPVersion::Gab72a38b, + )) + .load_example_wallet_with_verification() + .await; +} +#[tokio::test] +async fn verify_example_wallet_testnet_mskmgdbhotbpetcjwcspgopp_g93738061a() { + Testnet(MSKMGDBHOTBPETCJWCSPGOPP( + ExampleMSKMGDBHOTBPETCJWCSPGOPPVersion::G93738061a, + )) + .load_example_wallet_with_verification() + .await; +} +#[tokio::test] +async fn verify_example_wallet_testnet_mskmgdbhotbpetcjwcspgopp_ga74fed621() { + Testnet(MSKMGDBHOTBPETCJWCSPGOPP( + ExampleMSKMGDBHOTBPETCJWCSPGOPPVersion::Ga74fed621, + )) + .load_example_wallet_with_verification() + .await; +} +#[tokio::test] +async fn verify_example_wallet_mainnet_vtfcorfbcbpctcfupmegmwbp_v28() { + Mainnet(VTFCORFBCBPCTCFUPMEGMWBP( + ExampleVTFCORFBCBPCTCFUPMEGMWBPVersion::V28, + )) + .load_example_wallet_with_verification() + .await; +} +#[tokio::test] +async fn verify_example_wallet_mainnet_hhcclaltpcckcsslpcnetblr_gf0aaf9347() { + Mainnet(HHCCLALTPCCKCSSLPCNETBLR( + ExampleHHCCLALTPCCKCSSLPCNETBLRVersion::Gf0aaf9347, + )) + .load_example_wallet_with_verification() + .await; +} + +async fn loaded_wallet_assert( + wallet: LightWallet, + expected_seed_phrase: String, + expected_balance: u64, + expected_num_addresses: usize, +) { + assert_wallet_capability_matches_seed(&wallet, expected_seed_phrase).await; + + let wc = wallet.wallet_capability(); + assert_eq!(wc.addresses().len(), expected_num_addresses); + for addr in wc.addresses().iter() { + assert!(addr.orchard().is_some()); + assert!(addr.sapling().is_some()); + assert!(addr.transparent().is_some()); + } + + let client = crate::lightclient::LightClient::create_from_wallet_async(wallet) + .await + .unwrap(); + let balance = client.do_balance().await; + assert_eq!(balance.orchard_balance, Some(expected_balance)); + if expected_balance > 0 { + crate::testutils::lightclient::from_inputs::quick_send( + &client, + vec![( + &crate::get_base_address_macro!(client, "sapling"), + 11011, + None, + )], + ) + .await + .unwrap(); + client.do_sync(true).await.unwrap(); + crate::testutils::lightclient::from_inputs::quick_send( + &client, + vec![( + &crate::get_base_address_macro!(client, "transparent"), + 28000, + None, + )], + ) + .await + .unwrap(); + } +} + +// todo: proptest enum +#[tokio::test] +async fn reload_wallet_from_buffer() { + use crate::testvectors::seeds::CHIMNEY_BETTER_SEED; + use crate::wallet::WalletBase; + use crate::wallet::WalletCapability; + + let mid_wallet = Testnet(CBBHRWIILGBRABABSSHSMTPR( + ExampleCBBHRWIILGBRABABSSHSMTPRVersion::V28, + )) + .load_example_wallet_with_verification() + .await; + + let mid_client = LightClient::create_from_wallet_async(mid_wallet) + .await + .unwrap(); + let mid_buffer = mid_client.export_save_buffer_async().await.unwrap(); + let wallet = LightWallet::read_internal( + &mid_buffer[..], + &mid_client.wallet.transaction_context.config, + ) + .await + .map_err(|e| format!("Cannot deserialize rebuffered LightWallet: {}", e)) + .unwrap(); + let expected_mnemonic = ( + Mnemonic::from_phrase(CHIMNEY_BETTER_SEED.to_string()).unwrap(), + 0, + ); + + let expected_wc = WalletCapability::new_from_phrase( + &mid_client.wallet.transaction_context.config, + &expected_mnemonic.0, + expected_mnemonic.1, + ) + .unwrap(); + let wc = wallet.wallet_capability(); + + let UnifiedKeyStore::Spend(usk) = wc.unified_key_store() else { + panic!("should be spending key!") + }; + let UnifiedKeyStore::Spend(expected_usk) = expected_wc.unified_key_store() else { + panic!("should be spending key!") + }; + + assert_eq!( + usk.to_bytes(Era::Orchard), + expected_usk.to_bytes(Era::Orchard) + ); + assert_eq!(usk.orchard().to_bytes(), expected_usk.orchard().to_bytes()); + assert_eq!(usk.sapling().to_bytes(), expected_usk.sapling().to_bytes()); + assert_eq!( + usk.transparent().to_bytes(), + expected_usk.transparent().to_bytes() + ); + + assert_eq!(wc.addresses().len(), 3); + for addr in wc.addresses().iter() { + assert!(addr.orchard().is_some()); + assert!(addr.sapling().is_some()); + assert!(addr.transparent().is_some()); + } + + let ufvk = usk.to_unified_full_viewing_key(); + let ufvk_string = ufvk.encode(&wallet.transaction_context.config.chain); + let ufvk_base = WalletBase::Ufvk(ufvk_string.clone()); + let view_wallet = LightWallet::new( + wallet.transaction_context.config.clone(), + ufvk_base, + wallet.get_birthday().await, + ) + .unwrap(); + let v_wc = view_wallet.wallet_capability(); + let UnifiedKeyStore::View(v_ufvk) = v_wc.unified_key_store() else { + panic!("should be viewing key!"); + }; + let v_ufvk_string = v_ufvk.encode(&wallet.transaction_context.config.chain); + assert_eq!(ufvk_string, v_ufvk_string); + + let client = LightClient::create_from_wallet_async(wallet).await.unwrap(); + let balance = client.do_balance().await; + assert_eq!(balance.orchard_balance, Some(10342837)); +} diff --git a/zingolib/src/wallet/error.rs b/zingolib/src/wallet/error.rs index 9217637453..7e30a153f6 100644 --- a/zingolib/src/wallet/error.rs +++ b/zingolib/src/wallet/error.rs @@ -1,6 +1,7 @@ //! Errors for [`crate::wallet`] and sub-modules use thiserror::Error; +use zcash_keys::keys::DerivationError; use crate::wallet::data::OutgoingTxData; @@ -41,3 +42,40 @@ pub enum BalanceError { #[error("conversion failed. {0}")] ConversionFailed(#[from] crate::utils::error::ConversionError), } + +/// Errors associated with balance key derivation +#[derive(Debug, Error)] +pub enum KeyError { + /// Error asociated with standard IO + #[error("{0}")] + IoError(#[from] std::io::Error), + /// Invalid account ID + #[error("Account ID should be at most 31 bits")] + InvalidAccountId(#[from] zip32::TryFromIntError), + /// Key derivation failed + // TODO: add std::Error to zcash_keys::keys::DerivationError in LRZ fork and add thiserror #[from] macro + #[error("Key derivation failed")] + KeyDerivationError(DerivationError), + /// Key decoding failed + // TODO: add std::Error to zcash_keys::keys::DecodingError in LRZ fork and add thiserror #[from] macro + #[error("Key decoding failed")] + KeyDecodingError, + /// Key parsing failed + #[error("Key parsing failed. {0}")] + KeyParseError(#[from] zcash_address::unified::ParseError), + /// No spend capability + #[error("No spend capability")] + NoSpendCapability, + /// No view capability + #[error("No view capability")] + NoViewCapability, + /// Invalid non-hardened child indexes + #[error("Outside range of non-hardened child indexes")] + InvalidNonHardenedChildIndex, + /// Network mismatch + #[error("Decoded unified full viewing key does not match current network")] + NetworkMismatch, + /// Invalid format + #[error("Viewing keys must be imported in the unified format")] + InvalidFormat, +} diff --git a/zingolib/src/wallet/keys.rs b/zingolib/src/wallet/keys.rs index 7d1e326d78..66667ec351 100644 --- a/zingolib/src/wallet/keys.rs +++ b/zingolib/src/wallet/keys.rs @@ -1,8 +1,8 @@ //! TODO: Add Mod Description Here! //! In all cases in this file "external_version" refers to a serialization version that is interpreted //! from a source outside of the code-base e.g. a wallet-file. +use crate::config::{ChainType, ZingoConfig}; use base58::ToBase58; -use ripemd160::Digest; use sapling_crypto::{ zip32::{DiversifiableFullViewingKey, ExtendedSpendingKey}, PaymentAddress, @@ -12,15 +12,14 @@ use zcash_client_backend::address; use zcash_primitives::{ consensus::NetworkConstants, legacy::TransparentAddress, zip32::ChildIndex, }; -use zingoconfig::{ChainType, ZingoConfig}; -pub mod extended_transparent; +pub mod legacy; pub mod unified; /// Sha256(Sha256(value)) pub fn double_sha256(payload: &[u8]) -> Vec { - let h1 = Sha256::digest(payload); - let h2 = Sha256::digest(&h1); + let h1 = ::digest(payload); + let h2 = ::digest(h1.as_slice()); h2.to_vec() } diff --git a/zingolib/src/wallet/keys/legacy.rs b/zingolib/src/wallet/keys/legacy.rs new file mode 100644 index 0000000000..d523b6fc13 --- /dev/null +++ b/zingolib/src/wallet/keys/legacy.rs @@ -0,0 +1,195 @@ +//! Module for legacy code associated with wallet keys required for backward-compatility with old wallet versions + +use std::io::{self, Read, Write}; + +use bip32::ExtendedPublicKey; +use byteorder::{ReadBytesExt, WriteBytesExt}; +use bytes::LittleEndian; +use zcash_address::unified::Typecode; +use zcash_encoding::CompactSize; +use zcash_keys::keys::{Era, UnifiedFullViewingKey, UnifiedSpendingKey}; +use zcash_primitives::legacy::{ + keys::{AccountPubKey, NonHardenedChildIndex}, + TransparentAddress, +}; + +use crate::wallet::{error::KeyError, traits::ReadableWriteable}; + +use super::unified::{KEY_TYPE_EMPTY, KEY_TYPE_SPEND, KEY_TYPE_VIEW}; + +pub mod extended_transparent; + +/// TODO: Add Doc Comment Here! +#[derive(Clone, Debug)] +#[non_exhaustive] +pub enum Capability { + /// TODO: Add Doc Comment Here! + None, + /// TODO: Add Doc Comment Here! + View(ViewingKeyType), + /// TODO: Add Doc Comment Here! + Spend(SpendKeyType), +} + +impl ReadableWriteable<(), ()> for Capability +where + V: ReadableWriteable<(), ()>, + S: ReadableWriteable<(), ()>, +{ + const VERSION: u8 = 1; + fn read(mut reader: R, _input: ()) -> io::Result { + let _version = Self::get_version(&mut reader)?; + let capability_type = reader.read_u8()?; + Ok(match capability_type { + KEY_TYPE_EMPTY => Capability::None, + KEY_TYPE_VIEW => Capability::View(V::read(&mut reader, ())?), + KEY_TYPE_SPEND => Capability::Spend(S::read(&mut reader, ())?), + x => { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("Unknown wallet Capability type: {}", x), + )) + } + }) + } + + fn write(&self, mut writer: W, _input: ()) -> io::Result<()> { + writer.write_u8(Self::VERSION)?; + match self { + Capability::None => writer.write_u8(KEY_TYPE_EMPTY), + Capability::View(vk) => { + writer.write_u8(KEY_TYPE_VIEW)?; + vk.write(&mut writer, ()) + } + Capability::Spend(sk) => { + writer.write_u8(KEY_TYPE_SPEND)?; + sk.write(&mut writer, ()) + } + } + } +} + +pub(crate) fn legacy_fvks_to_ufvk( + orchard_fvk: Option<&orchard::keys::FullViewingKey>, + sapling_fvk: Option<&sapling_crypto::zip32::DiversifiableFullViewingKey>, + transparent_fvk: Option<&extended_transparent::ExtendedPubKey>, + parameters: &P, +) -> Result { + use zcash_address::unified::Encoding; + + let mut fvks = Vec::new(); + if let Some(fvk) = orchard_fvk { + fvks.push(zcash_address::unified::Fvk::Orchard(fvk.to_bytes())); + } + if let Some(fvk) = sapling_fvk { + fvks.push(zcash_address::unified::Fvk::Sapling(fvk.to_bytes())); + } + if let Some(fvk) = transparent_fvk { + let mut fvk_bytes = [0u8; 65]; + fvk_bytes[0..32].copy_from_slice(&fvk.chain_code[..]); + fvk_bytes[32..65].copy_from_slice(&fvk.public_key.serialize()[..]); + fvks.push(zcash_address::unified::Fvk::P2pkh(fvk_bytes)); + } + + let ufvk = zcash_address::unified::Ufvk::try_from_items(fvks)?; + + UnifiedFullViewingKey::decode(parameters, &ufvk.encode(¶meters.network_type())) + .map_err(|_| KeyError::KeyDecodingError) +} + +pub(crate) fn legacy_sks_to_usk( + orchard_key: &orchard::keys::SpendingKey, + sapling_key: &sapling_crypto::zip32::ExtendedSpendingKey, + transparent_key: &extended_transparent::ExtendedPrivKey, +) -> Result { + let mut usk_bytes = vec![]; + + // hard-coded Orchard Era ID due to `id()` being a private fn + usk_bytes.write_u32::(0xc2d6_d0b4)?; + + CompactSize::write( + &mut usk_bytes, + usize::try_from(Typecode::Orchard).expect("typecode to usize should not fail"), + )?; + let orchard_key_bytes = orchard_key.to_bytes(); + CompactSize::write(&mut usk_bytes, orchard_key_bytes.len())?; + usk_bytes.write_all(orchard_key_bytes)?; + + CompactSize::write( + &mut usk_bytes, + usize::try_from(Typecode::Sapling).expect("typecode to usize should not fail"), + )?; + let sapling_key_bytes = sapling_key.to_bytes(); + CompactSize::write(&mut usk_bytes, sapling_key_bytes.len())?; + usk_bytes.write_all(&sapling_key_bytes)?; + + // the following code performs the same operations for calling `to_bytes()` on an AccountPrivKey in LRZ + let prefix = bip32::Prefix::XPRV; + let mut chain_code = [0u8; 32]; + chain_code.copy_from_slice(&transparent_key.chain_code); + let attrs = bip32::ExtendedKeyAttrs { + depth: 4, + parent_fingerprint: [0xff, 0xff, 0xff, 0xff], + child_number: bip32::ChildNumber::new(0, true).expect("correct"), + chain_code, + }; + // Add leading `0` byte + let mut key_bytes = [0u8; 33]; + key_bytes[1..].copy_from_slice(transparent_key.private_key.as_ref()); + + let extended_key = bip32::ExtendedKey { + prefix, + attrs, + key_bytes, + }; + + let xprv_encoded = extended_key.to_string(); + let account_tkey_bytes = bs58::decode(xprv_encoded) + .with_check(None) + .into_vec() + .expect("correct") + .split_off(bip32::Prefix::LENGTH); + + CompactSize::write( + &mut usk_bytes, + usize::try_from(Typecode::P2pkh).expect("typecode to usize should not fail"), + )?; + CompactSize::write(&mut usk_bytes, account_tkey_bytes.len())?; + usk_bytes.write_all(&account_tkey_bytes)?; + + UnifiedSpendingKey::from_bytes(Era::Orchard, &usk_bytes).map_err(|_| KeyError::KeyDecodingError) +} + +/// Generates a transparent address from legacy key +/// +/// Legacy key is a key used ONLY during wallet load for wallet versions <29 +/// This legacy key is already derived to the external scope so should only derive a child at the `address_index` +/// and use this child to derive the transparent address +#[allow(deprecated)] +pub(crate) fn generate_transparent_address_from_legacy_key( + external_pubkey: &AccountPubKey, + address_index: NonHardenedChildIndex, +) -> Result { + let external_pubkey_bytes = external_pubkey.serialize(); + + let mut chain_code = [0u8; 32]; + chain_code.copy_from_slice(&external_pubkey_bytes[..32]); + let public_key = secp256k1::PublicKey::from_slice(&external_pubkey_bytes[32..])?; + + let extended_pubkey = ExtendedPublicKey::new( + public_key, + bip32::ExtendedKeyAttrs { + depth: 4, + parent_fingerprint: [0xff, 0xff, 0xff, 0xff], + child_number: bip32::ChildNumber::new(0, true) + .expect("hard-coded index of 0 is not larger than the hardened bit"), + chain_code, + }, + ); + + // address generation copied from IncomingViewingKey::derive_address in LRZ + let child_key = extended_pubkey.derive_child(address_index.into())?; + Ok(zcash_primitives::legacy::keys::pubkey_to_address( + child_key.public_key(), + )) +} diff --git a/zingolib/src/wallet/keys/extended_transparent.rs b/zingolib/src/wallet/keys/legacy/extended_transparent.rs similarity index 92% rename from zingolib/src/wallet/keys/extended_transparent.rs rename to zingolib/src/wallet/keys/legacy/extended_transparent.rs index 3f04e14e46..8e6c0f8635 100644 --- a/zingolib/src/wallet/keys/extended_transparent.rs +++ b/zingolib/src/wallet/keys/legacy/extended_transparent.rs @@ -1,13 +1,13 @@ //! TODO: Add Mod Discription Here! use std::io; +use zcash_primitives::consensus::NetworkConstants; +use crate::config::ZingoConfig; use byteorder::{ReadBytesExt, WriteBytesExt}; use lazy_static::lazy_static; use ring::hmac::{self, Context, Key}; use secp256k1::{Error, PublicKey, Secp256k1, SecretKey, SignOnly}; use zcash_encoding::Vector; -use zcash_primitives::consensus::NetworkConstants; -use zingoconfig::ZingoConfig; use crate::wallet::traits::ReadableWriteable; @@ -152,7 +152,7 @@ impl ExtendedPrivKey { } } -impl ReadableWriteable<()> for SecretKey { +impl ReadableWriteable for SecretKey { const VERSION: u8 = 0; // not applicable fn read(mut reader: R, _: ()) -> std::io::Result { let mut secret_key_bytes = [0; 32]; @@ -161,12 +161,12 @@ impl ReadableWriteable<()> for SecretKey { .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string())) } - fn write(&self, mut writer: W) -> std::io::Result<()> { + fn write(&self, mut writer: W, _input: ()) -> std::io::Result<()> { writer.write(&self.secret_bytes()).map(|_| ()) } } -impl ReadableWriteable<()> for ExtendedPrivKey { +impl ReadableWriteable for ExtendedPrivKey { const VERSION: u8 = 1; fn read(mut reader: R, _: ()) -> std::io::Result { @@ -179,9 +179,9 @@ impl ReadableWriteable<()> for ExtendedPrivKey { }) } - fn write(&self, mut writer: W) -> std::io::Result<()> { + fn write(&self, mut writer: W, _input: ()) -> std::io::Result<()> { writer.write_u8(Self::VERSION)?; - self.private_key.write(&mut writer)?; + self.private_key.write(&mut writer, ())?; Vector::write(&mut writer, &self.chain_code, |w, byte| w.write_u8(*byte))?; Ok(()) } @@ -225,7 +225,7 @@ impl ExtendedPubKey { } } -impl ReadableWriteable<()> for PublicKey { +impl ReadableWriteable for PublicKey { const VERSION: u8 = 0; // not applicable fn read(mut reader: R, _: ()) -> std::io::Result { let mut public_key_bytes = [0; 33]; @@ -234,15 +234,15 @@ impl ReadableWriteable<()> for PublicKey { .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string())) } - fn write(&self, mut writer: W) -> std::io::Result<()> { + fn write(&self, mut writer: W, _input: ()) -> std::io::Result<()> { writer.write(&self.serialize()).map(|_| ()) } } -impl ReadableWriteable<()> for ExtendedPubKey { +impl ReadableWriteable for ExtendedPubKey { const VERSION: u8 = 1; - fn read(mut reader: R, _: ()) -> std::io::Result { + fn read(mut reader: R, _input: ()) -> std::io::Result { Self::get_version(&mut reader)?; let public_key = PublicKey::read(&mut reader, ())?; let chain_code = Vector::read(&mut reader, |r| r.read_u8())?; @@ -252,9 +252,9 @@ impl ReadableWriteable<()> for ExtendedPubKey { }) } - fn write(&self, mut writer: W) -> std::io::Result<()> { + fn write(&self, mut writer: W, _input: ()) -> std::io::Result<()> { writer.write_u8(Self::VERSION)?; - self.public_key.write(&mut writer)?; + self.public_key.write(&mut writer, ())?; Vector::write(&mut writer, &self.chain_code, |w, byte| w.write_u8(*byte))?; Ok(()) } diff --git a/zingolib/src/wallet/keys/unified.rs b/zingolib/src/wallet/keys/unified.rs index 47d7bdfac7..87dccafb71 100644 --- a/zingolib/src/wallet/keys/unified.rs +++ b/zingolib/src/wallet/keys/unified.rs @@ -1,4 +1,5 @@ -//! TODO: Add Mod Discription Here! +//! TODO: Add Mod Description Here! + use std::sync::atomic; use std::{ collections::{HashMap, HashSet}, @@ -8,95 +9,244 @@ use std::{ use std::{marker::PhantomData, sync::Arc}; use append_only_vec::AppendOnlyVec; -use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; +use bip0039::Mnemonic; +use byteorder::{ReadBytesExt, WriteBytesExt}; +use bytes::LittleEndian; +use getset::{Getters, Setters}; + use orchard::note_encryption::OrchardDomain; use sapling_crypto::note_encryption::SaplingDomain; -use zcash_primitives::consensus::{BranchId, NetworkConstants, Parameters}; -use zcash_primitives::zip339::Mnemonic; - -use secp256k1::SecretKey; -use zcash_address::unified::{Container, Encoding, Typecode, Ufvk}; +use zcash_address::unified::{Encoding as _, Ufvk}; use zcash_client_backend::address::UnifiedAddress; use zcash_client_backend::keys::{Era, UnifiedSpendingKey}; +use zcash_client_backend::wallet::TransparentAddressMetadata; use zcash_encoding::{CompactSize, Vector}; -use zcash_primitives::zip32::AccountId; -use zcash_primitives::{legacy::TransparentAddress, zip32::DiversifierIndex}; -use zingoconfig::ZingoConfig; +use zcash_keys::{ + encoding::AddressCodec, + keys::{DerivationError, UnifiedFullViewingKey}, +}; +use zcash_primitives::consensus::{NetworkConstants, Parameters}; +use zcash_primitives::legacy::{ + keys::{AccountPubKey, IncomingViewingKey, NonHardenedChildIndex}, + TransparentAddress, +}; +use zcash_primitives::zip32::{AccountId, DiversifierIndex}; +use crate::wallet::error::KeyError; use crate::wallet::traits::{DomainWalletExt, ReadableWriteable, Recipient}; - -use super::{ - extended_transparent::{ExtendedPrivKey, ExtendedPubKey, KeyIndex}, - get_zaddr_from_bip39seed, ToBase58Check, +use crate::{ + config::{ChainType, ZingoConfig}, + wallet::data::new_persistent_ephemeral_address, }; -/// TODO: Add Doc Comment Here! -#[derive(Clone, Debug)] -#[non_exhaustive] -pub enum Capability { - /// TODO: Add Doc Comment Here! - None, - /// TODO: Add Doc Comment Here! - View(ViewingKeyType), - /// TODO: Add Doc Comment Here! - Spend(SpendKeyType), +use super::legacy::{generate_transparent_address_from_legacy_key, legacy_sks_to_usk, Capability}; +use super::ToBase58Check; + +pub(crate) const KEY_TYPE_EMPTY: u8 = 0; +pub(crate) const KEY_TYPE_VIEW: u8 = 1; +pub(crate) const KEY_TYPE_SPEND: u8 = 2; + +/// In-memory store for wallet spending or viewing keys +#[derive(Debug)] +pub enum UnifiedKeyStore { + /// Wallet with spend capability + Spend(Box), + /// Wallet with view capability + View(Box), + /// Wallet with no keys + Empty, } -impl Capability { - /// TODO: Add Doc Comment Here! - pub fn can_spend(&self) -> bool { - matches!(self, Capability::Spend(_)) +impl UnifiedKeyStore { + /// Returns true if [`UnifiedKeyStore`] is of `Spend` variant + pub fn is_spending_key(&self) -> bool { + matches!(self, UnifiedKeyStore::Spend(_)) } - /// TODO: Add Doc Comment Here! - pub fn can_view(&self) -> bool { + /// Returns true if [`UnifiedKeyStore`] is of `Spend` variant + pub fn is_empty(&self) -> bool { + matches!(self, UnifiedKeyStore::Empty) + } +} + +impl ReadableWriteable for UnifiedKeyStore { + const VERSION: u8 = 0; + fn read(mut reader: R, input: ChainType) -> io::Result { + let _version = Self::get_version(&mut reader)?; + let key_type = reader.read_u8()?; + Ok(match key_type { + KEY_TYPE_SPEND => { + UnifiedKeyStore::Spend(Box::new(UnifiedSpendingKey::read(reader, ())?)) + } + KEY_TYPE_VIEW => { + UnifiedKeyStore::View(Box::new(UnifiedFullViewingKey::read(reader, input)?)) + } + KEY_TYPE_EMPTY => UnifiedKeyStore::Empty, + x => { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("Unknown key type: {}", x), + )) + } + }) + } + + fn write(&self, mut writer: W, input: ChainType) -> io::Result<()> { + writer.write_u8(Self::VERSION)?; match self { - Capability::None => false, - Capability::View(_) => true, - Capability::Spend(_) => true, + UnifiedKeyStore::Spend(usk) => { + writer.write_u8(KEY_TYPE_SPEND)?; + usk.write(&mut writer, ()) + } + UnifiedKeyStore::View(ufvk) => { + writer.write_u8(KEY_TYPE_VIEW)?; + ufvk.write(&mut writer, input) + } + UnifiedKeyStore::Empty => writer.write_u8(KEY_TYPE_EMPTY), } } +} +impl ReadableWriteable for UnifiedSpendingKey { + const VERSION: u8 = 0; - /// TODO: Add Doc Comment Here! - pub fn kind_str(&self) -> &'static str { - match self { - Capability::None => "No key", - Capability::View(_) => "View only", - Capability::Spend(_) => "Spend capable", + fn read(mut reader: R, _input: ()) -> io::Result { + let len = CompactSize::read(&mut reader)?; + let mut usk = vec![0u8; len as usize]; + reader.read_exact(&mut usk)?; + + UnifiedSpendingKey::from_bytes(Era::Orchard, &usk) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "USK bytes are invalid")) + } + + fn write(&self, mut writer: W, _input: ()) -> io::Result<()> { + let usk_bytes = self.to_bytes(Era::Orchard); + CompactSize::write(&mut writer, usk_bytes.len())?; + writer.write_all(&usk_bytes)?; + Ok(()) + } +} +impl ReadableWriteable for UnifiedFullViewingKey { + const VERSION: u8 = 0; + + fn read(mut reader: R, input: ChainType) -> io::Result { + let len = CompactSize::read(&mut reader)?; + let mut ufvk = vec![0u8; len as usize]; + reader.read_exact(&mut ufvk)?; + let ufvk_encoded = std::str::from_utf8(&ufvk) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?; + + UnifiedFullViewingKey::decode(&input, ufvk_encoded).map_err(|e| { + io::Error::new( + io::ErrorKind::InvalidData, + format!("UFVK decoding error: {}", e), + ) + }) + } + + fn write(&self, mut writer: W, input: ChainType) -> io::Result<()> { + let ufvk_bytes = self.encode(&input).as_bytes().to_vec(); + CompactSize::write(&mut writer, ufvk_bytes.len())?; + writer.write_all(&ufvk_bytes)?; + Ok(()) + } +} + +impl TryFrom<&UnifiedKeyStore> for UnifiedSpendingKey { + type Error = KeyError; + fn try_from(unified_key_store: &UnifiedKeyStore) -> Result { + match unified_key_store { + UnifiedKeyStore::Spend(usk) => Ok(*usk.clone()), + _ => Err(KeyError::NoSpendCapability), } } } +impl TryFrom<&UnifiedKeyStore> for orchard::keys::SpendingKey { + type Error = KeyError; + fn try_from(unified_key_store: &UnifiedKeyStore) -> Result { + let usk = UnifiedSpendingKey::try_from(unified_key_store)?; + Ok(*usk.orchard()) + } +} +impl TryFrom<&UnifiedKeyStore> for sapling_crypto::zip32::ExtendedSpendingKey { + type Error = KeyError; + fn try_from(unified_key_store: &UnifiedKeyStore) -> Result { + let usk = UnifiedSpendingKey::try_from(unified_key_store)?; + Ok(usk.sapling().clone()) + } +} +impl TryFrom<&UnifiedKeyStore> for zcash_primitives::legacy::keys::AccountPrivKey { + type Error = KeyError; + fn try_from(unified_key_store: &UnifiedKeyStore) -> Result { + let usk = UnifiedSpendingKey::try_from(unified_key_store)?; + Ok(usk.transparent().clone()) + } +} -/// TODO: Add Doc Comment Here! -#[derive(Debug)] -pub struct WalletCapability { - /// TODO: Add Doc Comment Here! - pub transparent: Capability< - super::extended_transparent::ExtendedPubKey, - super::extended_transparent::ExtendedPrivKey, - >, - /// TODO: Add Doc Comment Here! - pub sapling: Capability< - sapling_crypto::zip32::DiversifiableFullViewingKey, - sapling_crypto::zip32::ExtendedSpendingKey, - >, - /// TODO: Add Doc Comment Here! - pub orchard: Capability, +impl TryFrom<&UnifiedKeyStore> for UnifiedFullViewingKey { + type Error = KeyError; + fn try_from(unified_key_store: &UnifiedKeyStore) -> Result { + match unified_key_store { + UnifiedKeyStore::Spend(usk) => Ok(usk.to_unified_full_viewing_key()), + UnifiedKeyStore::View(ufvk) => Ok(*ufvk.clone()), + UnifiedKeyStore::Empty => Err(KeyError::NoViewCapability), + } + } +} +impl TryFrom<&UnifiedKeyStore> for orchard::keys::FullViewingKey { + type Error = KeyError; + fn try_from(unified_key_store: &UnifiedKeyStore) -> Result { + let ufvk = UnifiedFullViewingKey::try_from(unified_key_store)?; + ufvk.orchard().ok_or(KeyError::NoViewCapability).cloned() + } +} +impl TryFrom<&UnifiedKeyStore> for sapling_crypto::zip32::DiversifiableFullViewingKey { + type Error = KeyError; + fn try_from(unified_key_store: &UnifiedKeyStore) -> Result { + let ufvk = UnifiedFullViewingKey::try_from(unified_key_store)?; + ufvk.sapling().ok_or(KeyError::NoViewCapability).cloned() + } +} +impl TryFrom<&UnifiedKeyStore> for zcash_primitives::legacy::keys::AccountPubKey { + type Error = KeyError; + fn try_from(unified_key_store: &UnifiedKeyStore) -> Result { + let ufvk = UnifiedFullViewingKey::try_from(unified_key_store)?; + ufvk.transparent() + .ok_or(KeyError::NoViewCapability) + .cloned() + } +} +/// Interface to cryptographic capabilities that the library requires for +/// various operations.
+/// It is created either from a [BIP39 mnemonic phrase](),
+/// loaded from a [`zcash_keys::keys::UnifiedSpendingKey`]
+/// or a [`zcash_keys::keys::UnifiedFullViewingKey`].

+/// In addition to fundamental spending and viewing keys, the type caches generated addresses. +#[derive(Debug, Getters, Setters)] +pub struct WalletCapability { + /// Unified key store + #[getset(get = "pub", set = "pub(crate)")] + unified_key_store: UnifiedKeyStore, + /// Cache of transparent addresses that the user has created. + /// Receipts to a single address are correlated on chain. + /// TODO: Is there any reason to have this field, apart from the + /// unified_addresses field? transparent_child_addresses: Arc>, - addresses: append_only_vec::AppendOnlyVec, - // Not all diversifier indexes produce valid sapling addresses. - // Because of this, the index isn't necessarily equal to addresses.len() + // TODO: read/write for ephmereral addresses + // TODO: Remove this field and exclusively use the TxMap field instead + transparent_child_ephemeral_addresses: + Arc>, + /// Cache of unified_addresses + unified_addresses: append_only_vec::AppendOnlyVec, addresses_write_lock: AtomicBool, } impl Default for WalletCapability { fn default() -> Self { Self { - orchard: Capability::None, - sapling: Capability::None, - transparent: Capability::None, + unified_key_store: UnifiedKeyStore::Empty, transparent_child_addresses: Arc::new(AppendOnlyVec::new()), - addresses: AppendOnlyVec::new(), + transparent_child_ephemeral_addresses: Arc::new(AppendOnlyVec::new()), + unified_addresses: AppendOnlyVec::new(), addresses_write_lock: AtomicBool::new(false), } } @@ -120,10 +270,10 @@ pub struct ReceiverSelection { pub transparent: bool, } -impl ReadableWriteable<()> for ReceiverSelection { +impl ReadableWriteable for ReceiverSelection { const VERSION: u8 = 1; - fn read(mut reader: R, _: ()) -> io::Result { + fn read(mut reader: R, _input: ()) -> io::Result { let _version = Self::get_version(&mut reader)?; let receivers = reader.read_u8()?; Ok(Self { @@ -133,7 +283,7 @@ impl ReadableWriteable<()> for ReceiverSelection { }) } - fn write(&self, mut writer: W) -> io::Result<()> { + fn write(&self, mut writer: W, _input: ()) -> io::Result<()> { writer.write_u8(Self::VERSION)?; let mut receivers = 0; if self.orchard { @@ -158,7 +308,7 @@ fn read_write_receiver_selections() { { let mut receivers_selected_bytes = [0; 2]; receivers_selected - .write(receivers_selected_bytes.as_mut_slice()) + .write(receivers_selected_bytes.as_mut_slice(), ()) .unwrap(); assert_eq!(i as u8, receivers_selected_bytes[1]); } @@ -169,88 +319,76 @@ impl WalletCapability { &self, receiver: &TransparentAddress, ) -> Option { - self.addresses + self.unified_addresses .iter() .find(|ua| ua.transparent() == Some(receiver)) .cloned() } /// TODO: Add Doc Comment Here! pub fn addresses(&self) -> &AppendOnlyVec { - &self.addresses + &self.unified_addresses } /// TODO: Add Doc Comment Here! pub fn transparent_child_addresses(&self) -> &Arc> { &self.transparent_child_addresses } - - /// TODO: Add Doc Comment Here! - pub fn ufvk(&self) -> Result { - use zcash_address::unified::Fvk as UfvkComponent; - let o_fvk = - UfvkComponent::Orchard(orchard::keys::FullViewingKey::try_from(self)?.to_bytes()); - let s_fvk = UfvkComponent::Sapling( - sapling_crypto::zip32::DiversifiableFullViewingKey::try_from(self)?.to_bytes(), - ); - let mut t_fvk_bytes = [0u8; 65]; - let possible_transparent_key: Result = self.try_into(); - if let Ok(t_ext_pk) = possible_transparent_key { - t_fvk_bytes[0..32].copy_from_slice(&t_ext_pk.chain_code[..]); - t_fvk_bytes[32..65].copy_from_slice(&t_ext_pk.public_key.serialize()[..]); - let t_fvk = UfvkComponent::P2pkh(t_fvk_bytes); - Ufvk::try_from_items(vec![o_fvk, s_fvk, t_fvk]).map_err(|e| e.to_string()) - } else { - Ufvk::try_from_items(vec![o_fvk, s_fvk]).map_err(|e| e.to_string()) - } - } - - /// TODO: Add Doc Comment Here! + /// Generates a unified address from the given desired receivers + /// + /// See [`crate::wallet::WalletCapability::generate_transparent_receiver`] for information on using `legacy_key` pub fn new_address( &self, desired_receivers: ReceiverSelection, + legacy_key: bool, ) -> Result { - if (desired_receivers.transparent & !self.transparent.can_view()) - | (desired_receivers.sapling & !self.sapling.can_view() - | (desired_receivers.orchard & !self.orchard.can_view())) - { - return Err("The wallet is not capable of producing desired receivers.".to_string()); - } if self .addresses_write_lock .swap(true, atomic::Ordering::Acquire) { return Err("addresses_write_lock collision!".to_string()); } - let previous_num_addresses = self.addresses.len(); + + let previous_num_addresses = self.unified_addresses.len(); let orchard_receiver = if desired_receivers.orchard { - let fvk: orchard::keys::FullViewingKey = match self.try_into() { + let fvk: orchard::keys::FullViewingKey = match self.unified_key_store().try_into() { Ok(viewkey) => viewkey, Err(e) => { self.addresses_write_lock .swap(false, atomic::Ordering::Release); - return Err(e); + return Err(e.to_string()); } }; - Some(fvk.address_at(self.addresses.len(), orchard::keys::Scope::External)) + Some(fvk.address_at(self.unified_addresses.len(), orchard::keys::Scope::External)) } else { None }; // produce a Sapling address to increment Sapling diversifier index - let sapling_receiver = if desired_receivers.sapling && self.sapling.can_view() { + let sapling_receiver = if desired_receivers.sapling { let mut sapling_diversifier_index = DiversifierIndex::new(); let mut address; let mut count = 0; let fvk: sapling_crypto::zip32::DiversifiableFullViewingKey = - self.try_into().expect("to create an fvk"); + match self.unified_key_store().try_into() { + Ok(viewkey) => viewkey, + Err(e) => { + self.addresses_write_lock + .swap(false, atomic::Ordering::Release); + return Err(e.to_string()); + } + }; loop { (sapling_diversifier_index, address) = fvk .find_address(sapling_diversifier_index) .expect("Diversifier index overflow"); sapling_diversifier_index .increment() - .expect("diversifier index overflow"); - if count == self.addresses.len() { + .expect("Diversifier index overflow"); + // Not all sapling_diversifier_indexes produce valid + // sapling addresses. + // Because of this self.unified_addresses.len() + // will be <= sapling_diversifier_index + if count == self.unified_addresses.len() { break; } count += 1; @@ -261,44 +399,8 @@ impl WalletCapability { }; let transparent_receiver = if desired_receivers.transparent { - let child_index = KeyIndex::from_index(self.addresses.len() as u32); - let child_pk = match &self.transparent { - Capability::Spend(ext_sk) => { - let secp = secp256k1::Secp256k1::new(); - Some( - match ext_sk.derive_private_key(child_index) { - Err(e) => { - self.addresses_write_lock - .swap(false, atomic::Ordering::Release); - return Err(format!( - "Transparent private key derivation failed: {e}" - )); - } - Ok(res) => res.private_key, - } - .public_key(&secp), - ) - } - Capability::View(ext_pk) => Some(match ext_pk.derive_public_key(child_index) { - Err(e) => { - self.addresses_write_lock - .swap(false, atomic::Ordering::Release); - return Err(format!("Transparent public key derivation failed: {e}")); - } - Ok(res) => res.public_key, - }), - Capability::None => None, - }; - if let Some(pk) = child_pk { - self.transparent_child_addresses.push(( - self.addresses.len(), - #[allow(deprecated)] - zcash_primitives::legacy::keys::pubkey_to_address(&pk), - )); - Some(pk) - } else { - None - } + self.generate_transparent_receiver(legacy_key) + .map_err(|e| e.to_string())? } else { None }; @@ -306,12 +408,7 @@ impl WalletCapability { let ua = UnifiedAddress::from_receivers( orchard_receiver, sapling_receiver, - #[allow(deprecated)] - transparent_receiver - .as_ref() - // This is deprecated. Not sure what the alternative is, - // other than implementing it ourselves. - .map(zcash_primitives::legacy::keys::pubkey_to_address), + transparent_receiver, ); let ua = match ua { Some(address) => address, @@ -324,60 +421,100 @@ impl WalletCapability { ); } }; - self.addresses.push(ua.clone()); - assert_eq!(self.addresses.len(), previous_num_addresses + 1); + self.unified_addresses.push(ua.clone()); + assert_eq!(self.unified_addresses.len(), previous_num_addresses + 1); self.addresses_write_lock .swap(false, atomic::Ordering::Release); Ok(ua) } + /// Generates a transparent receiver for the specified scope. + pub fn generate_transparent_receiver( + &self, + // this should only be `true` when generating transparent addresses while loading from legacy keys (pre wallet version 29) + // legacy transparent keys are already derived to the external scope so setting `legacy_key` to `true` will skip this scope derivation + legacy_key: bool, + ) -> Result, bip32::Error> { + let derive_address = |transparent_fvk: &AccountPubKey, + child_index: NonHardenedChildIndex| + -> Result { + let t_addr = if legacy_key { + generate_transparent_address_from_legacy_key(transparent_fvk, child_index)? + } else { + transparent_fvk + .derive_external_ivk()? + .derive_address(child_index)? + }; + + self.transparent_child_addresses + .push((self.addresses().len(), t_addr)); + Ok(t_addr) + }; + let child_index = NonHardenedChildIndex::from_index(self.addresses().len() as u32) + .expect("hardened bit should not be set for non-hardened child indexes"); + let transparent_receiver = match self.unified_key_store() { + UnifiedKeyStore::Spend(usk) => { + derive_address(&usk.transparent().to_account_pubkey(), child_index) + .map(Option::Some) + } + UnifiedKeyStore::View(ufvk) => ufvk + .transparent() + .map(|pub_key| derive_address(pub_key, child_index)) + .transpose(), + UnifiedKeyStore::Empty => Ok(None), + }?; + + Ok(transparent_receiver) + } + /// TODO: Add Doc Comment Here! + #[deprecated(note = "not used in zingolib codebase")] pub fn get_taddr_to_secretkey_map( &self, - config: &ZingoConfig, - ) -> Result, String> { - if let Capability::Spend(transparent_sk) = &self.transparent { + chain: &ChainType, + ) -> Result, KeyError> { + if let UnifiedKeyStore::Spend(usk) = self.unified_key_store() { self.transparent_child_addresses() .iter() - .map(|(i, taddr)| -> Result<_, String> { + .map(|(i, taddr)| -> Result<_, KeyError> { let hash = match taddr { TransparentAddress::PublicKeyHash(hash) => hash, TransparentAddress::ScriptHash(hash) => hash, }; Ok(( - hash.to_base58check(&config.chain.b58_pubkey_address_prefix(), &[]), - transparent_sk - .derive_private_key(KeyIndex::Normal(*i as u32)) - .map_err(|e| e.to_string())? - .private_key, + hash.to_base58check(&chain.b58_pubkey_address_prefix(), &[]), + usk.transparent() + .derive_external_secret_key( + NonHardenedChildIndex::from_index(*i as u32) + .ok_or(KeyError::InvalidNonHardenedChildIndex)?, + ) + .map_err(DerivationError::Transparent) + .map_err(KeyError::KeyDerivationError)?, )) }) .collect::>() } else { - Err("Wallet is no capable to spend transparent funds".to_string()) + Err(KeyError::NoSpendCapability) } } /// TODO: Add Doc Comment Here! - pub fn new_from_seed(config: &ZingoConfig, seed: &[u8; 64], position: u32) -> Self { - let (sapling_key, _, _) = get_zaddr_from_bip39seed(config, seed, position); - let transparent_parent_key = - super::extended_transparent::ExtendedPrivKey::get_ext_taddr_from_bip39seed( - config, seed, position, - ); - - let orchard_key = orchard::keys::SpendingKey::from_zip32_seed( + pub fn new_from_seed( + config: &ZingoConfig, + seed: &[u8; 64], + position: u32, + ) -> Result { + let usk = UnifiedSpendingKey::from_seed( + &config.chain, seed, - config.chain.coin_type(), - AccountId::try_from(position).unwrap(), + AccountId::try_from(position).map_err(KeyError::InvalidAccountId)?, ) - .unwrap(); - Self { - orchard: Capability::Spend(orchard_key), - sapling: Capability::Spend(sapling_key), - transparent: Capability::Spend(transparent_parent_key), + .map_err(KeyError::KeyDerivationError)?; + + Ok(Self { + unified_key_store: UnifiedKeyStore::Spend(Box::new(usk)), ..Default::default() - } + }) } /// TODO: Add Doc Comment Here! @@ -385,84 +522,49 @@ impl WalletCapability { config: &ZingoConfig, seed_phrase: &Mnemonic, position: u32, - ) -> Result { + ) -> Result { // The seed bytes is the raw entropy. To pass it to HD wallet generation, // we need to get the 64 byte bip39 entropy let bip39_seed = seed_phrase.to_seed(""); - Ok(Self::new_from_seed(config, &bip39_seed, position)) + Self::new_from_seed(config, &bip39_seed, position) } /// Creates a new `WalletCapability` from a unified spending key. - pub fn new_from_usk(usk: &[u8]) -> Result { + pub fn new_from_usk(usk: &[u8]) -> Result { // Decode unified spending key let usk = UnifiedSpendingKey::from_bytes(Era::Orchard, usk) - .map_err(|_| "Error decoding unified spending key.")?; - - // Workaround https://github.com/zcash/librustzcash/issues/929 by serializing and deserializing the transparent key. - let transparent_bytes = usk.transparent().to_bytes(); - let transparent_ext_key = transparent_key_from_bytes(transparent_bytes.as_slice()) - .map_err(|e| format!("Error processing transparent key: {}", e))?; + .map_err(|_| KeyError::KeyDecodingError)?; Ok(Self { - orchard: Capability::Spend(usk.orchard().to_owned()), - sapling: Capability::Spend(usk.sapling().to_owned()), - transparent: Capability::Spend(transparent_ext_key), + unified_key_store: UnifiedKeyStore::Spend(Box::new(usk)), ..Default::default() }) } /// TODO: Add Doc Comment Here! - pub fn new_from_ufvk(config: &ZingoConfig, ufvk_encoded: String) -> Result { + pub fn new_from_ufvk(config: &ZingoConfig, ufvk_encoded: String) -> Result { // Decode UFVK if ufvk_encoded.starts_with(config.chain.hrp_sapling_extended_full_viewing_key()) { - return Err("Viewing keys must be imported in the unified format".to_string()); + return Err(KeyError::InvalidFormat); } - let (network, ufvk) = Ufvk::decode(&ufvk_encoded) - .map_err(|e| format!("Error decoding unified full viewing key: {}", e))?; + let (network, ufvk) = + Ufvk::decode(&ufvk_encoded).map_err(|_| KeyError::KeyDecodingError)?; if network != config.chain.network_type() { - return Err("Given UFVK is not valid for current chain".to_string()); + return Err(KeyError::NetworkMismatch); } + let ufvk = UnifiedFullViewingKey::parse(&ufvk).map_err(|_| KeyError::KeyDecodingError)?; - // Initialize an instance with no capabilities. - let mut wc = WalletCapability::default(); - for fvk in ufvk.items() { - use zcash_address::unified::Fvk as UfvkComponent; - match fvk { - UfvkComponent::Orchard(key_bytes) => { - wc.orchard = Capability::View( - orchard::keys::FullViewingKey::from_bytes(&key_bytes) - .ok_or("Orchard FVK deserialization failed")?, - ); - } - UfvkComponent::Sapling(key_bytes) => { - wc.sapling = Capability::View( - sapling_crypto::zip32::DiversifiableFullViewingKey::read( - &key_bytes[..], - (), - ) - .map_err(|e| e.to_string())?, - ); - } - UfvkComponent::P2pkh(key_bytes) => { - wc.transparent = Capability::View(ExtendedPubKey { - chain_code: key_bytes[0..32].to_vec(), - public_key: secp256k1::PublicKey::from_slice(&key_bytes[32..65]) - .map_err(|e| e.to_string())?, - }); - } - UfvkComponent::Unknown { typecode, data: _ } => { - log::info!( - "Unknown receiver of type {} found in Unified Viewing Key", - typecode - ); - } - } - } - Ok(wc) + Ok(Self { + unified_key_store: UnifiedKeyStore::View(Box::new(ufvk)), + ..Default::default() + }) } - pub(crate) fn get_all_taddrs(&self, config: &ZingoConfig) -> HashSet { - self.addresses + /// external here refers to HD keys: + /// + /// where external and internal were inherited from the BIP44 conventions + fn get_external_taddrs(&self, chain: &crate::config::ChainType) -> HashSet { + self.unified_addresses .iter() .filter_map(|address| { address.transparent().and_then(|transparent_receiver| { @@ -471,7 +573,7 @@ impl WalletCapability { { Some(super::ToBase58Check::to_base58check( hash.as_slice(), - &config.chain.b58_pubkey_address_prefix(), + &chain.b58_pubkey_address_prefix(), &[], )) } else { @@ -482,6 +584,19 @@ impl WalletCapability { .collect() } + pub(crate) fn get_ephemeral_taddrs(&self, chain: &crate::config::ChainType) -> HashSet { + self.transparent_child_ephemeral_addresses + .iter() + .map(|(transparent_address, _metadata)| transparent_address.encode(chain)) + .collect() + } + + pub(crate) fn get_taddrs(&self, chain: &crate::config::ChainType) -> HashSet { + self.get_external_taddrs(chain) + .union(&self.get_ephemeral_taddrs(chain)) + .cloned() + .collect() + } /// TODO: Add Doc Comment Here! pub fn first_sapling_address(&self) -> sapling_crypto::PaymentAddress { // This index is dangerous, but all ways to instantiate a UnifiedSpendAuthority @@ -489,15 +604,10 @@ impl WalletCapability { *self.addresses()[0].sapling().unwrap() } - /// Returns a selection of pools where the wallet can spend funds. - pub fn can_spend_from_all_pools(&self) -> bool { - self.orchard.can_spend() && self.sapling.can_spend() && self.transparent.can_spend() - } - /// TODO: Add Doc Comment Here! //TODO: NAME?????!! pub fn get_trees_witness_trees(&self) -> Option { - if self.can_spend_from_all_pools() { + if self.unified_key_store().is_spending_key() { Some(crate::data::witness_trees::WitnessTrees::default()) } else { None @@ -506,91 +616,164 @@ impl WalletCapability { /// Returns a selection of pools where the wallet can view funds. pub fn can_view(&self) -> ReceiverSelection { - ReceiverSelection { - orchard: self.orchard.can_view(), - sapling: self.sapling.can_view(), - transparent: self.transparent.can_view(), + match self.unified_key_store() { + UnifiedKeyStore::Spend(_) => ReceiverSelection { + orchard: true, + sapling: true, + transparent: true, + }, + UnifiedKeyStore::View(ufvk) => ReceiverSelection { + orchard: ufvk.orchard().is_some(), + sapling: ufvk.sapling().is_some(), + transparent: ufvk.transparent().is_some(), + }, + UnifiedKeyStore::Empty => ReceiverSelection { + orchard: false, + sapling: false, + transparent: false, + }, } } } -/// Reads a transparent ExtendedPrivKey from a buffer that has a 32 byte private key and 32 byte chain code. -fn transparent_key_from_bytes(bytes: &[u8]) -> Result { - let mut reader = std::io::Cursor::new(bytes); - - let private_key = SecretKey::read(&mut reader, ())?; - let mut chain_code = [0; 32]; - reader.read_exact(&mut chain_code)?; +impl ReadableWriteable for WalletCapability { + const VERSION: u8 = 4; - Ok(ExtendedPrivKey { - chain_code: chain_code.to_vec(), - private_key, - }) -} + fn read(mut reader: R, input: ChainType) -> io::Result { + let version = Self::get_version(&mut reader)?; + let legacy_key: bool; + let ephemeral_addresses_len: u32; -impl ReadableWriteable<()> for Capability -where - V: ReadableWriteable<()>, - S: ReadableWriteable<()>, -{ - const VERSION: u8 = 1; - fn read(mut reader: R, _input: ()) -> io::Result { - let _version = Self::get_version(&mut reader)?; - let capability_type = reader.read_u8()?; - Ok(match capability_type { - 0 => Capability::None, - 1 => Capability::View(V::read(&mut reader, ())?), - 2 => Capability::Spend(S::read(&mut reader, ())?), - x => { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - format!("Unknown wallet Capability type: {}", x), - )) + let wc = match version { + // in version 1, only spending keys are stored + 1 => { + legacy_key = true; + ephemeral_addresses_len = 0; + + // Create a temporary USK for address generation to load old wallets + // due to missing BIP0032 transparent extended private key data + // + // USK is re-derived later from seed due to missing BIP0032 transparent extended private key data + let orchard_sk = orchard::keys::SpendingKey::read(&mut reader, ())?; + let sapling_sk = sapling_crypto::zip32::ExtendedSpendingKey::read(&mut reader)?; + let transparent_sk = + super::legacy::extended_transparent::ExtendedPrivKey::read(&mut reader, ())?; + let usk = legacy_sks_to_usk(&orchard_sk, &sapling_sk, &transparent_sk) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?; + Self { + unified_key_store: UnifiedKeyStore::Spend(Box::new(usk)), + ..Default::default() + } } - }) - } + 2 => { + legacy_key = true; + ephemeral_addresses_len = 0; + + let orchard_capability = Capability::< + orchard::keys::FullViewingKey, + orchard::keys::SpendingKey, + >::read(&mut reader, ())?; + let sapling_capability = Capability::< + sapling_crypto::zip32::DiversifiableFullViewingKey, + sapling_crypto::zip32::ExtendedSpendingKey, + >::read(&mut reader, ())?; + let transparent_capability = Capability::< + super::legacy::extended_transparent::ExtendedPubKey, + super::legacy::extended_transparent::ExtendedPrivKey, + >::read(&mut reader, ())?; + + let orchard_fvk = match &orchard_capability { + Capability::View(fvk) => Some(fvk), + _ => None, + }; + let sapling_fvk = match &sapling_capability { + Capability::View(fvk) => Some(fvk), + _ => None, + }; + let transparent_fvk = match &transparent_capability { + Capability::View(fvk) => Some(fvk), + _ => None, + }; + + let unified_key_store = if orchard_fvk.is_some() + || sapling_fvk.is_some() + || transparent_fvk.is_some() + { + // In the case of loading from viewing keys: + // Create the UFVK from FVKs. + let ufvk = super::legacy::legacy_fvks_to_ufvk( + orchard_fvk, + sapling_fvk, + transparent_fvk, + &input, + ) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?; + UnifiedKeyStore::View(Box::new(ufvk)) + } else if matches!(sapling_capability.clone(), Capability::Spend(_)) { + // In the case of loading spending keys: + // Only sapling is checked for spend capability due to only supporting a full set of spend keys + // + // Create a temporary USK for address generation to load old wallets + // due to missing BIP0032 transparent extended private key data + // + // USK is re-derived later from seed due to missing BIP0032 transparent extended private key data + // this missing data is not required for UFVKs + let orchard_sk = match &orchard_capability { + Capability::Spend(sk) => sk, + _ => return Err(io::Error::new( + io::ErrorKind::InvalidData, + "Orchard spending key not found. Wallet should have full spend capability!" + .to_string(), + )), + }; + let sapling_sk = match &sapling_capability { + Capability::Spend(sk) => sk, + _ => return Err(io::Error::new( + io::ErrorKind::InvalidData, + "Sapling spending key not found. Wallet should have full spend capability!" + .to_string(), + )), + }; + let transparent_sk = match &transparent_capability { + Capability::Spend(sk) => sk, + _ => return Err(io::Error::new( + io::ErrorKind::InvalidData, + "Transparent spending key not found. Wallet should have full spend capability!" + .to_string(), + )), + }; - fn write(&self, mut writer: W) -> io::Result<()> { - writer.write_u8(Self::VERSION)?; - match self { - Capability::None => writer.write_u8(0), - Capability::View(vk) => { - writer.write_u8(1)?; - vk.write(&mut writer) - } - Capability::Spend(sk) => { - writer.write_u8(2)?; - sk.write(&mut writer) + let usk = legacy_sks_to_usk(orchard_sk, sapling_sk, transparent_sk) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?; + + UnifiedKeyStore::Spend(Box::new(usk)) + } else { + UnifiedKeyStore::Empty + }; + + Self { + unified_key_store, + ..Default::default() + } } - } - } -} + 3 => { + legacy_key = false; + ephemeral_addresses_len = 0; -impl ReadableWriteable<()> for WalletCapability { - const VERSION: u8 = 2; + Self { + unified_key_store: UnifiedKeyStore::read(&mut reader, input)?, + ..Default::default() + } + } + 4 => { + legacy_key = false; + ephemeral_addresses_len = reader.read_u32::()?; - fn read(mut reader: R, _input: ()) -> io::Result { - let version = Self::get_version(&mut reader)?; - let wc = match version { - // in version 1, only spending keys are stored - 1 => { - let orchard = orchard::keys::SpendingKey::read(&mut reader, ())?; - let sapling = sapling_crypto::zip32::ExtendedSpendingKey::read(&mut reader)?; - let transparent = - super::extended_transparent::ExtendedPrivKey::read(&mut reader, ())?; Self { - orchard: Capability::Spend(orchard), - sapling: Capability::Spend(sapling), - transparent: Capability::Spend(transparent), + unified_key_store: UnifiedKeyStore::read(&mut reader, input)?, ..Default::default() } } - 2 => Self { - orchard: Capability::read(&mut reader, ())?, - sapling: Capability::read(&mut reader, ())?, - transparent: Capability::read(&mut reader, ())?, - ..Default::default() - }, _ => { return Err(io::Error::new( io::ErrorKind::InvalidData, @@ -598,29 +781,38 @@ impl ReadableWriteable<()> for WalletCapability { )) } }; - let receiver_selections = Vector::read(reader, |r| ReceiverSelection::read(r, ()))?; + let receiver_selections = Vector::read(&mut reader, |r| ReceiverSelection::read(r, ()))?; for rs in receiver_selections { - wc.new_address(rs) + wc.new_address(rs, legacy_key) .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; } + + for _ in 0..ephemeral_addresses_len { + new_persistent_ephemeral_address( + &wc.transparent_child_ephemeral_addresses, + &wc.ephemeral_ivk() + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?, + ) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + } + Ok(wc) } - fn write(&self, mut writer: W) -> io::Result<()> { + fn write(&self, mut writer: W, input: ChainType) -> io::Result<()> { writer.write_u8(Self::VERSION)?; - self.orchard.write(&mut writer)?; - self.sapling.write(&mut writer)?; - self.transparent.write(&mut writer)?; + writer.write_u32::(self.transparent_child_ephemeral_addresses.len() as u32)?; + self.unified_key_store().write(&mut writer, input)?; Vector::write( &mut writer, - &self.addresses.iter().collect::>(), + &self.unified_addresses.iter().collect::>(), |w, address| { ReceiverSelection { orchard: address.orchard().is_some(), sapling: address.sapling().is_some(), transparent: address.transparent().is_some(), } - .write(w) + .write(w, ()) }, ) } @@ -673,78 +865,6 @@ where __scope: PhantomData, } -impl TryFrom<&WalletCapability> for super::extended_transparent::ExtendedPrivKey { - type Error = String; - fn try_from(wc: &WalletCapability) -> Result { - match &wc.transparent { - Capability::Spend(sk) => Ok(sk.clone()), - _ => Err("The wallet is not capable of spending transparent funds".to_string()), - } - } -} - -impl TryFrom<&WalletCapability> for sapling_crypto::zip32::ExtendedSpendingKey { - type Error = String; - fn try_from(wc: &WalletCapability) -> Result { - match &wc.sapling { - Capability::Spend(sk) => Ok(sk.clone()), - _ => Err("The wallet is not capable of spending Sapling funds".to_string()), - } - } -} - -impl TryFrom<&WalletCapability> for orchard::keys::SpendingKey { - type Error = String; - fn try_from(wc: &WalletCapability) -> Result { - match &wc.orchard { - Capability::Spend(sk) => Ok(*sk), - _ => Err("The wallet is not capable of spending Orchard funds".to_string()), - } - } -} - -impl TryFrom<&WalletCapability> for super::extended_transparent::ExtendedPubKey { - type Error = String; - fn try_from(wc: &WalletCapability) -> Result { - match &wc.transparent { - Capability::Spend(ext_sk) => Ok(ExtendedPubKey::from(ext_sk)), - Capability::View(ext_pk) => Ok(ext_pk.clone()), - Capability::None => { - Err("The wallet is not capable of viewing transparent funds".to_string()) - } - } - } -} - -impl TryFrom<&WalletCapability> for orchard::keys::FullViewingKey { - type Error = String; - fn try_from(wc: &WalletCapability) -> Result { - match &wc.orchard { - Capability::Spend(sk) => Ok(orchard::keys::FullViewingKey::from(sk)), - Capability::View(fvk) => Ok(fvk.clone()), - Capability::None => { - Err("The wallet is not capable of viewing Orchard funds".to_string()) - } - } - } -} - -impl TryFrom<&WalletCapability> for sapling_crypto::zip32::DiversifiableFullViewingKey { - type Error = String; - fn try_from(wc: &WalletCapability) -> Result { - match &wc.sapling { - Capability::Spend(sk) => { - let dfvk = sk.to_diversifiable_full_viewing_key(); - Ok(dfvk) - } - Capability::View(fvk) => Ok(fvk.clone()), - Capability::None => { - Err("The wallet is not capable of viewing Sapling funds".to_string()) - } - } - } -} - /// TODO: Add Doc Comment Here! pub trait Fvk where @@ -788,96 +908,49 @@ impl Fvk for sapling_crypto::zip32::DiversifiableFullViewingKey { } } } +mod ephemeral { + use std::sync::Arc; + + use append_only_vec::AppendOnlyVec; + use zcash_client_backend::wallet::TransparentAddressMetadata; + use zcash_keys::keys::DerivationError; + use zcash_primitives::legacy::{ + keys::{AccountPubKey, NonHardenedChildIndex, TransparentKeyScope}, + TransparentAddress, + }; -impl TryFrom<&WalletCapability> for orchard::keys::OutgoingViewingKey { - type Error = String; - fn try_from(wc: &WalletCapability) -> Result { - let fvk: orchard::keys::FullViewingKey = wc.try_into()?; - Ok(fvk.to_ovk(orchard::keys::Scope::External)) - } -} + use crate::wallet::error::KeyError; -impl TryFrom<&WalletCapability> for sapling_crypto::keys::OutgoingViewingKey { - type Error = String; - fn try_from(wc: &WalletCapability) -> Result { - let fvk: sapling_crypto::zip32::DiversifiableFullViewingKey = wc.try_into()?; - Ok(fvk.fvk().ovk) - } -} + use super::WalletCapability; -impl TryFrom<&WalletCapability> for UnifiedSpendingKey { - type Error = io::Error; - - fn try_from(value: &WalletCapability) -> Result { - let transparent = &value.transparent; - let sapling = &value.sapling; - let orchard = &value.orchard; - match (transparent, sapling, orchard) { - (Capability::Spend(tkey), Capability::Spend(skey), Capability::Spend(okey)) => { - let mut key_bytes = Vec::new(); - // orchard Era usk - key_bytes.write_u32::(BranchId::Nu5.into())?; - - let okey_bytes = okey.to_bytes(); - CompactSize::write(&mut key_bytes, u32::from(Typecode::Orchard) as usize)?; - CompactSize::write(&mut key_bytes, okey_bytes.len())?; - key_bytes.write_all(okey_bytes)?; - - let skey_bytes = skey.to_bytes(); - CompactSize::write(&mut key_bytes, u32::from(Typecode::Sapling) as usize)?; - CompactSize::write(&mut key_bytes, skey_bytes.len())?; - key_bytes.write_all(&skey_bytes)?; - - let mut tkey_bytes = Vec::new(); - tkey_bytes.write_all(tkey.private_key.as_ref())?; - tkey_bytes.write_all(&tkey.chain_code)?; - - CompactSize::write(&mut key_bytes, u32::from(Typecode::P2pkh) as usize)?; - CompactSize::write(&mut key_bytes, tkey_bytes.len())?; - key_bytes.write_all(&tkey_bytes)?; - - UnifiedSpendingKey::from_bytes(Era::Orchard, &key_bytes).map_err(|e| { - io::Error::new(io::ErrorKind::InvalidInput, format!("bad usk: {e}")) - }) - } - _otherwise => Err(io::Error::new( - io::ErrorKind::InvalidData, - "don't have spend keys", - )), + impl WalletCapability { + pub(crate) fn ephemeral_ivk( + &self, + ) -> Result { + AccountPubKey::try_from(self.unified_key_store())? + .derive_ephemeral_ivk() + .map_err(DerivationError::Transparent) + .map_err(KeyError::KeyDerivationError) } - } -} - -#[cfg(test)] -pub async fn get_transparent_secretkey_pubkey_taddr( - lightclient: &crate::lightclient::LightClient, -) -> ( - Option, - Option, - Option, -) { - use super::address_from_pubkeyhash; - - let wc = lightclient.wallet.wallet_capability(); - // 2. Get an incoming transaction to a t address - let (sk, pk) = match &wc.transparent { - Capability::None => (None, None), - Capability::View(ext_pk) => { - let child_ext_pk = ext_pk.derive_public_key(KeyIndex::Normal(0)).ok(); - (None, child_ext_pk.map(|x| x.public_key)) + pub(crate) fn ephemeral_address( + ephemeral_ivk: &zcash_primitives::legacy::keys::EphemeralIvk, + ephemeral_address_index: u32, + ) -> Result<(TransparentAddress, TransparentAddressMetadata), KeyError> { + let address_index = NonHardenedChildIndex::from_index(ephemeral_address_index) + .ok_or(KeyError::InvalidNonHardenedChildIndex)?; + Ok(( + ephemeral_ivk + .derive_ephemeral_address(address_index) + .map_err(DerivationError::Transparent) + .map_err(KeyError::KeyDerivationError)?, + TransparentAddressMetadata::new(TransparentKeyScope::EPHEMERAL, address_index), + )) } - Capability::Spend(master_sk) => { - let secp = secp256k1::Secp256k1::new(); - let extsk = master_sk - .derive_private_key(KeyIndex::Normal(wc.transparent_child_addresses[0].0 as u32)) - .unwrap(); - let pk = extsk.private_key.public_key(&secp); - #[allow(deprecated)] - (Some(extsk.private_key), Some(pk)) + /// TODO: Add Doc Comment Here! + pub fn transparent_child_ephemeral_addresses( + &self, + ) -> &Arc> { + &self.transparent_child_ephemeral_addresses } - }; - let taddr = wc.addresses()[0] - .transparent() - .map(|taddr| address_from_pubkeyhash(&lightclient.config, *taddr)); - (sk, pk, taddr) + } } diff --git a/zingolib/src/wallet/message.rs b/zingolib/src/wallet/message.rs index 5f73073d1f..bbdde05afd 100644 --- a/zingolib/src/wallet/message.rs +++ b/zingolib/src/wallet/message.rs @@ -1,4 +1,5 @@ //! TODO: Add Crate Discription Here! +use crate::config::ChainType; use byteorder::ReadBytesExt; use bytes::{Buf, Bytes, IntoBuf}; use group::GroupEncoding; @@ -18,7 +19,6 @@ use zcash_primitives::{ consensus::BlockHeight, memo::{Memo, MemoBytes}, }; -use zingoconfig::ChainType; pub struct Message { pub to: PaymentAddress, diff --git a/zingolib/src/wallet/notes.rs b/zingolib/src/wallet/notes.rs index 3baaf66e4d..93ce80eedd 100644 --- a/zingolib/src/wallet/notes.rs +++ b/zingolib/src/wallet/notes.rs @@ -11,12 +11,12 @@ pub use orchard::OrchardNote; pub mod query; use zcash_client_backend::PoolType; - use zcash_primitives::transaction::TxId; use crate::wallet::notes::query::OutputPoolQuery; use crate::wallet::notes::query::OutputQuery; use crate::wallet::notes::query::OutputSpendStatusQuery; +use zingo_status::confirmation_status::ConfirmationStatus; /// An interface for accessing all the common functionality of all the outputs #[enum_dispatch::enum_dispatch(OutputInterface)] @@ -156,25 +156,32 @@ pub mod tests { use super::query::{OutputPoolQuery, OutputSpendStatusQuery}; + use zingo_status::confirmation_status::ConfirmationStatus::Confirmed; + use zingo_status::confirmation_status::ConfirmationStatus::Mempool; + #[test] fn note_queries() { - let spend = Some((default_txid(), 112358)); + let confirmed_spend = Some((default_txid(), Confirmed(112358.into()))); + let pending_spend = Some((default_txid(), Mempool(112357.into()))); let transparent_unspent_note = TransparentOutputBuilder::default().build(); let transparent_pending_spent_note = TransparentOutputBuilder::default() - .pending_spent(spend) + .spending_tx_status(pending_spend) .clone() .build(); let transparent_spent_note = TransparentOutputBuilder::default() - .spent(spend) + .spending_tx_status(confirmed_spend) .clone() .build(); let sapling_unspent_note = SaplingNoteBuilder::default().build(); let sapling_pending_spent_note = SaplingNoteBuilder::default() - .pending_spent(spend) + .spending_tx_status(pending_spend) + .clone() + .build(); + let sapling_spent_note = SaplingNoteBuilder::default() + .spending_tx_status(confirmed_spend) .clone() .build(); - let sapling_spent_note = SaplingNoteBuilder::default().spent(spend).clone().build(); let unspent_query = OutputSpendStatusQuery::only_unspent(); let pending_or_spent_query = OutputSpendStatusQuery::spentish(); diff --git a/zingolib/src/wallet/notes/interface.rs b/zingolib/src/wallet/notes/interface.rs index aa039bdb9f..f27c6b4a3f 100644 --- a/zingolib/src/wallet/notes/interface.rs +++ b/zingolib/src/wallet/notes/interface.rs @@ -3,6 +3,7 @@ use incrementalmerkletree::{Hashable, Position}; use zcash_client_backend::{PoolType, ShieldedProtocol}; use zcash_primitives::{memo::Memo, merkle_tree::HashSer, transaction::TxId}; +use zingo_status::confirmation_status::ConfirmationStatus; use crate::wallet::{ keys::unified::WalletCapability, @@ -42,35 +43,36 @@ pub trait OutputInterface: Sized { fn value(&self) -> u64; /// If the funds are spent, the TxId and Blockheight of record - fn spent(&self) -> &Option<(TxId, u32)>; + fn spending_tx_status(&self) -> &Option<(TxId, ConfirmationStatus)>; /// Mutable access to the spent field.. hmm NOTE: Should we keep this pattern? /// what is spent becomes a Vec, where the last element of that /// Vec is the last known block chain record of the spend. So then reorgs, just extend /// the Vec which tracks all BlockChain records of the value-transfer - fn spent_mut(&mut self) -> &mut Option<(TxId, u32)>; + fn spending_tx_status_mut(&mut self) -> &mut Option<(TxId, ConfirmationStatus)>; - /// The TxId and broadcast height of a transfer that's not known to be on-record on the chain - fn pending_spent(&self) -> &Option<(TxId, u32)>; - - /// TODO: Add Doc Comment Here! - fn pending_spent_mut(&mut self) -> &mut Option<(TxId, u32)>; + /// returns the id of the spending transaction, whether pending or no + fn spending_txid(&self) -> Option { + self.spending_tx_status().map(|(txid, _status)| txid) + } /// Returns true if the note has been presumptively spent but the spent has not been validated. fn is_pending_spent(&self) -> bool { - self.pending_spent().is_some() + self.spending_tx_status() + .is_some_and(|(_txid, status)| !status.is_confirmed()) } - /// returns true if the note is confirmed spent - fn is_spent(&self) -> bool { - self.spent().is_some() + /// returns true if the note is spent and the spend is validated confirmed on chain + fn is_spent_confirmed(&self) -> bool { + self.spending_tx_status() + .is_some_and(|(_txid, status)| status.is_confirmed()) } /// Returns true if the note has one of the spend statuses enumerated by the query fn spend_status_query(&self, query: OutputSpendStatusQuery) -> bool { - (*query.unspent() && !self.is_spent() && !self.is_pending_spent()) + (*query.unspent() && !self.is_spent_confirmed() && !self.is_pending_spent()) || (*query.pending_spent() && self.is_pending_spent()) - || (*query.spent() && self.is_spent()) + || (*query.spent() && self.is_spent_confirmed()) } /// Returns true if the note is unspent (spendable). @@ -99,7 +101,7 @@ pub trait ShieldedNoteInterface: OutputInterface + OutputConstructor + Sized { type Diversifier: Copy + FromBytes<11> + ToBytes<11>; /// TODO: Add Doc Comment Here! type Note: PartialEq - + for<'a> ReadableWriteable<(Self::Diversifier, &'a WalletCapability)> + + for<'a> ReadableWriteable<(Self::Diversifier, &'a WalletCapability), ()> + Clone; /// TODO: Add Doc Comment Here! type Node: Hashable + HashSer + FromCommitment + Send + Clone + PartialEq + Eq; @@ -116,8 +118,7 @@ pub trait ShieldedNoteInterface: OutputInterface + OutputConstructor + Sized { note: Self::Note, position_of_commitment_to_witness: Option, nullifier: Option, - spent: Option<(TxId, u32)>, - pending_spent: Option<(TxId, u32)>, + spend: Option<(TxId, ConfirmationStatus)>, memo: Option, is_change: bool, have_spending_key: bool, diff --git a/zingolib/src/wallet/notes/orchard.rs b/zingolib/src/wallet/notes/orchard.rs index c12d5fa4af..21a0d7fd7e 100644 --- a/zingolib/src/wallet/notes/orchard.rs +++ b/zingolib/src/wallet/notes/orchard.rs @@ -2,6 +2,7 @@ use incrementalmerkletree::Position; use zcash_client_backend::{PoolType, ShieldedProtocol}; use zcash_primitives::{memo::Memo, transaction::TxId}; +use zingo_status::confirmation_status::ConfirmationStatus; use crate::wallet::notes::interface::OutputConstructor; @@ -27,12 +28,8 @@ pub struct OrchardNote { pub(crate) nullifier: Option, - /// If this note was confirmed spent - pub spent: Option<(TxId, u32)>, // Todo: as related to pending spent, this is potential data incoherence - - /// If this note was spent in a send, but has not yet been confirmed. - /// Contains the transaction id and height at which it was broadcast - pub pending_spent: Option<(TxId, u32)>, + /// whether, where, and when it was spent + spend: Option<(TxId, ConfirmationStatus)>, /// TODO: Add Doc Comment Here! pub memo: Option, @@ -53,20 +50,12 @@ impl OutputInterface for OrchardNote { self.orchard_crypto_note.value().inner() } - fn spent(&self) -> &Option<(TxId, u32)> { - &self.spent - } - - fn spent_mut(&mut self) -> &mut Option<(TxId, u32)> { - &mut self.spent - } - - fn pending_spent(&self) -> &Option<(TxId, u32)> { - &self.pending_spent + fn spending_tx_status(&self) -> &Option<(TxId, ConfirmationStatus)> { + &self.spend } - fn pending_spent_mut(&mut self) -> &mut Option<(TxId, u32)> { - &mut self.pending_spent + fn spending_tx_status_mut(&mut self) -> &mut Option<(TxId, ConfirmationStatus)> { + &mut self.spend } } impl OutputConstructor for OrchardNote { @@ -117,8 +106,7 @@ impl ShieldedNoteInterface for OrchardNote { orchard_crypto_note: Self::Note, witnessed_position: Option, nullifier: Option, - spent: Option<(TxId, u32)>, - pending_spent: Option<(TxId, u32)>, + spend: Option<(TxId, ConfirmationStatus)>, memo: Option, is_change: bool, have_spending_key: bool, @@ -129,8 +117,7 @@ impl ShieldedNoteInterface for OrchardNote { orchard_crypto_note, witnessed_position, nullifier, - spent, - pending_spent, + spend, memo, is_change, have_spending_key, @@ -208,12 +195,13 @@ impl ShieldedNoteInterface for OrchardNote { } } -#[cfg(test)] +#[cfg(any(test, feature = "test-elevation"))] pub mod mocks { //! Mock version of the struct for testing use incrementalmerkletree::Position; use orchard::{keys::Diversifier, note::Nullifier, value::NoteValue}; use zcash_primitives::{memo::Memo, transaction::TxId}; + use zingo_status::confirmation_status::ConfirmationStatus; use crate::{ mocks::orchard_note::OrchardCryptoNoteBuilder, utils::build_method, @@ -230,8 +218,7 @@ pub mod mocks { witnessed_position: Option>, pub output_index: Option>, nullifier: Option>, - spent: Option>, - pending_spent: Option>, + spending_tx_status: Option>, memo: Option>, is_change: Option, have_spending_key: Option, @@ -247,8 +234,7 @@ pub mod mocks { witnessed_position: None, output_index: None, nullifier: None, - spent: None, - pending_spent: None, + spending_tx_status: None, memo: None, is_change: None, have_spending_key: None, @@ -261,8 +247,7 @@ pub mod mocks { build_method!(witnessed_position, Option); build_method!(output_index, Option); build_method!(nullifier, Option); - build_method!(spent, Option<(TxId, u32)>); - build_method!(pending_spent, Option<(TxId, u32)>); + build_method!(spending_tx_status, Option<(TxId, ConfirmationStatus)>); build_method!(memo, Option); #[doc = "Set the is_change field of the builder."] pub fn set_change(&mut self, is_change: bool) -> &mut Self { @@ -285,8 +270,7 @@ pub mod mocks { self.note.clone().unwrap().build(), self.witnessed_position.unwrap(), self.nullifier.unwrap(), - self.spent.unwrap(), - self.pending_spent.unwrap(), + self.spending_tx_status.unwrap(), self.memo.clone().unwrap(), self.is_change.unwrap(), self.have_spending_key.unwrap(), @@ -304,8 +288,7 @@ pub mod mocks { .witnessed_position(Some(Position::from(0))) .output_index(Some(0)) .nullifier(Some(Nullifier::from_bytes(&[0u8; 32]).unwrap())) - .spent(None) - .pending_spent(None) + .spending_tx_status(None) .memo(None) .set_change(false) .have_spending_key(true); diff --git a/zingolib/src/wallet/notes/sapling.rs b/zingolib/src/wallet/notes/sapling.rs index e067872440..79b8a2fa37 100644 --- a/zingolib/src/wallet/notes/sapling.rs +++ b/zingolib/src/wallet/notes/sapling.rs @@ -2,6 +2,7 @@ use incrementalmerkletree::Position; use zcash_client_backend::{PoolType, ShieldedProtocol}; use zcash_primitives::{memo::Memo, transaction::TxId}; +use zingo_status::confirmation_status::ConfirmationStatus; use crate::wallet::notes::interface::OutputConstructor; @@ -28,12 +29,8 @@ pub struct SaplingNote { /// TODO: Add Doc Comment Here! pub nullifier: Option, - /// TODO: Add Doc Comment Here! - pub spent: Option<(TxId, u32)>, // If this note was confirmed spent. Todo: as related to pending spent, this is potential data incoherence - - /// If this note was spent in a send, but has not yet been confirmed. - /// Contains the transaction id and height at which it was broadcast - pub pending_spent: Option<(TxId, u32)>, + /// whether, where, and when it was spent + spend: Option<(TxId, ConfirmationStatus)>, /// TODO: Add Doc Comment Here! pub memo: Option, @@ -49,17 +46,10 @@ impl std::fmt::Debug for SaplingNote { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("SaplingNoteData") .field("diversifier", &self.diversifier) - .field("note", &self.sapling_crypto_note) - .field("nullifier", &self.nullifier) - .field("spent", &self.spent) - .field("pending_spent", &self.pending_spent) .field("memo", &self.memo) - .field("diversifier", &self.diversifier) .field("note", &self.sapling_crypto_note) .field("nullifier", &self.nullifier) - .field("spent", &self.spent) - .field("pending_spent", &self.pending_spent) - .field("memo", &self.memo) + .field("spend", &self.spend) .field("is_change", &self.is_change) .finish_non_exhaustive() } @@ -74,20 +64,12 @@ impl OutputInterface for SaplingNote { self.sapling_crypto_note.value().inner() } - fn spent(&self) -> &Option<(TxId, u32)> { - &self.spent - } - - fn spent_mut(&mut self) -> &mut Option<(TxId, u32)> { - &mut self.spent - } - - fn pending_spent(&self) -> &Option<(TxId, u32)> { - &self.pending_spent + fn spending_tx_status(&self) -> &Option<(TxId, ConfirmationStatus)> { + &self.spend } - fn pending_spent_mut(&mut self) -> &mut Option<(TxId, u32)> { - &mut self.pending_spent + fn spending_tx_status_mut(&mut self) -> &mut Option<(TxId, ConfirmationStatus)> { + &mut self.spend } } impl OutputConstructor for SaplingNote { @@ -138,8 +120,7 @@ impl ShieldedNoteInterface for SaplingNote { sapling_crypto_note: sapling_crypto::Note, witnessed_position: Option, nullifier: Option, - spent: Option<(TxId, u32)>, - pending_spent: Option<(TxId, u32)>, + spend: Option<(TxId, ConfirmationStatus)>, memo: Option, is_change: bool, have_spending_key: bool, @@ -150,8 +131,7 @@ impl ShieldedNoteInterface for SaplingNote { sapling_crypto_note, witnessed_position, nullifier, - spent, - pending_spent, + spend, memo, is_change, have_spending_key, @@ -230,12 +210,13 @@ impl ShieldedNoteInterface for SaplingNote { } } -#[cfg(test)] +#[cfg(any(test, feature = "test-elevation"))] pub mod mocks { //! Mock version of the struct for testing use incrementalmerkletree::Position; use sapling_crypto::value::NoteValue; use zcash_primitives::{memo::Memo, transaction::TxId}; + use zingo_status::confirmation_status::ConfirmationStatus; use crate::{ mocks::SaplingCryptoNoteBuilder, @@ -253,8 +234,7 @@ pub mod mocks { witnessed_position: Option>, pub output_index: Option>, nullifier: Option>, - spent: Option>, - pending_spent: Option>, + spending_tx_status: Option>, memo: Option>, is_change: Option, have_spending_key: Option, @@ -270,8 +250,7 @@ pub mod mocks { witnessed_position: None, output_index: None, nullifier: None, - spent: None, - pending_spent: None, + spending_tx_status: None, memo: None, is_change: None, have_spending_key: None, @@ -284,8 +263,7 @@ pub mod mocks { build_method!(witnessed_position, Option); build_method!(output_index, Option); build_method!(nullifier, Option); - build_method!(spent, Option<(TxId, u32)>); - build_method!(pending_spent, Option<(TxId, u32)>); + build_method!(spending_tx_status, Option<(TxId, ConfirmationStatus)>); build_method!(memo, Option); #[doc = "Set the is_change field of the builder."] pub fn set_change(&mut self, is_change: bool) -> &mut Self { @@ -308,8 +286,7 @@ pub mod mocks { self.note.clone().unwrap().build(), self.witnessed_position.unwrap(), self.nullifier.unwrap(), - self.spent.unwrap(), - self.pending_spent.unwrap(), + self.spending_tx_status.unwrap(), self.memo.clone().unwrap(), self.is_change.unwrap(), self.have_spending_key.unwrap(), @@ -327,8 +304,7 @@ pub mod mocks { .witnessed_position(Some(Position::from(0))) .output_index(Some(0)) .nullifier(Some(sapling_crypto::Nullifier::from_bytes([0; 32]))) - .spent(None) - .pending_spent(None) + .spending_tx_status(None) .memo(None) .set_change(false) .have_spending_key(true); diff --git a/zingolib/src/wallet/notes/transparent.rs b/zingolib/src/wallet/notes/transparent.rs index 13a9c26cf0..b87dc37578 100644 --- a/zingolib/src/wallet/notes/transparent.rs +++ b/zingolib/src/wallet/notes/transparent.rs @@ -5,6 +5,7 @@ use byteorder::{ReadBytesExt, WriteBytesExt}; use zcash_client_backend::PoolType; use zcash_primitives::transaction::{components::OutPoint, TxId}; +use zingo_status::confirmation_status::ConfirmationStatus; use crate::wallet::notes::{ interface::OutputConstructor, query::OutputSpendStatusQuery, OutputInterface, @@ -25,11 +26,8 @@ pub struct TransparentOutput { /// TODO: Add Doc Comment Here! pub value: u64, - spent: Option<(TxId, u32)>, // If this utxo was confirmed spent Todo: potential data incoherence with pending_spent - - /// If this utxo was spent in a send, but has not yet been confirmed. - /// Contains the txid and height at which the Tx was broadcast - pub pending_spent: Option<(TxId, u32)>, + /// whether, where, and when it was spent + spend: Option<(TxId, ConfirmationStatus)>, } impl OutputInterface for TransparentOutput { @@ -41,20 +39,12 @@ impl OutputInterface for TransparentOutput { self.value } - fn spent(&self) -> &Option<(TxId, u32)> { - &self.spent - } - - fn spent_mut(&mut self) -> &mut Option<(TxId, u32)> { - &mut self.spent - } - - fn pending_spent(&self) -> &Option<(TxId, u32)> { - &self.pending_spent + fn spending_tx_status(&self) -> &Option<(TxId, ConfirmationStatus)> { + &self.spend } - fn pending_spent_mut(&mut self) -> &mut Option<(TxId, u32)> { - &mut self.pending_spent + fn spending_tx_status_mut(&mut self) -> &mut Option<(TxId, ConfirmationStatus)> { + &mut self.spend } } impl OutputConstructor for TransparentOutput { @@ -94,8 +84,7 @@ impl TransparentOutput { output_index: u64, script: Vec, value: u64, - spent: Option<(TxId, u32)>, - pending_spent: Option<(TxId, u32)>, + spend: Option<(TxId, ConfirmationStatus)>, ) -> Self { Self { address, @@ -103,8 +92,7 @@ impl TransparentOutput { output_index, script, value, - spent, - pending_spent, + spend, } } @@ -131,11 +119,13 @@ impl TransparentOutput { writer.write_u64::(self.value)?; writer.write_i32::(0)?; - let (spent, spent_at_height) = if let Some(spent_tuple) = self.spent { - (Some(spent_tuple.0), Some(spent_tuple.1 as i32)) - } else { - (None, None) - }; + let confirmed_spend = self + .spending_tx_status() + .as_ref() + .and_then(|(txid, status)| status.get_confirmed_height().map(|height| (txid, height))); + + let spent = confirmed_spend.map(|(txid, _height)| txid); + let spent_at_height = confirmed_spend.map(|(_txid, height)| u32::from(height) as i32); zcash_encoding::Vector::write(&mut writer, &self.script, |w, b| w.write_all(&[*b]))?; @@ -209,6 +199,8 @@ impl TransparentOutput { } else { None }; + let spend = + spent_tuple.map(|(txid, height)| (txid, ConfirmationStatus::Confirmed(height.into()))); Ok(TransparentOutput { address, @@ -216,16 +208,16 @@ impl TransparentOutput { output_index, script, value, - spent: spent_tuple, - pending_spent: None, + spend, }) } } -#[cfg(test)] +#[cfg(any(test, feature = "test-elevation"))] pub mod mocks { //! Mock version of the struct for testing use zcash_primitives::{legacy::TransparentAddress, transaction::TxId}; + use zingo_status::confirmation_status::ConfirmationStatus; use crate::{utils::build_method, wallet::notes::TransparentOutput}; @@ -237,8 +229,7 @@ pub mod mocks { pub output_index: Option, script: Option>, value: Option, - spent: Option>, - pending_spent: Option>, + spending_tx_status: Option>, } #[allow(dead_code)] //TODO: fix this gross hack that I tossed in to silence the language-analyzer false positive impl TransparentOutputBuilder { @@ -250,8 +241,7 @@ pub mod mocks { output_index: None, script: None, value: None, - spent: None, - pending_spent: None, + spending_tx_status: None, } } // Methods to set each field @@ -260,8 +250,7 @@ pub mod mocks { build_method!(output_index, u64); build_method!(script, Vec); build_method!(value, u64); - build_method!(spent, Option<(TxId, u32)>); - build_method!(pending_spent, Option<(TxId, u32)>); + build_method!(spending_tx_status, Option<(TxId, ConfirmationStatus)>); /// builds a mock TransparentNote after all pieces are supplied pub fn build(&self) -> TransparentOutput { @@ -271,8 +260,7 @@ pub mod mocks { self.output_index.unwrap(), self.script.clone().unwrap(), self.value.unwrap(), - self.spent.unwrap(), - self.pending_spent.unwrap(), + self.spending_tx_status.unwrap(), ) } } @@ -286,8 +274,7 @@ pub mod mocks { .output_index(0) .script(TransparentAddress::ScriptHash([0; 20]).script().0) .value(100_000) - .spent(None) - .pending_spent(None); + .spending_tx_status(None); builder } } diff --git a/zingolib/src/wallet/propose.rs b/zingolib/src/wallet/propose.rs new file mode 100644 index 0000000000..f7b342bd39 --- /dev/null +++ b/zingolib/src/wallet/propose.rs @@ -0,0 +1,167 @@ +//! creating proposals from wallet data + +use std::{convert::Infallible, num::NonZeroU32, ops::DerefMut as _}; + +use thiserror::Error; +use zcash_client_backend::{ + data_api::wallet::input_selection::GreedyInputSelector, + zip321::{TransactionRequest, Zip321Error}, + ShieldedProtocol, +}; +use zcash_primitives::{memo::MemoBytes, transaction::components::amount::NonNegativeAmount}; + +use crate::config::ChainType; + +use super::{ + send::change_memo_from_transaction_request, + tx_map::{TxMap, TxMapTraitError}, + LightWallet, +}; + +type GISKit = + GreedyInputSelector; + +// This private helper is a very small DRY, but it has already corrected a minor +// divergence in change strategy. +// Because shielding operations are never expected to create dust notes this change +// is not a bugfix. +fn build_default_giskit(memo: Option) -> GISKit { + let change_strategy = zcash_client_backend::fees::zip317::SingleOutputChangeStrategy::new( + zcash_primitives::transaction::fees::zip317::FeeRule::standard(), + memo, + ShieldedProtocol::Orchard, + ); // review consider change strategy! + + GISKit::new( + change_strategy, + zcash_client_backend::fees::DustOutputPolicy::new( + zcash_client_backend::fees::DustAction::AllowDustChange, + None, + ), + ) +} + +/// Errors that can result from do_propose +#[derive(Debug, Error)] +pub enum ProposeSendError { + /// error in using trait to create spend proposal + #[error("{0}")] + Proposal( + zcash_client_backend::data_api::error::Error< + TxMapTraitError, + TxMapTraitError, + zcash_client_backend::data_api::wallet::input_selection::GreedyInputSelectorError< + zcash_primitives::transaction::fees::zip317::FeeError, + zcash_client_backend::wallet::NoteId, + >, + zcash_primitives::transaction::fees::zip317::FeeError, + >, + ), + /// failed to construct a transaction request + #[error("{0}")] + TransactionRequestFailed(#[from] Zip321Error), + /// send all is transferring no value + #[error("send all is transferring no value. only enough funds to pay the fees!")] + ZeroValueSendAll, + /// failed to calculate balance. + #[error("failed to calculated balance. {0}")] + BalanceError(#[from] crate::wallet::error::BalanceError), +} + +/// Errors that can result from do_propose +#[derive(Debug, Error)] +pub enum ProposeShieldError { + /// error in parsed addresses + #[error("{0}")] + Receiver(zcash_client_backend::zip321::Zip321Error), + #[error("{0}")] + /// error in using trait to create shielding proposal + Component( + zcash_client_backend::data_api::error::Error< + TxMapTraitError, + TxMapTraitError, + zcash_client_backend::data_api::wallet::input_selection::GreedyInputSelectorError< + zcash_primitives::transaction::fees::zip317::FeeError, + Infallible, + >, + zcash_primitives::transaction::fees::zip317::FeeError, + >, + ), +} + +impl LightWallet { + /// Creates a proposal from a transaction request. + pub(crate) async fn create_send_proposal( + &self, + request: TransactionRequest, + ) -> Result { + let num_ephemeral_addresses = self + .transaction_context + .key + .transparent_child_ephemeral_addresses() + .len() as u32; + let memo = change_memo_from_transaction_request(&request, num_ephemeral_addresses); + + let input_selector = build_default_giskit(Some(memo)); + let mut tmamt = self + .transaction_context + .transaction_metadata_set + .write() + .await; + + zcash_client_backend::data_api::wallet::propose_transfer::< + TxMap, + ChainType, + GISKit, + TxMapTraitError, + >( + tmamt.deref_mut(), + &self.transaction_context.config.chain, + zcash_primitives::zip32::AccountId::ZERO, + &input_selector, + request, + NonZeroU32::MIN, //review! use custom constant? + ) + .map_err(ProposeSendError::Proposal) + } + + /// The shield operation consumes a proposal that transfers value + /// into the Orchard pool. + /// + /// The proposal is generated with this method, which operates on + /// the balance transparent pool, without other input. + /// In other words, shield does not take a user-specified amount + /// to shield, rather it consumes all transparent value in the wallet that + /// can be consumsed without costing more in zip317 fees than is being transferred. + pub(crate) async fn create_shield_proposal( + &self, + ) -> Result { + let input_selector = build_default_giskit(None); + + let mut tmamt = self + .transaction_context + .transaction_metadata_set + .write() + .await; + + let proposed_shield = zcash_client_backend::data_api::wallet::propose_shielding::< + TxMap, + ChainType, + GISKit, + TxMapTraitError, + >( + &mut tmamt, + &self.transaction_context.config.chain, + &input_selector, + // don't shield dust + NonNegativeAmount::const_from_u64(10_000), + &self.get_transparent_addresses(), + // review! do we want to require confirmations? + // make it configurable? + 0, + ) + .map_err(ProposeShieldError::Component)?; + + Ok(proposed_shield) + } +} diff --git a/zingolib/src/wallet/send.rs b/zingolib/src/wallet/send.rs index c5c6edca3a..4707f622ab 100644 --- a/zingolib/src/wallet/send.rs +++ b/zingolib/src/wallet/send.rs @@ -1,60 +1,21 @@ //! This mod contains pieces of the impl LightWallet that are invoked during a send. -use crate::wallet::notes::OutputInterface; -use crate::wallet::now; -use crate::{data::receivers::Receivers, wallet::data::SpendableSaplingNote}; -use futures::Future; - -use log::{error, info}; - -use orchard::note_encryption::OrchardDomain; - -use rand::rngs::OsRng; - -use sapling_crypto::note_encryption::SaplingDomain; -use sapling_crypto::prover::{OutputProver, SpendProver}; - -use orchard::tree::MerkleHashOrchard; -use shardtree::error::{QueryError, ShardTreeError}; -use shardtree::store::memory::MemoryShardStore; -use shardtree::ShardTree; -use zcash_note_encryption::Domain; +use log::error; +use zcash_address::AddressKind; +use zcash_client_backend::proposal::Proposal; +use zcash_proofs::prover::LocalTxProver; use std::cmp; -use std::convert::Infallible; -use std::ops::Add; -use std::sync::mpsc::channel; +use std::ops::DerefMut as _; -use zcash_client_backend::{address, zip321::TransactionRequest, PoolType, ShieldedProtocol}; -use zcash_keys::address::Address; -use zcash_primitives::transaction::builder::{BuildResult, Progress}; -use zcash_primitives::transaction::components::amount::NonNegativeAmount; -use zcash_primitives::transaction::fees::fixed::FeeRule as FixedFeeRule; -use zcash_primitives::transaction::{self, Transaction}; -use zcash_primitives::{ - consensus::BlockHeight, - legacy::Script, - memo::Memo, - transaction::{ - builder::Builder, - components::{Amount, OutPoint, TxOut}, - fees::zip317::MINIMUM_FEE, - }, -}; -use zcash_primitives::{memo::MemoBytes, transaction::TxId}; +use zcash_client_backend::zip321::TransactionRequest; +use zcash_keys::address::UnifiedAddress; +use zcash_primitives::memo::Memo; +use zcash_primitives::memo::MemoBytes; -use zingo_memo::create_wallet_internal_memo_version_0; -use zingo_status::confirmation_status::ConfirmationStatus; +use zingo_memo::create_wallet_internal_memo_version_1; -use crate::data::witness_trees::{WitnessTrees, COMMITMENT_TREE_LEVELS, MAX_SHARD_LEVEL}; - -use super::data::SpendableOrchardNote; - -use super::notes::ShieldedNoteInterface; -use super::{notes, LightWallet}; - -use super::traits::{DomainWalletExt, Recipient, SpendableNote}; -use super::utils::get_price; +use super::LightWallet; /// TODO: Add Doc Comment Here! #[derive(Debug, Clone)] @@ -71,8 +32,6 @@ pub struct SendProgress { pub last_result: Option>, } -pub(crate) type NoteSelectionPolicy = Vec; - impl SendProgress { /// TODO: Add Doc Comment Here! pub fn new(id: u32) -> Self { @@ -86,65 +45,12 @@ impl SendProgress { } } -fn add_notes_to_total( - candidates: Vec, - target_amount: Amount, -) -> (Vec, Amount) { - let mut notes = Vec::new(); - let mut running_total = Amount::zero(); - for note in candidates { - if running_total >= target_amount { - break; - } - running_total = running_total - .add( - Amount::from_u64(::WalletNote::value_from_note( - note.note(), - )) - .expect("should be within the valid monetary range of zatoshis"), - ) - .expect("should be within the valid monetary range of zatoshis"); - notes.push(note); - } - - (notes, running_total) -} - -type TxBuilder<'a> = Builder<'a, zingoconfig::ChainType, ()>; impl LightWallet { - pub(super) async fn get_orchard_anchor( - &self, - tree: &ShardTree< - MemoryShardStore, - COMMITMENT_TREE_LEVELS, - MAX_SHARD_LEVEL, - >, - ) -> Result> { - Ok(orchard::Anchor::from(tree.root_at_checkpoint_depth( - self.transaction_context.config.reorg_buffer_offset as usize, - )?)) - } - - pub(super) async fn get_sapling_anchor( - &self, - tree: &ShardTree< - MemoryShardStore, - COMMITMENT_TREE_LEVELS, - MAX_SHARD_LEVEL, - >, - ) -> Result> { - Ok(sapling_crypto::Anchor::from( - tree.root_at_checkpoint_depth( - self.transaction_context.config.reorg_buffer_offset as usize, - )?, - )) - } - /// Determines the target height for a transaction, and the offset from which to /// select anchors, based on the current synchronised block chain. - pub(super) async fn get_target_height_and_anchor_offset(&self) -> Option<(u32, usize)> { + pub(crate) async fn get_target_height_and_anchor_offset(&self) -> Option<(u32, usize)> { let range = { - let blocks = self.blocks.read().await; + let blocks = self.last_100_blocks.read().await; ( blocks.last().map(|block| block.height as u32), blocks.first().map(|block| block.height as u32), @@ -169,7 +75,7 @@ impl LightWallet { } // Reset the send progress status to blank - async fn reset_send_progress(&self) { + pub(crate) async fn reset_send_progress(&self) { let mut g = self.send_progress.write().await; let next_id = g.id + 1; @@ -181,708 +87,131 @@ impl LightWallet { pub async fn get_send_progress(&self) -> SendProgress { self.send_progress.read().await.clone() } +} - /// TODO: Add Doc Comment Here! - pub async fn send_to_addresses( - &self, - sapling_prover: P, - policy: NoteSelectionPolicy, - receivers: Receivers, - submission_height: BlockHeight, - broadcast_fn: F, - ) -> Result - where - F: Fn(Box<[u8]>) -> Fut, - Fut: Future>, - { - // Reset the progress to start. Any errors will get recorded here - self.reset_send_progress().await; - - // Sanity check that this is a spending wallet. Why isn't this done earlier? - if !self.wallet_capability().can_spend_from_all_pools() { - // Creating transactions in context of all possible combinations - // of wallet capabilities requires a rigorous case study - // and can have undesired effects if not implemented properly. - // - // Thus we forbid spending for wallets without complete spending capability for now - return Err("Wallet is in watch-only mode and thus it cannot spend.".to_string()); - } - // Create the transaction - let start_time = now(); - let build_result = self - .create_publication_ready_transaction( - submission_height, - start_time, - receivers, - policy, - sapling_prover, - ) - .await?; - - // Call the internal function - let send_result = self - .send_to_addresses_inner(build_result.transaction(), submission_height, broadcast_fn) - .await; - - self.set_send_result(send_result.clone().map(|txid| txid.to_string())) - .await; - - send_result - } +use thiserror::Error; +#[allow(missing_docs)] // error types document themselves +#[derive(Debug, Error)] +pub enum BuildTransactionError { + #[error("No witness trees. This is viewkey watch, not spendkey wallet.")] + NoSpendCapability, + #[error("Could not load sapling_params: {0:?}")] + SaplingParams(String), + #[error("Could not find UnifiedSpendKey: {0:?}")] + UnifiedSpendKey(#[from] crate::wallet::error::KeyError), + #[error("Can't Calculate {0:?}")] + Calculation( + #[from] + zcash_client_backend::data_api::error::Error< + crate::wallet::tx_map::TxMapTraitError, + std::convert::Infallible, + std::convert::Infallible, + zcash_primitives::transaction::fees::zip317::FeeError, + >, + ), + #[error("Only tex multistep transactions are supported!")] + NonTexMultiStep, +} - async fn create_publication_ready_transaction( +impl LightWallet { + pub(crate) async fn create_transaction( &self, - submission_height: BlockHeight, - start_time: u64, - receivers: Receivers, - policy: NoteSelectionPolicy, - sapling_prover: P, - // We only care about the transaction...but it can now only be aquired by reference - // from the build result, so we need to return the whole thing - ) -> Result { - // Start building transaction with spends and outputs set by: - // * target amount - // * selection policy - // * recipient list - let txmds_readlock = self + proposal: &Proposal, + ) -> Result<(), BuildTransactionError> { + if self .transaction_context .transaction_metadata_set .read() - .await; - let witness_trees = txmds_readlock - .witness_trees() - .ok_or("No spend capability.")?; - let (tx_builder, total_shielded_receivers) = match self - .create_and_populate_tx_builder( - submission_height, - witness_trees, - start_time, - receivers, - policy, - ) .await + .witness_trees() + .is_none() { - Ok(tx_builder) => tx_builder, - Err(s) => { - return Err(s); - } - }; - - drop(txmds_readlock); - // The builder now has the correct set of inputs and outputs - - // Set up a channel to receive updates on the progress of building the transaction. - // This progress monitor, the channel monitoring it, and the types necessary for its - // construction are unnecessary for sending. - let (transmitter, receiver) = channel::(); - let progress = self.send_progress.clone(); - - // Use a separate thread to handle sending from std::mpsc to tokio::sync::mpsc - let (transmitter2, mut receiver2) = tokio::sync::mpsc::unbounded_channel(); - std::thread::spawn(move || { - while let Ok(r) = receiver.recv() { - transmitter2.send(r.cur()).unwrap(); - } - }); - - let progress_handle = tokio::spawn(async move { - while let Some(r) = receiver2.recv().await { - info!("{}: Progress: {r}", now() - start_time); - progress.write().await.progress = r; - } - - progress.write().await.is_send_in_progress = false; - }); - - { - let mut p = self.send_progress.write().await; - p.is_send_in_progress = true; - p.progress = 0; - p.total = total_shielded_receivers; + return Err(BuildTransactionError::NoSpendCapability); } - info!("{}: Building transaction", now() - start_time); - - let tx_builder = tx_builder.with_progress_notifier(transmitter); - let build_result = match tx_builder.build( - OsRng, - &sapling_prover, - &sapling_prover, - &transaction::fees::fixed::FeeRule::non_standard(MINIMUM_FEE), - ) { - Ok(res) => res, - Err(e) => { - let e = format!("Error creating transaction: {:?}", e); - error!("{}", e); - self.send_progress.write().await.is_send_in_progress = false; - return Err(e); - } - }; - progress_handle.await.unwrap(); - Ok(build_result) - } - - async fn create_and_populate_tx_builder( - &self, - submission_height: BlockHeight, - witness_trees: &WitnessTrees, - start_time: u64, - receivers: Receivers, - policy: NoteSelectionPolicy, - ) -> Result<(TxBuilder<'_>, u32), String> { - let fee_rule = - &zcash_primitives::transaction::fees::fixed::FeeRule::non_standard(MINIMUM_FEE); // Start building tx - let mut total_shielded_receivers; - let mut orchard_notes; - let mut sapling_notes; - let mut utxos; - let mut tx_builder; - let mut proposed_fee = MINIMUM_FEE; - let mut total_value_covered_by_selected; - let total_earmarked_for_recipients: u64 = - receivers.iter().map(|to| u64::from(to.amount)).sum(); - info!( - "0: Creating transaction sending {} zatoshis to {} addresses", - total_earmarked_for_recipients, - receivers.len() - ); - loop { - tx_builder = match self - .create_tx_builder(submission_height, witness_trees) - .await - { - Err(ShardTreeError::Query(QueryError::NotContained(addr))) => Err(format!( - "could not create anchor, missing address {addr:?}. \ - If you are fully synced, you may need to rescan to proceed" - )), - Err(ShardTreeError::Query(QueryError::CheckpointPruned)) => { - let blocks = self.blocks.read().await.len(); - let offset = self.transaction_context.config.reorg_buffer_offset; - Err(format!( - "The reorg buffer offset has been set to {} \ - but there are only {} blocks in the wallet. \ - Please sync at least {} more blocks before trying again", - offset, - blocks, - offset + 1 - blocks as u32 - )) - } - Err(ShardTreeError::Query(QueryError::TreeIncomplete(addrs))) => Err(format!( - "could not create anchor, missing addresses {addrs:?}. \ - If you are fully synced, you may need to rescan to proceed" - )), - Err(ShardTreeError::Insert(_)) => unreachable!(), - Err(ShardTreeError::Storage(_infallible)) => unreachable!(), - Ok(v) => Ok(v), - }?; - - // Select notes to cover the target value - info!("{}: Adding outputs", now() - start_time); - (total_shielded_receivers, tx_builder) = self - .add_consumer_specified_outputs_to_builder(tx_builder, receivers.clone()) - .expect("To add outputs"); - - let earmark_total_plus_default_fee = - total_earmarked_for_recipients + u64::from(proposed_fee); - // Select notes as a fn of target amount - ( - orchard_notes, - sapling_notes, - utxos, - total_value_covered_by_selected, - ) = match self - .select_notes_and_utxos( - Amount::from_u64(earmark_total_plus_default_fee) - .expect("Valid amount, from u64."), - &policy, - ) - .await - { - Ok(notes) => notes, - Err(insufficient_amount) => { - let e = format!( - "Insufficient verified shielded funds. Have {} zats, need {} zats. NOTE: funds need at least {} confirmations before they can be spent. Transparent funds must be shielded before they can be spent. If you are trying to spend transparent funds, please use the shield button and try again in a few minutes.", - insufficient_amount, earmark_total_plus_default_fee, self.transaction_context.config - .reorg_buffer_offset + 1 - ); - error!("{}", e); - return Err(e); - } - }; - - info!("Selected notes worth {}", total_value_covered_by_selected); - - info!( - "{}: Adding {} sapling notes, {} orchard notes, and {} utxos", - now() - start_time, - &sapling_notes.len(), - &orchard_notes.len(), - &utxos.len() - ); - - let temp_tx_builder = match self.add_change_output_to_builder( - tx_builder, - Amount::from_u64(earmark_total_plus_default_fee).expect("valid value of u64"), - Amount::from_u64(total_value_covered_by_selected).unwrap(), - &mut total_shielded_receivers, - &receivers, - ) { - Ok(txb) => txb, - Err(r) => { - return Err(r); - } - }; - info!("{}: selecting notes", now() - start_time); - tx_builder = match self - .add_spends_to_builder( - temp_tx_builder, - witness_trees, - &orchard_notes, - &sapling_notes, - &utxos, - ) - .await - { - Ok(tx_builder) => tx_builder, - - Err(s) => { - return Err(s); - } - }; - proposed_fee = tx_builder.get_fee(fee_rule).unwrap(); - if u64::from(proposed_fee) + total_earmarked_for_recipients - <= total_value_covered_by_selected - { - break; - } - } - Ok((tx_builder, total_shielded_receivers)) - } - - async fn create_tx_builder( - &self, - submission_height: BlockHeight, - witness_trees: &WitnessTrees, - ) -> Result> { - let orchard_anchor = self - .get_orchard_anchor(&witness_trees.witness_tree_orchard) - .await?; - let sapling_anchor = self - .get_sapling_anchor(&witness_trees.witness_tree_sapling) - .await?; - Ok(Builder::new( - self.transaction_context.config.chain, - submission_height, - transaction::builder::BuildConfig::Standard { - // TODO: We probably need this - sapling_anchor: Some(sapling_anchor), - orchard_anchor: Some(orchard_anchor), - }, - )) - } - - fn add_consumer_specified_outputs_to_builder<'a>( - &'a self, - mut tx_builder: TxBuilder<'a>, - receivers: Receivers, - ) -> Result<(u32, TxBuilder<'_>), String> { - // Convert address (str) to RecipientAddress and value to Amount - - // We'll use the first ovk to encrypt outgoing transactions - let sapling_ovk = - sapling_crypto::keys::OutgoingViewingKey::try_from(&*self.wallet_capability()).unwrap(); - let orchard_ovk = - orchard::keys::OutgoingViewingKey::try_from(&*self.wallet_capability()).unwrap(); - - let mut total_shielded_receivers = 0u32; - for crate::data::receivers::Receiver { - recipient_address, - amount, - memo, - } in receivers - { - // Compute memo if it exists - let validated_memo = match memo { - None => MemoBytes::from(Memo::Empty), - Some(s) => s, - }; - - if let Err(e) = match recipient_address { - address::Address::Transparent(to) => tx_builder - .add_transparent_output(&to, amount) - .map_err(transaction::builder::Error::TransparentBuild), - address::Address::Sapling(to) => { - total_shielded_receivers += 1; - tx_builder.add_sapling_output(Some(sapling_ovk), to, amount, validated_memo) - } - address::Address::Unified(ua) => { - if let Some(orchard_addr) = ua.orchard() { - total_shielded_receivers += 1; - tx_builder.add_orchard_output::( - Some(orchard_ovk.clone()), - *orchard_addr, - u64::from(amount), - validated_memo, - ) - } else if let Some(sapling_addr) = ua.sapling() { - total_shielded_receivers += 1; - tx_builder.add_sapling_output( - Some(sapling_ovk), - *sapling_addr, - amount, - validated_memo, - ) - } else { - return Err("Received UA with no Orchard or Sapling receiver".to_string()); - } - } - } { - let e = format!("Error adding output: {:?}", e); - error!("{}", e); - return Err(e); - } - } - Ok((total_shielded_receivers, tx_builder)) - } + // Reset the progress to start. Any errors will get recorded here + self.reset_send_progress().await; - async fn get_all_domain_specific_notes(&self) -> Vec - where - D: DomainWalletExt, - ::Recipient: Recipient, - ::Note: PartialEq + Clone, - { - let wc = self.wallet_capability(); - let tranmds_lth = self.transactions(); - let transaction_metadata_set = tranmds_lth.read().await; - let mut candidate_notes = transaction_metadata_set - .transaction_records_by_id - .iter() - .flat_map(|(transaction_id, transaction)| { - D::WalletNote::transaction_metadata_notes(transaction) - .iter() - .map(move |note| (*transaction_id, note)) - }) - .filter_map( - |(transaction_id, note): (transaction::TxId, &D::WalletNote)| -> Option { - // Get the spending key for the selected fvk, if we have it - let extsk = D::wc_to_sk(&wc); - SpendableNote::from(transaction_id, note, extsk.ok().as_ref()) - } - ) - .collect::>(); - candidate_notes.sort_unstable_by(|spendable_note_1, spendable_note_2| { - D::WalletNote::value_from_note(spendable_note_2.note()) - .cmp(&D::WalletNote::value_from_note(spendable_note_1.note())) - }); - candidate_notes - } + let (sapling_output, sapling_spend): (Vec, Vec) = + crate::wallet::utils::read_sapling_params() + .map_err(BuildTransactionError::SaplingParams)?; + let sapling_prover = + zcash_proofs::prover::LocalTxProver::from_bytes(&sapling_spend, &sapling_output); - async fn select_notes_and_utxos( - &self, - target_amount: Amount, - policy: &NoteSelectionPolicy, - ) -> Result< - ( - Vec, - Vec, - Vec, - u64, - ), - u64, - > { - let mut all_transparent_value_in_wallet = Amount::zero(); - let mut utxos = Vec::new(); //utxo stands for Unspent Transaction Output - let mut sapling_value_selected = Amount::zero(); - let mut sapling_notes = Vec::new(); - let mut orchard_value_selected = Amount::zero(); - let mut orchard_notes = Vec::new(); - // Correctness of this loop depends on: - // * uniqueness - for pool in policy { - match pool { - // Transparent: This opportunistic shielding sweeps all transparent value leaking identifying information to - // a funder of the wallet's transparent value. We should change this. - PoolType::Transparent => { - utxos = self - .get_utxos() - .await - .iter() - .filter(|utxo| utxo.pending_spent.is_none() && !utxo.is_spent()) - .cloned() - .collect::>(); - all_transparent_value_in_wallet = - utxos.iter().fold(Amount::zero(), |prev, utxo| { - (prev + Amount::from_u64(utxo.value).unwrap()).unwrap() - }); - } - PoolType::Shielded(ShieldedProtocol::Sapling) => { - let sapling_candidates = self - .get_all_domain_specific_notes::() - .await - .into_iter() - .filter(|note| note.spend_key().is_some()) - .collect(); - (sapling_notes, sapling_value_selected) = add_notes_to_total::( - sapling_candidates, - (target_amount - orchard_value_selected - all_transparent_value_in_wallet) - .unwrap(), - ); - } - PoolType::Shielded(ShieldedProtocol::Orchard) => { - let orchard_candidates = self - .get_all_domain_specific_notes::() - .await - .into_iter() - .filter(|note| note.spend_key().is_some()) - .collect(); - (orchard_notes, orchard_value_selected) = add_notes_to_total::( - orchard_candidates, - (target_amount - all_transparent_value_in_wallet - sapling_value_selected) - .unwrap(), - ); - } + match proposal.steps().len() { + 1 => { + self.create_transaction_helper(sapling_prover, proposal) + .await } - // Check how much we've selected - if (all_transparent_value_in_wallet + sapling_value_selected + orchard_value_selected) - .unwrap() - >= target_amount + 2 if proposal.steps()[1] + .transaction_request() + .payments() + .values() + .any(|payment| { + matches!(payment.recipient_address().kind(), AddressKind::Tex(_)) + }) => { - return Ok(( - orchard_notes, - sapling_notes, - utxos, - u64::try_from( - (all_transparent_value_in_wallet - + sapling_value_selected - + orchard_value_selected) - .unwrap(), - ) - .expect("u64 representable."), - )); + self.create_transaction_helper(sapling_prover, proposal) + .await } - } - // If we can't select enough, then we need to return empty handed - Err(u64::try_from( - (all_transparent_value_in_wallet + sapling_value_selected + orchard_value_selected) - .unwrap(), - ) - .expect("u64 representable")) + _ => Err(BuildTransactionError::NonTexMultiStep), + } } - // TODO: LEGACY. to be deprecated when zip317 lands - fn add_change_output_to_builder<'a>( + async fn create_transaction_helper( &self, - mut tx_builder: TxBuilder<'a>, - target_amount: Amount, - selected_value: Amount, - total_shielded_receivers: &mut u32, - receivers: &Receivers, - ) -> Result, String> { - let destination_uas = receivers - .iter() - .filter_map(|receiver| match receiver.recipient_address { - address::Address::Sapling(_) => None, - address::Address::Transparent(_) => None, - address::Address::Unified(ref ua) => Some(ua.clone()), - }) - .collect::>(); - let uas_bytes = match create_wallet_internal_memo_version_0(destination_uas.as_slice()) { - Ok(bytes) => bytes, - Err(e) => { - log::error!( - "Could not write uas to memo field: {e}\n\ - Your wallet will display an incorrect sent-to address. This is a visual error only.\n\ - The correct address was sent to." - ); - [0; 511] - } - }; - let orchard_ovk = - orchard::keys::OutgoingViewingKey::try_from(&*self.wallet_capability()).unwrap(); - *total_shielded_receivers += 1; - if let Err(e) = tx_builder.add_orchard_output::( - Some(orchard_ovk.clone()), - *self.wallet_capability().addresses()[0].orchard().unwrap(), - u64::try_from(selected_value).expect("u64 representable") - - u64::try_from(target_amount).expect("u64 representable"), - // Here we store the uas we sent to in the memo field. - // These are used to recover the full UA we sent to. - MemoBytes::from(Memo::Arbitrary(Box::new(uas_bytes))), - ) { - let e = format!("Error adding change output: {:?}", e); - error!("{}", e); - return Err(e); - }; - Ok(tx_builder) + sapling_prover: LocalTxProver, + proposal: &Proposal, + ) -> Result<(), BuildTransactionError> { + zcash_client_backend::data_api::wallet::create_proposed_transactions( + self.transaction_context + .transaction_metadata_set + .write() + .await + .deref_mut(), + &self.transaction_context.config.chain, + &sapling_prover, + &sapling_prover, + &self + .transaction_context + .key + .unified_key_store() + .try_into()?, + zcash_client_backend::wallet::OvkPolicy::Sender, + proposal, + Some(self.wallet_capability().first_sapling_address()), + )?; + Ok(()) } +} - async fn add_spends_to_builder<'a>( - &'a self, - mut tx_builder: TxBuilder<'a>, - witness_trees: &WitnessTrees, - orchard_notes: &[SpendableOrchardNote], - sapling_notes: &[SpendableSaplingNote], - utxos: &[notes::TransparentOutput], - ) -> Result, String> { - // Add all tinputs - // Create a map from address -> sk for all taddrs, so we can spend from the - // right address - let address_to_sk = self - .wallet_capability() - .get_taddr_to_secretkey_map(&self.transaction_context.config) - .unwrap(); - - utxos - .iter() - .map(|utxo| { - let outpoint: OutPoint = utxo.to_outpoint(); - - let coin = TxOut { - value: NonNegativeAmount::from_u64(utxo.value).unwrap(), - script_pubkey: Script(utxo.script.clone()), - }; - - match address_to_sk.get(&utxo.address) { - Some(sk) => tx_builder - .add_transparent_input(*sk, outpoint, coin) - .map_err(|e| { - transaction::builder::Error::::TransparentBuild(e) - }), - None => { - // Something is very wrong - let e = format!("Couldn't find the secretkey for taddr {}", utxo.address); - error!("{}", e); - - Err(transaction::builder::Error::::TransparentBuild( - transaction::components::transparent::builder::Error::InvalidAddress, - )) - } +// TODO: move to a more suitable place +pub(crate) fn change_memo_from_transaction_request( + request: &TransactionRequest, + mut num_ephemeral_addresses: u32, +) -> MemoBytes { + let mut recipient_uas = Vec::new(); + let mut ephemeral_address_indexes = Vec::new(); + for payment in request.payments().values() { + match payment.recipient_address().kind() { + AddressKind::Unified(ua) => { + if let Ok(ua) = UnifiedAddress::try_from(ua.clone()) { + recipient_uas.push(ua); } - }) - .collect::, _>>() - .map_err(|e| format!("{:?}", e))?; - - for selected in sapling_notes.iter() { - info!("Adding sapling spend"); - // Turbofish only needed for error type - if let Err(e) = tx_builder.add_sapling_spend::( - &selected.extsk.clone().unwrap(), - selected.note.clone(), - witness_trees - .witness_tree_sapling - .witness_at_checkpoint_depth( - selected.witnessed_position, - self.transaction_context.config.reorg_buffer_offset as usize, - ) - .map_err(|e| format!("failed to compute sapling witness: {e}"))?, - ) { - let e = format!("Error adding note: {:?}", e); - error!("{}", e); - return Err(e); } - } + AddressKind::Tex(_) => { + ephemeral_address_indexes.push(num_ephemeral_addresses); - for selected in orchard_notes.iter() { - info!("Adding orchard spend"); - if let Err(e) = tx_builder.add_orchard_spend::( - &selected.spend_key.unwrap(), - selected.note, - orchard::tree::MerklePath::from( - witness_trees - .witness_tree_orchard - .witness_at_checkpoint_depth( - selected.witnessed_position, - self.transaction_context.config.reorg_buffer_offset as usize, - ) - .map_err(|e| format!("failed to compute orchard witness: {e}"))?, - ), - ) { - let e = format!("Error adding note: {:?}", e); - error!("{}", e); - return Err(e); + num_ephemeral_addresses += 1; } + _ => (), } - Ok(tx_builder) } - - pub(crate) async fn send_to_addresses_inner( - &self, - transaction: &Transaction, - submission_height: BlockHeight, - broadcast_fn: F, - ) -> Result - where - F: Fn(Box<[u8]>) -> Fut, - Fut: Future>, - { - { - self.send_progress.write().await.is_send_in_progress = false; - } - - // Create the transaction bytes - let mut raw_transaction = vec![]; - transaction.write(&mut raw_transaction).unwrap(); - - let serverz_transaction_id = - broadcast_fn(raw_transaction.clone().into_boxed_slice()).await?; - - // Add this transaction to the mempool structure - { - let price = self.price.read().await.clone(); - - let status = ConfirmationStatus::Pending(submission_height); - self.transaction_context - .scan_full_tx( - transaction, - status, - Some(now() as u32), - get_price(now(), &price), - ) - .await; - } - - let calculated_txid = transaction.txid(); - - let accepted_txid = match crate::utils::conversion::txid_from_hex_encoded_str( - serverz_transaction_id.as_str(), - ) { - Ok(serverz_txid) => { - if calculated_txid != serverz_txid { - // happens during darkside tests - error!( - "served txid {} does not match calulated txid {}", - serverz_txid, calculated_txid, - ); - }; - if self.transaction_context.config.accept_server_txids { - serverz_txid - } else { - calculated_txid - } - } - Err(e) => { - error!("server returned invalid txid {}", e); - calculated_txid - } - }; - - Ok(accepted_txid) - } -} - -// TODO: move to a more suitable place -pub(crate) fn change_memo_from_transaction_request(request: &TransactionRequest) -> MemoBytes { - let recipient_uas = request - .payments() - .iter() - .filter_map(|(_, payment)| match payment.recipient_address { - Address::Transparent(_) => None, - Address::Sapling(_) => None, - Address::Unified(ref ua) => Some(ua.clone()), - }) - .collect::>(); - let uas_bytes = match create_wallet_internal_memo_version_0(recipient_uas.as_slice()) { + let uas_bytes = match create_wallet_internal_memo_version_1( + recipient_uas.as_slice(), + ephemeral_address_indexes.as_slice(), + ) { Ok(bytes) => bytes, Err(e) => { log::error!( @@ -900,12 +229,12 @@ pub(crate) fn change_memo_from_transaction_request(request: &TransactionRequest) mod tests { use std::str::FromStr; - use zcash_client_backend::{address::Address, zip321::TransactionRequest}; + use zcash_address::ZcashAddress; + use zcash_client_backend::zip321::TransactionRequest; use zcash_primitives::{ memo::{Memo, MemoBytes}, transaction::components::amount::NonNegativeAmount, }; - use zingoconfig::ChainType; use crate::data::receivers::{transaction_request_from_receivers, Receivers}; @@ -913,12 +242,12 @@ mod tests { fn test_build_request() { let amount_1 = NonNegativeAmount::const_from_u64(20000); let recipient_address_1 = - Address::decode(&ChainType::Testnet, "utest17wwv8nuvdnpjsxtu6ndz6grys5x8wphcwtzmg75wkx607c7cue9qz5kfraqzc7k9dfscmylazj4nkwazjj26s9rhyjxm0dcqm837ykgh2suv0at9eegndh3kvtfjwp3hhhcgk55y9d2ys56zkw8aaamcrv9cy0alj0ndvd0wll4gxhrk9y4yy9q9yg8yssrencl63uznqnkv7mk3w05").unwrap(); + ZcashAddress::try_from_encoded("utest17wwv8nuvdnpjsxtu6ndz6grys5x8wphcwtzmg75wkx607c7cue9qz5kfraqzc7k9dfscmylazj4nkwazjj26s9rhyjxm0dcqm837ykgh2suv0at9eegndh3kvtfjwp3hhhcgk55y9d2ys56zkw8aaamcrv9cy0alj0ndvd0wll4gxhrk9y4yy9q9yg8yssrencl63uznqnkv7mk3w05").unwrap(); let memo_1 = None; let amount_2 = NonNegativeAmount::const_from_u64(20000); let recipient_address_2 = - Address::decode(&ChainType::Testnet, "utest17wwv8nuvdnpjsxtu6ndz6grys5x8wphcwtzmg75wkx607c7cue9qz5kfraqzc7k9dfscmylazj4nkwazjj26s9rhyjxm0dcqm837ykgh2suv0at9eegndh3kvtfjwp3hhhcgk55y9d2ys56zkw8aaamcrv9cy0alj0ndvd0wll4gxhrk9y4yy9q9yg8yssrencl63uznqnkv7mk3w05").unwrap(); + ZcashAddress::try_from_encoded("utest17wwv8nuvdnpjsxtu6ndz6grys5x8wphcwtzmg75wkx607c7cue9qz5kfraqzc7k9dfscmylazj4nkwazjj26s9rhyjxm0dcqm837ykgh2suv0at9eegndh3kvtfjwp3hhhcgk55y9d2ys56zkw8aaamcrv9cy0alj0ndvd0wll4gxhrk9y4yy9q9yg8yssrencl63uznqnkv7mk3w05").unwrap(); let memo_2 = Some(MemoBytes::from( Memo::from_str("the lake wavers along the beach").expect("string can memofy"), )); diff --git a/zingolib/src/wallet/sync.rs b/zingolib/src/wallet/sync.rs new file mode 100644 index 0000000000..b4b8be5eb5 --- /dev/null +++ b/zingolib/src/wallet/sync.rs @@ -0,0 +1,96 @@ +//! Trait implmentations for sync interface + +use std::{ + collections::{BTreeMap, HashMap}, + sync::atomic, +}; + +use zcash_keys::keys::{UnifiedFullViewingKey, UnifiedSpendingKey}; +use zcash_primitives::consensus::BlockHeight; +use zingo_sync::{ + interface::{SyncBlocks, SyncNullifiers, SyncShardTrees, SyncWallet}, + primitives::{NullifierMap, SyncState, WalletBlock}, + witness::ShardTrees, +}; +use zip32::AccountId; + +use crate::wallet::LightWallet; + +impl SyncWallet for LightWallet { + type Error = (); + + fn get_birthday(&self) -> Result { + let birthday = self.birthday.load(atomic::Ordering::Relaxed); + Ok(BlockHeight::from_u32(birthday as u32)) + } + + fn get_sync_state_mut(&mut self) -> Result<&mut SyncState, Self::Error> { + Ok(self.sync_state_mut()) + } + + fn get_unified_full_viewing_keys( + &self, + ) -> Result, Self::Error> { + let account_id = AccountId::try_from(0).unwrap(); + let seed = self + .mnemonic() + .map(|(mmemonic, _)| mmemonic) + .unwrap() + .to_seed(""); + let usk = UnifiedSpendingKey::from_seed( + &self.transaction_context.config.chain, + &seed, + account_id, + ) + .unwrap(); + let ufvk = usk.to_unified_full_viewing_key(); + let mut ufvk_map = HashMap::new(); + ufvk_map.insert(account_id, ufvk); + + Ok(ufvk_map) + } +} + +impl SyncBlocks for LightWallet { + fn get_wallet_block(&self, block_height: BlockHeight) -> Result { + self.wallet_blocks.get(&block_height).cloned().ok_or(()) + } + + fn get_wallet_blocks_mut( + &mut self, + ) -> Result<&mut BTreeMap, Self::Error> { + Ok(self.wallet_blocks_mut()) + } + + fn append_wallet_blocks( + &mut self, + mut wallet_compact_blocks: BTreeMap, + ) -> Result<(), Self::Error> { + self.wallet_blocks.append(&mut wallet_compact_blocks); + + Ok(()) + } +} + +impl SyncNullifiers for LightWallet { + fn get_nullifiers_mut(&mut self) -> Result<&mut NullifierMap, ()> { + Ok(self.nullifier_map_mut()) + } + + fn append_nullifiers(&mut self, mut nullifier_map: NullifierMap) -> Result<(), Self::Error> { + self.nullifier_map + .sapling_mut() + .append(nullifier_map.sapling_mut()); + self.nullifier_map + .orchard_mut() + .append(nullifier_map.orchard_mut()); + + Ok(()) + } +} + +impl SyncShardTrees for LightWallet { + fn get_shard_trees_mut(&mut self) -> Result<&mut ShardTrees, Self::Error> { + Ok(self.shard_trees_mut()) + } +} diff --git a/zingolib/src/wallet/traits.rs b/zingolib/src/wallet/traits.rs index cd15e5c23f..7cf872fcb5 100644 --- a/zingolib/src/wallet/traits.rs +++ b/zingolib/src/wallet/traits.rs @@ -2,6 +2,7 @@ use crate::wallet::notes::interface::OutputConstructor; use std::io::{self, Read, Write}; +use crate::config::ChainType; use crate::data::witness_trees::WitnessTrees; use crate::wallet::notes::OutputInterface; use crate::wallet::notes::ShieldedNoteInterface; @@ -12,7 +13,7 @@ use crate::wallet::{ }, keys::unified::WalletCapability, notes::{OrchardNote, SaplingNote}, - tx_map_and_maybe_trees::TxMapAndMaybeTrees, + tx_map::TxMap, }; use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; use incrementalmerkletree::{witness::IncrementalWitness, Hashable, Level, Position}; @@ -50,7 +51,9 @@ use zcash_primitives::{ Transaction, TxId, }, }; -use zingoconfig::ChainType; +use zingo_status::confirmation_status::ConfirmationStatus; + +use super::keys::unified::UnifiedKeyStore; /// This provides a uniform `.to_bytes` to types that might require it in a generic context. pub trait ToBytes { @@ -468,11 +471,11 @@ pub trait DomainWalletExt: type Fvk: Clone + Send + Diversifiable - + for<'a> TryFrom<&'a WalletCapability> + + for<'a> TryFrom<&'a UnifiedKeyStore> + super::keys::unified::Fvk; /// TODO: Add Doc Comment Here! - type SpendingKey: for<'a> TryFrom<&'a WalletCapability> + Clone; + type SpendingKey: for<'a> TryFrom<&'a UnifiedKeyStore> + Clone; /// TODO: Add Doc Comment Here! type CompactOutput: CompactOutput; /// TODO: Add Doc Comment Here! @@ -497,7 +500,7 @@ pub trait DomainWalletExt: /// TODO: Add Doc Comment Here! fn transaction_metadata_set_to_shardtree( - txmds: &TxMapAndMaybeTrees, + txmds: &TxMap, ) -> Option<&MemoryStoreShardTree<::Node>> { txmds .witness_trees() @@ -506,7 +509,7 @@ pub trait DomainWalletExt: /// TODO: Add Doc Comment Here! fn transaction_metadata_set_to_shardtree_mut( - txmds: &mut TxMapAndMaybeTrees, + txmds: &mut TxMap, ) -> Option<&mut MemoryStoreShardTree<::Node>> { txmds .witness_trees_mut() @@ -540,10 +543,7 @@ pub trait DomainWalletExt: ) -> Option<&'a UnifiedAddress>; /// TODO: Add Doc Comment Here! - fn wc_to_fvk(wc: &WalletCapability) -> Result; - - /// TODO: Add Doc Comment Here! - fn wc_to_sk(wc: &WalletCapability) -> Result; + fn unified_key_store_to_fvk(unified_key_store: &UnifiedKeyStore) -> Result; } impl DomainWalletExt for SaplingDomain { @@ -605,12 +605,8 @@ impl DomainWalletExt for SaplingDomain { .find(|ua| ua.sapling() == Some(receiver)) } - fn wc_to_fvk(wc: &WalletCapability) -> Result { - Self::Fvk::try_from(wc) - } - - fn wc_to_sk(wc: &WalletCapability) -> Result { - Self::SpendingKey::try_from(wc) + fn unified_key_store_to_fvk(unified_key_store: &UnifiedKeyStore) -> Result { + Self::Fvk::try_from(unified_key_store).map_err(|e| e.to_string()) } } @@ -673,12 +669,8 @@ impl DomainWalletExt for OrchardDomain { .find(|unified_address| unified_address.orchard() == Some(receiver)) } - fn wc_to_fvk(wc: &WalletCapability) -> Result { - Self::Fvk::try_from(wc) - } - - fn wc_to_sk(wc: &WalletCapability) -> Result { - Self::SpendingKey::try_from(wc) + fn unified_key_store_to_fvk(unified_key_store: &UnifiedKeyStore) -> Result { + Self::Fvk::try_from(unified_key_store).map_err(|e| e.to_string()) } } @@ -764,8 +756,7 @@ where note_and_metadata: &D::WalletNote, spend_key: Option<&D::SpendingKey>, ) -> bool { - note_and_metadata.spent().is_none() - && note_and_metadata.pending_spent().is_none() + note_and_metadata.spending_tx_status().is_none() && spend_key.is_some() && note_and_metadata.value() != 0 } @@ -889,15 +880,15 @@ impl SpendableNote for SpendableOrchardNote { } /// TODO: Add Doc Comment Here! -pub trait ReadableWriteable: Sized { +pub trait ReadableWriteable: Sized { /// TODO: Add Doc Comment Here! const VERSION: u8; /// TODO: Add Doc Comment Here! - fn read(reader: R, input: Input) -> io::Result; + fn read(reader: R, input: ReadInput) -> io::Result; /// TODO: Add Doc Comment Here! - fn write(&self, writer: W) -> io::Result<()>; + fn write(&self, writer: W, input: WriteInput) -> io::Result<()>; /// TODO: Add Doc Comment Here! fn get_version(mut reader: R) -> io::Result { @@ -916,10 +907,10 @@ pub trait ReadableWriteable: Sized { } } -impl ReadableWriteable<()> for orchard::keys::SpendingKey { +impl ReadableWriteable for orchard::keys::SpendingKey { const VERSION: u8 = 0; //Not applicable - fn read(mut reader: R, _: ()) -> io::Result { + fn read(mut reader: R, _input: ()) -> io::Result { let mut data = [0u8; 32]; reader.read_exact(&mut data)?; @@ -931,27 +922,27 @@ impl ReadableWriteable<()> for orchard::keys::SpendingKey { }) } - fn write(&self, mut writer: W) -> io::Result<()> { + fn write(&self, mut writer: W, _input: ()) -> io::Result<()> { writer.write_all(self.to_bytes()) } } -impl ReadableWriteable<()> for sapling_crypto::zip32::ExtendedSpendingKey { +impl ReadableWriteable for sapling_crypto::zip32::ExtendedSpendingKey { const VERSION: u8 = 0; //Not applicable - fn read(reader: R, _: ()) -> io::Result { + fn read(reader: R, _input: ()) -> io::Result { Self::read(reader) } - fn write(&self, writer: W) -> io::Result<()> { + fn write(&self, writer: W, _input: ()) -> io::Result<()> { self.write(writer) } } -impl ReadableWriteable<()> for sapling_crypto::zip32::DiversifiableFullViewingKey { +impl ReadableWriteable for sapling_crypto::zip32::DiversifiableFullViewingKey { const VERSION: u8 = 0; //Not applicable - fn read(mut reader: R, _: ()) -> io::Result { + fn read(mut reader: R, _input: ()) -> io::Result { let mut fvk_bytes = [0u8; 128]; reader.read_exact(&mut fvk_bytes)?; sapling_crypto::zip32::DiversifiableFullViewingKey::from_bytes(&fvk_bytes).ok_or( @@ -962,19 +953,19 @@ impl ReadableWriteable<()> for sapling_crypto::zip32::DiversifiableFullViewingKe ) } - fn write(&self, mut writer: W) -> io::Result<()> { + fn write(&self, mut writer: W, _input: ()) -> io::Result<()> { writer.write_all(&self.to_bytes()) } } -impl ReadableWriteable<()> for orchard::keys::FullViewingKey { +impl ReadableWriteable for orchard::keys::FullViewingKey { const VERSION: u8 = 0; //Not applicable - fn read(reader: R, _: ()) -> io::Result { + fn read(reader: R, _input: ()) -> io::Result { Self::read(reader) } - fn write(&self, writer: W) -> io::Result<()> { + fn write(&self, writer: W, _input: ()) -> io::Result<()> { self.write(writer) } } @@ -991,17 +982,19 @@ impl ReadableWriteable<(sapling_crypto::Diversifier, &WalletCapability)> for sap let rseed = super::data::read_sapling_rseed(&mut reader)?; Ok( - ::wc_to_fvk(wallet_capability) - .expect("to get an fvk from a wc") - .fvk() - .vk - .to_payment_address(diversifier) - .unwrap() - .create_note(sapling_crypto::value::NoteValue::from_raw(value), rseed), + ::unified_key_store_to_fvk( + wallet_capability.unified_key_store(), + ) + .expect("to get an fvk from the unified key store") + .fvk() + .vk + .to_payment_address(diversifier) + .unwrap() + .create_note(sapling_crypto::value::NoteValue::from_raw(value), rseed), ) } - fn write(&self, mut writer: W) -> io::Result<()> { + fn write(&self, mut writer: W, _input: ()) -> io::Result<()> { writer.write_u8(Self::VERSION)?; writer.write_u64::(self.value().inner())?; super::data::write_sapling_rseed(&mut writer, self.rseed())?; @@ -1034,8 +1027,10 @@ impl ReadableWriteable<(orchard::keys::Diversifier, &WalletCapability)> for orch "Nullifier not for note", ))?; - let fvk = ::wc_to_fvk(wallet_capability) - .expect("to get an fvk from a wc"); + let fvk = ::unified_key_store_to_fvk( + wallet_capability.unified_key_store(), + ) + .expect("to get an fvk from the unified key store"); Option::from(orchard::note::Note::from_parts( fvk.address(diversifier, orchard::keys::Scope::External), orchard::value::NoteValue::from_raw(value), @@ -1045,7 +1040,7 @@ impl ReadableWriteable<(orchard::keys::Diversifier, &WalletCapability)> for orch .ok_or(io::Error::new(io::ErrorKind::InvalidInput, "Invalid note")) } - fn write(&self, mut writer: W) -> io::Result<()> { + fn write(&self, mut writer: W, _input: ()) -> io::Result<()> { writer.write_u8(Self::VERSION)?; writer.write_u64::(self.value().inner())?; writer.write_all(&self.rho().to_bytes())?; @@ -1092,8 +1087,10 @@ where reader.read_exact(&mut diversifier_bytes)?; let diversifier = T::Diversifier::from_bytes(diversifier_bytes); - let note = - >::read(&mut reader, (diversifier, wallet_capability))?; + let note = >::read( + &mut reader, + (diversifier, wallet_capability), + )?; let witnessed_position = if external_version >= 4 { Position::from(reader.read_u64::()?) @@ -1130,6 +1127,9 @@ where Ok((TxId::from_bytes(transaction_id_bytes), height)) })?; + let spend = + spent.map(|(txid, height)| (txid, ConfirmationStatus::Confirmed(height.into()))); + if external_version < 3 { let _pending_spent = { Optional::read(&mut reader, |r| { @@ -1177,8 +1177,7 @@ where note, Some(witnessed_position), Some(nullifier), - spent, - None, + spend, memo, is_change, have_spending_key, @@ -1186,13 +1185,13 @@ where )) } - fn write(&self, mut writer: W) -> io::Result<()> { + fn write(&self, mut writer: W, _input: ()) -> io::Result<()> { // Write a version number first, so we can later upgrade this if needed. writer.write_u8(Self::VERSION)?; writer.write_all(&self.diversifier().to_bytes())?; - self.note().write(&mut writer)?; + self.note().write(&mut writer, ())?; writer.write_u64::(u64::from(self.witnessed_position().ok_or( io::Error::new( io::ErrorKind::InvalidData, @@ -1210,12 +1209,17 @@ where .to_bytes(), )?; + let confirmed_spend = self + .spending_tx_status() + .as_ref() + .and_then(|(txid, status)| status.get_confirmed_height().map(|height| (txid, height))); + Optional::write( &mut writer, - self.spent().as_ref(), + confirmed_spend, |w, (transaction_id, height)| { w.write_all(transaction_id.as_ref())?; - w.write_u32::(*height) + w.write_u32::(height.into()) }, )?; diff --git a/zingolib/src/wallet/transaction_context.rs b/zingolib/src/wallet/transaction_context.rs index 98b1b1a20a..1c10ac84ae 100644 --- a/zingolib/src/wallet/transaction_context.rs +++ b/zingolib/src/wallet/transaction_context.rs @@ -3,13 +3,13 @@ use std::sync::Arc; use tokio::sync::RwLock; +use crate::config::ZingoConfig; use zcash_client_backend::ShieldedProtocol; use zcash_primitives::{consensus::BlockHeight, transaction::TxId}; -use zingoconfig::ZingoConfig; -use crate::wallet::{keys::unified::WalletCapability, tx_map_and_maybe_trees::TxMapAndMaybeTrees}; +use crate::wallet::{keys::unified::WalletCapability, tx_map::TxMap}; -/// TODO: Add Doc Comment Here! +/// All the data you need to propose, build, and sign a transaction. #[derive(Clone)] pub struct TransactionContext { /// TODO: Add Doc Comment Here! @@ -17,7 +17,7 @@ pub struct TransactionContext { /// TODO: Add Doc Comment Here! pub(crate) key: Arc, /// TODO: Add Doc Comment Here! - pub transaction_metadata_set: Arc>, + pub transaction_metadata_set: Arc>, } impl TransactionContext { @@ -25,7 +25,7 @@ impl TransactionContext { pub fn new( config: &ZingoConfig, key: Arc, - transaction_metadata_set: Arc>, + transaction_metadata_set: Arc>, ) -> Self { Self { config: config.clone(), @@ -54,24 +54,32 @@ impl TransactionContext { } /// These functions are responsible for receiving a full Transaction and storing it, with a few major caveats. -/// The first layer is CompactTransaction. see fn trial_decrypt_domain_specific_outputs -/// in some cases, including send, read memos, discover outgoing transaction (mark change / scan for internal change), additional information and processing are required -/// some of the steps in scan_full_tx are similar to or repeat steps in trial_ddso -/// however, scan full tx is more limited than trial_ddso. -/// scan_full_tx has no access to a transmitter. So it is incapable of **sending a request on a transmitter for another task to fetch a witnessed position**. -/// unlike a viewkey wallet, a spendkey wallet MUST pass reread the block to find a witnessed position to pass to add_new_note. scan_full_tx cannot do this. +/// The first layer is CompactTransaction. +/// See trial_decrypt_domain_specific_outputs in some cases, including: +/// * send +/// * read memos +/// * discover outgoing transaction +/// * (mark change / scan for internal change) +/// +/// Additional information and processing are required some of the steps in scan_full_tx are similar to or +/// repeat steps in trial_ddso however, scan_full_tx is more limited than trial_ddso: +/// * scan_full_tx has no access to a transmitter. So it is incapable of **sending a request on a transmitter for another task to fetch a witnessed position**. +/// +/// Unlike a viewkey wallet, a spendkey wallet MUST pass reread the block to find a witnessed position to pass to add_new_note. scan_full_tx cannot do this. /// thus, scan_full_tx is incomplete and skips some steps on the assumption that they will be covered elsewhere. Notably, add_note is not called inside scan_full_tx. /// (A viewkey wallet, on the other hand, doesnt need witness and could maybe get away with only calling scan_full_tx) -pub mod decrypt_transaction { +mod decrypt_transaction { use crate::{ error::{ZingoLibError, ZingoLibResult}, + utils::interpret_taddr_as_tex_addr, wallet::{ + self, data::OutgoingTxData, keys::{ address_from_pubkeyhash, - unified::{External, Fvk}, + unified::{External, Fvk, Ivk}, }, - notes::ShieldedNoteInterface, + notes::{ShieldedNoteInterface, TransparentOutput}, traits::{ self as zingo_traits, Bundle as _, DomainWalletExt, Recipient as _, ShieldedOutputExt as _, Spend as _, ToBytes as _, @@ -81,13 +89,17 @@ pub mod decrypt_transaction { }; use orchard::note_encryption::OrchardDomain; use sapling_crypto::note_encryption::SaplingDomain; - use std::{collections::HashSet, convert::TryInto}; + use std::convert::TryInto; use zcash_client_backend::address::{Address, UnifiedAddress}; - use zcash_note_encryption::try_output_recovery_with_ovk; + use zcash_note_encryption::{try_output_recovery_with_ovk, Domain}; use zcash_primitives::{ + legacy::TransparentAddress, memo::{Memo, MemoBytes}, - transaction::{Transaction, TxId}, + transaction::{ + components::{transparent::Authorized, TxIn}, + Transaction, TxId, + }, }; use zingo_memo::{parse_zingo_memo, ParsedMemo}; use zingo_status::confirmation_status::ConfirmationStatus; @@ -95,6 +107,7 @@ pub mod decrypt_transaction { use super::TransactionContext; impl TransactionContext { + /// TODO: Extend error handling up from memo read pub(crate) async fn scan_full_tx( &self, transaction: &Transaction, @@ -105,10 +118,8 @@ pub mod decrypt_transaction { // Set up data structures to record scan results let mut txid_indexed_zingo_memos = Vec::new(); - // Collect our t-addresses for easy checking - let taddrs_set = self.key.get_all_taddrs(&self.config); - let mut outgoing_metadatas = vec![]; + let mut sent_to_tex = false; // Execute scanning operations self.decrypt_transaction_to_record( @@ -117,41 +128,13 @@ pub mod decrypt_transaction { block_time, &mut outgoing_metadatas, &mut txid_indexed_zingo_memos, - &taddrs_set, + &mut sent_to_tex, ) .await; // Post process scan results - { - let tx_map = self.transaction_metadata_set.write().await; - if let Some(transaction_record) = - tx_map.transaction_records_by_id.get(&transaction.txid()) - { - // `transaction_kind` uses outgoing_tx_data to determine the SendType but not to distinguish Sent(_) from Received - // therefore, its safe to use it here to establish whether the transaction was created by this capacility or not. - if let TransactionKind::Sent(_) = tx_map - .transaction_records_by_id - .transaction_kind(transaction_record, &self.config.chain) - { - if let Some(t_bundle) = transaction.transparent_bundle() { - for vout in &t_bundle.vout { - if let Some(taddr) = vout.recipient_address().map(|raw_taddr| { - address_from_pubkeyhash(&self.config, raw_taddr) - }) { - if !taddrs_set.contains(&taddr) { - outgoing_metadatas.push(OutgoingTxData { - recipient_address: taddr, - value: u64::from(vout.value), - memo: Memo::Empty, - recipient_ua: None, - }); - } - } - } - } - } - } - } + self.post_process_scan_results(transaction, &mut outgoing_metadatas, sent_to_tex) + .await; if !outgoing_metadatas.is_empty() { self.transaction_metadata_set @@ -161,8 +144,9 @@ pub mod decrypt_transaction { .add_outgoing_metadata(&transaction.txid(), outgoing_metadatas); } - self.update_outgoing_txdatas_with_uas(txid_indexed_zingo_memos) - .await; + self.update_from_zingomemos(txid_indexed_zingo_memos) + .await + .expect("Zingo Memo data has been successfully applied without error."); // Update price if available if price.is_some() { @@ -182,7 +166,7 @@ pub mod decrypt_transaction { block_time: Option, outgoing_metadatas: &mut Vec, arbitrary_memos_with_txids: &mut Vec<(ParsedMemo, TxId)>, - taddrs_set: &HashSet, + sent_to_tex: &mut bool, ) { //todo: investigate scanning all bundles simultaneously @@ -190,7 +174,7 @@ pub mod decrypt_transaction { transaction, status, block_time, - taddrs_set, + sent_to_tex, ) .await; self.decrypt_transaction_to_record_sapling( @@ -216,65 +200,141 @@ pub mod decrypt_transaction { transaction: &Transaction, status: ConfirmationStatus, block_time: Option, - taddrs_set: &HashSet, + sent_to_tex: &mut bool, ) { // Scan all transparent outputs to see if we received any money + self.account_for_transparent_receipts(transaction, status, block_time) + .await; + // Scan transparent spends + self.account_for_transparent_spending(transaction, status, block_time, sent_to_tex) + .await; + } + + /// This method records that receipt in the relevant receiving + /// TransactionRecord in the TransactionRecordsById database. + async fn record_taddr_receipt( + &self, + transaction: &zcash_primitives::transaction::Transaction, + status: ConfirmationStatus, + output_taddr: String, + block_time: Option, + vout: &zcash_primitives::transaction::components::TxOut, + n: usize, + ) { + self.transaction_metadata_set + .write() + .await + .transaction_records_by_id + .add_new_taddr_output( + transaction.txid(), + output_taddr.clone(), + status, + block_time, + vout, + n as u32, + ); + } + /// New value has been detected for one of the wallet's transparent + /// keys. This method accounts for this by updating the relevant + /// receiving TransactionRecord in the TransactionRecordsById database. + async fn account_for_transparent_receipts( + &self, + transaction: &Transaction, + status: ConfirmationStatus, + block_time: Option, + ) { if let Some(t_bundle) = transaction.transparent_bundle() { + // Collect our t-addresses for easy checking + // the get_taddres method includes epehemeral 320 taddrs + let taddrs_set = self.key.get_taddrs(&self.config.chain); for (n, vout) in t_bundle.vout.iter().enumerate() { if let Some(taddr) = vout.recipient_address() { let output_taddr = address_from_pubkeyhash(&self.config, taddr); if taddrs_set.contains(&output_taddr) { - // This is our address. Add this as an output to the txid - self.transaction_metadata_set - .write() - .await - .transaction_records_by_id - .add_new_taddr_output( - transaction.txid(), - output_taddr.clone(), - status, - block_time, - vout, - n as u32, - ); + self.record_taddr_receipt( + transaction, + status, + output_taddr, + block_time, + vout, + n, + ) + .await; } } } } - - // Scan transparent spends - + } + async fn get_spent_utxo( + &self, + vin: &TxIn, + ) -> Option<(TransparentOutput, TxId, u64)> { + let current_transaction_records_by_id = &self + .transaction_metadata_set + .read() + .await + .transaction_records_by_id; + let prev_transaction_id = TxId::from_bytes(*vin.prevout.hash()); + let prev_n = vin.prevout.n() as u64; + if let Some(prev_tx_record) = + current_transaction_records_by_id.get(&prev_transaction_id) + { + // One of the tx outputs is a match + prev_tx_record + .transparent_outputs + .iter() + .find(|u| u.txid == prev_transaction_id && u.output_index == prev_n) + .map(|spent_utxo| (spent_utxo.clone(), prev_transaction_id, prev_n)) + } else { + None + } + } + // Za has ds-integration with the name "ephermal" address. + fn identify_rejection_address(&self, spent_utxo: TransparentOutput) -> Option { + if self + .key + .get_ephemeral_taddrs(&self.config.chain) + .contains(&spent_utxo.address) + { + Some(spent_utxo.address) + } else { + None + } + } + async fn account_for_transparent_spending( + &self, + transaction: &Transaction, + status: ConfirmationStatus, + block_time: Option, + sent_to_tex: &mut bool, + ) { // Scan all the inputs to see if we spent any transparent funds in this tx - let mut total_transparent_value_spent = 0; let mut spent_utxos = vec![]; + let mut total_transparent_value_spent = 0; { - let current_transaction_records_by_id = &self - .transaction_metadata_set - .read() - .await - .transaction_records_by_id; + // We are the authors of the 320 transactions (that we care about), therefore + // we have the transaction that encumbered the (misanmed) "ephemeral" taddr + // with funds. + // We backtrack from the vins of every transaction to check for "ephemeral" addresses + // (from zingomemo), to determine if a given vin is the source of funds for a "tex" + // address. That is, we know the ephemeral taddrs, we simply need to check if they are + // the receiver in a given transaction. If the are then, their spend identifies a tex. if let Some(t_bundle) = transaction.transparent_bundle() { for vin in t_bundle.vin.iter() { - // Find the prev txid that was spent - let prev_transaction_id = TxId::from_bytes(*vin.prevout.hash()); - let prev_n = vin.prevout.n() as u64; - - if let Some(wtx) = - current_transaction_records_by_id.get(&prev_transaction_id) + if let Some((spent_utxo, prev_transaction_id, prev_n)) = + self.get_spent_utxo(vin).await { - // One of the tx outputs is a match - if let Some(spent_utxo) = wtx - .transparent_outputs - .iter() - .find(|u| u.txid == prev_transaction_id && u.output_index == prev_n) + total_transparent_value_spent += spent_utxo.value; + spent_utxos.push(( + prev_transaction_id, + prev_n as u32, + transaction.txid(), + )); + if let Some(_rejection_address) = + self.identify_rejection_address(spent_utxo) { - total_transparent_value_spent += spent_utxo.value; - spent_utxos.push(( - prev_transaction_id, - prev_n as u32, - transaction.txid(), - )); + *sent_to_tex = true; } } } @@ -305,60 +365,6 @@ pub mod decrypt_transaction { } } - async fn update_outgoing_txdatas_with_uas( - &self, - txid_indexed_zingo_memos: Vec<(ParsedMemo, TxId)>, - ) { - for (parsed_zingo_memo, txid) in txid_indexed_zingo_memos { - match parsed_zingo_memo { - ParsedMemo::Version0 { uas } => { - for ua in uas { - if let Some(transaction) = self - .transaction_metadata_set - .write() - .await - .transaction_records_by_id - .get_mut(&txid) - { - if !transaction.outgoing_tx_data.is_empty() { - let outgoing_potential_receivers = [ - ua.orchard().map(|oaddr| { - oaddr.b32encode_for_network(&self.config.chain) - }), - ua.sapling().map(|zaddr| { - zaddr.b32encode_for_network(&self.config.chain) - }), - ua.transparent().map(|taddr| { - address_from_pubkeyhash(&self.config, *taddr) - }), - Some(ua.encode(&self.config.chain)), - ]; - transaction - .outgoing_tx_data - .iter_mut() - .filter(|out_meta| { - outgoing_potential_receivers - .contains(&Some(out_meta.recipient_address.clone())) - }) - .for_each(|out_metadata| { - out_metadata.recipient_ua = - Some(ua.encode(&self.config.chain)) - }) - } - } - } - } - other_memo_version => { - log::error!( - "Wallet internal memo is from a future version of the protocol\n\ - Please ensure that your software is up-to-date.\n\ - Memo: {other_memo_version:?}" - ) - } - } - } - } - #[allow(clippy::too_many_arguments)] async fn decrypt_transaction_to_record_sapling( &self, @@ -397,6 +403,79 @@ pub mod decrypt_transaction { .await; } + /// account here is a verb meaning record note data + /// and perform some other appropriate actions + async fn account_for_shielded_receipts( + &self, + ivk: Ivk, + domain_tagged_outputs: &[( + D, + <::Bundle as wallet::traits::Bundle>::Output, + )], + status: ConfirmationStatus, + transaction: &Transaction, + block_time: Option, + arbitrary_memos_with_txids: &mut Vec<(ParsedMemo, TxId)>, + ) where + D: zingo_traits::DomainWalletExt, + ::Recipient: wallet::traits::Recipient, + ::Note: PartialEq, + ::Note: Clone, + D::Memo: zingo_traits::ToBytes<512>, + { + let decrypt_attempts = zcash_note_encryption::batch::try_note_decryption( + &[ivk.ivk], + domain_tagged_outputs, + ) + .into_iter() + .enumerate(); + for (output_index, decrypt_attempt) in decrypt_attempts { + let ((note, to, memo_bytes), _ivk_num) = match decrypt_attempt { + Some(plaintext) => plaintext, + _ => continue, + }; + let memo_bytes = MemoBytes::from_bytes(&memo_bytes.to_bytes()).unwrap(); + // if status is pending add the whole pending note + // otherwise, just update the output index + + let tx_map = &mut self + .transaction_metadata_set + .write() + .await + .transaction_records_by_id; + + let transaction_record = tx_map.create_modify_get_transaction_record( + &transaction.txid(), + status, + block_time, + ); + + if !status.is_confirmed() { + transaction_record.add_pending_note::(note.clone(), to, output_index); + } else { + let _note_does_not_exist_result = + transaction_record.update_output_index::(note.clone(), output_index); + } + + let memo = memo_bytes + .clone() + .try_into() + .unwrap_or(Memo::Future(memo_bytes)); + if let Memo::Arbitrary(ref wallet_internal_data) = memo { + match parse_zingo_memo(*wallet_internal_data.as_ref()) { + Ok(parsed_zingo_memo) => { + arbitrary_memos_with_txids + .push((parsed_zingo_memo, transaction.txid())); + } + Err(e) => { + let _memo_error: ZingoLibResult<()> = + ZingoLibError::CouldNotDecodeMemo(e).handle(); + } + } + } + tx_map.add_memo_to_note_metadata::(&transaction.txid(), note, memo); + } + } /// Transactions contain per-protocol "bundles" of components. /// The component details vary by protocol. /// In Sapling the components are "Spends" and "Outputs" @@ -412,9 +491,36 @@ pub mod decrypt_transaction { arbitrary_memos_with_txids: &mut Vec<(ParsedMemo, TxId)>, ) { type FnGenBundle = ::Bundle; + let domain_tagged_outputs = + as zingo_traits::Bundle>::from_transaction(transaction) + .into_iter() + .flat_map(|bundle| bundle.output_elements().into_iter()) + .map(|output| { + ( + output.domain(status.get_height(), self.config.chain), + output.clone(), + ) + }) + .collect::>(); + + let Ok(fvk) = D::unified_key_store_to_fvk(self.key.unified_key_store()) else { + // skip scanning if wallet has no viewing capability + return; + }; + let (ivk, ovk) = (fvk.derive_ivk::(), fvk.derive_ovk::()); + + self.account_for_shielded_receipts( + ivk, + &domain_tagged_outputs, + status, + transaction, + block_time, + arbitrary_memos_with_txids, + ) + .await; // Check if any of the nullifiers generated in this transaction are ours. We only need this for pending transactions, // because for transactions in the block, we will check the nullifiers from the blockdata - if status.is_pending() { + if !status.is_confirmed() { let unspent_nullifiers = self .transaction_metadata_set .read() @@ -450,86 +556,6 @@ pub mod decrypt_transaction { // 1. There's more than one way to be "spent". // 2. It's possible for a "nullifier" to be in the wallet's spent list, but never in the global ledger. // - let domain_tagged_outputs = - as zingo_traits::Bundle>::from_transaction(transaction) - .into_iter() - .flat_map(|bundle| bundle.output_elements().into_iter()) - .map(|output| { - ( - output.domain(status.get_height(), self.config.chain), - output.clone(), - ) - }) - .collect::>(); - - let Ok(fvk) = D::wc_to_fvk(&self.key) else { - // skip scanning if wallet has not viewing capability - return; - }; - let (ivk, ovk) = (fvk.derive_ivk::(), fvk.derive_ovk::()); - - let decrypt_attempts = zcash_note_encryption::batch::try_note_decryption( - &[ivk.ivk], - &domain_tagged_outputs, - ) - .into_iter() - .enumerate(); - for (output_index, decrypt_attempt) in decrypt_attempts { - let ((note, to, memo_bytes), _ivk_num) = match decrypt_attempt { - Some(plaintext) => plaintext, - _ => continue, - }; - let memo_bytes = MemoBytes::from_bytes(&memo_bytes.to_bytes()).unwrap(); - // if status is pending add the whole pending note - // otherwise, just update the output index - if let Some(height) = status.get_pending_height() { - self.transaction_metadata_set - .write() - .await - .transaction_records_by_id - .add_pending_note::( - transaction.txid(), - height, - block_time, - note.clone(), - to, - output_index, - ); - } else { - self.transaction_metadata_set - .write() - .await - .transaction_records_by_id - .update_output_index::( - transaction.txid(), - status, - block_time, - note.clone(), - output_index, - ) - } - let memo = memo_bytes - .clone() - .try_into() - .unwrap_or(Memo::Future(memo_bytes)); - if let Memo::Arbitrary(ref wallet_internal_data) = memo { - match parse_zingo_memo(*wallet_internal_data.as_ref()) { - Ok(parsed_zingo_memo) => { - arbitrary_memos_with_txids - .push((parsed_zingo_memo, transaction.txid())); - } - Err(e) => { - let _memo_error: ZingoLibResult<()> = - ZingoLibError::CouldNotDecodeMemo(e).handle(); - } - } - } - self.transaction_metadata_set - .write() - .await - .transaction_records_by_id - .add_memo_to_note_metadata::(&transaction.txid(), note, memo); - } for (_domain, output) in domain_tagged_outputs { outgoing_metadatas.extend( match try_output_recovery_with_ovk::< @@ -596,5 +622,164 @@ pub mod decrypt_transaction { ); } } + + async fn post_process_scan_results( + &self, + transaction: &Transaction, + outgoing_metadatas: &mut Vec, + sent_to_tex: bool, + ) { + // TODO: Account for ephemeral_taddresses + // Collect our t-addresses for easy checking + let taddrs_set = self.key.get_taddrs(&self.config.chain); + let tx_map = self.transaction_metadata_set.write().await; + if let Some(transaction_record) = + tx_map.transaction_records_by_id.get(&transaction.txid()) + { + // `transaction_kind` uses outgoing_tx_data to determine the SendType but not to distinguish Sent(_) from Received + // therefore, its safe to use it here to establish whether the transaction was created by this capacility or not. + if let TransactionKind::Sent(_) = tx_map + .transaction_records_by_id + .transaction_kind(transaction_record, &self.config.chain) + { + if let Some(t_bundle) = transaction.transparent_bundle() { + for vout in &t_bundle.vout { + if let Some(taddr) = vout.recipient_address().map(|raw_taddr| { + match sent_to_tex { + false => address_from_pubkeyhash(&self.config, raw_taddr), + true=> match raw_taddr{ + TransparentAddress::PublicKeyHash(taddr_bytes) => interpret_taddr_as_tex_addr(taddr_bytes, &self.config.chain), + TransparentAddress::ScriptHash(_taddr_bytes) => { + // tex addresses are P2PKH only. If this is a script hash, then we were wrong about + // it being a tex. + todo!("This happens if someone mislabels in a zingomemo, or zingolib logic an address as \"ephemeral\".");}, + }, + } + }) { + if !taddrs_set.contains(&taddr) { + outgoing_metadatas.push(OutgoingTxData { + recipient_address: taddr, + value: u64::from(vout.value), + memo: Memo::Empty, + recipient_ua: None, + }); + } + } + } + } + } + } + } + } + mod zingo_memos { + + use zcash_keys::address::UnifiedAddress; + use zcash_primitives::transaction::TxId; + use zingo_memo::ParsedMemo; + + use crate::wallet::{ + error::KeyError, keys::address_from_pubkeyhash, traits::Recipient as _, + transaction_context::TransactionContext, transaction_record::TransactionRecord, + }; + + #[derive(Debug)] + pub(crate) enum InvalidMemoError { + #[allow(dead_code)] + InvalidEphemeralIndex(KeyError), + } + impl TransactionContext { + async fn handle_uas( + &self, + uas: Vec, + transaction: &mut TransactionRecord, + ) { + for ua in uas { + let outgoing_potential_receivers = [ + ua.orchard() + .map(|oaddr| oaddr.b32encode_for_network(&self.config.chain)), + ua.sapling() + .map(|zaddr| zaddr.b32encode_for_network(&self.config.chain)), + ua.transparent() + .map(|taddr| address_from_pubkeyhash(&self.config, *taddr)), + Some(ua.encode(&self.config.chain)), + ]; + transaction + .outgoing_tx_data + .iter_mut() + .filter(|out_meta| { + outgoing_potential_receivers + .contains(&Some(out_meta.recipient_address.clone())) + }) + .for_each(|out_metadata| { + out_metadata.recipient_ua = Some(ua.encode(&self.config.chain)) + }) + } + } + async fn handle_texes( + &self, + zingo_memo_stored_indices: Vec, + ) -> Result<(), InvalidMemoError> { + // Get list of ephemeral keys already registered to the capability. + // TODO: This doesn't currently handle out-of-order sync where + // the ephemeral address is discovered (from the memo) **after** the + // corresponding TEX address has been "passed". + let current_keys = self.key.transparent_child_ephemeral_addresses(); + let total_keys = current_keys.len(); + for ephemeral_address_index in zingo_memo_stored_indices { + if (ephemeral_address_index as usize) < total_keys { + // The emphemeral key is in the structure at its appropriate location. + return Ok(()); + } else { + // The detected key is derived from a higher index than any previously stored key. + // * generate the keys to fill in the "gap". + for _index in (total_keys as u32)..=ephemeral_address_index { + crate::wallet::data::new_persistent_ephemeral_address( + current_keys, + &self + .key + .ephemeral_ivk() + .map_err(InvalidMemoError::InvalidEphemeralIndex)?, + ) + .map_err(InvalidMemoError::InvalidEphemeralIndex)?; + } + } + } + Ok(()) + } + + pub(super) async fn update_from_zingomemos( + &self, + txid_indexed_zingo_memos: Vec<(ParsedMemo, TxId)>, + ) -> Result<(), InvalidMemoError> { + for (parsed_zingo_memo, txid) in txid_indexed_zingo_memos { + if let Some(transaction) = self + .transaction_metadata_set + .write() + .await + .transaction_records_by_id + .get_mut(&txid) + { + match parsed_zingo_memo { + ParsedMemo::Version0 { uas } => self.handle_uas(uas, transaction).await, + ParsedMemo::Version1 { + uas, + ephemeral_address_indexes, + } => { + self.handle_uas(uas, transaction).await; + self.handle_texes(ephemeral_address_indexes).await?; + } + other_memo_version => { + log::error!( + "Wallet internal memo is from a future version of the protocol\n\ + Please ensure that your software is up-to-date.\n\ + Memo: {other_memo_version:?}" + ) + } + } + } + } + Ok(()) + } + } } } diff --git a/zingolib/src/wallet/transaction_record.rs b/zingolib/src/wallet/transaction_record.rs index 185733db8a..45b7f48219 100644 --- a/zingolib/src/wallet/transaction_record.rs +++ b/zingolib/src/wallet/transaction_record.rs @@ -15,6 +15,7 @@ use zcash_client_backend::{ }; use zcash_primitives::{consensus::BlockHeight, transaction::TxId}; +use crate::wallet::traits::Recipient as _; use crate::{ error::ZingoLibError, wallet::{ @@ -72,6 +73,8 @@ pub struct TransactionRecord { pub price: Option, } +// much data assignment of this struct is done through the pub fields as of january 2024. Todo: should have private fields and public methods. + // set impl TransactionRecord { /// TODO: Add Doc Comment Here! @@ -110,7 +113,56 @@ impl TransactionRecord { } } } - // much data assignment of this struct is done through the pub fields as of january 2024. Todo: should have private fields and public methods. + + /// adds a note. however, does not fully commit to adding a note, because this note isnt chained into block + pub(crate) fn add_pending_note( + &mut self, + note: D::Note, + to: D::Recipient, + output_index: usize, + ) { + match D::WalletNote::get_record_outputs(self) + .iter_mut() + .find(|n| n.note() == ¬e) + { + None => { + let nd = D::WalletNote::from_parts( + to.diversifier(), + note, + None, + None, + None, + None, + // if this is change, we'll mark it later in check_notes_mark_change + false, + false, + Some(output_index as u32), + ); + + D::WalletNote::transaction_metadata_notes_mut(self).push(nd); + } + Some(_) => {} + } + } + + /// returns Err(()) if note does not exist + pub(crate) fn update_output_index( + &mut self, + note: D::Note, + output_index: usize, + ) -> Result<(), ()> { + if let Some(n) = D::WalletNote::transaction_metadata_notes_mut(self) + .iter_mut() + .find(|n| n.note() == ¬e) + { + if n.output_index().is_none() { + *n.output_index_mut() = Some(output_index as u32) + } + Ok(()) + } else { + Err(()) + } + } } //get impl TransactionRecord { @@ -465,8 +517,8 @@ impl TransactionRecord { writer.write_all(self.txid.as_ref())?; - zcash_encoding::Vector::write(&mut writer, &self.sapling_notes, |w, nd| nd.write(w))?; - zcash_encoding::Vector::write(&mut writer, &self.orchard_notes, |w, nd| nd.write(w))?; + zcash_encoding::Vector::write(&mut writer, &self.sapling_notes, |w, nd| nd.write(w, ()))?; + zcash_encoding::Vector::write(&mut writer, &self.orchard_notes, |w, nd| nd.write(w, ()))?; zcash_encoding::Vector::write(&mut writer, &self.transparent_outputs, |w, u| u.write(w))?; for pool in self.value_spent_by_pool() { @@ -524,11 +576,14 @@ pub enum SendType { SendToSelf, } -#[cfg(test)] +#[cfg(any(test, feature = "test-elevation"))] pub mod mocks { //! Mock version of the struct for testing use zcash_primitives::transaction::TxId; + use zingo_status::confirmation_status::ConfirmationStatus; + use zingo_status::confirmation_status::ConfirmationStatus::Confirmed; + use zingo_status::confirmation_status::ConfirmationStatus::Mempool; use crate::{ mocks::{ @@ -661,8 +716,8 @@ pub mod mocks { orchard_spent: u64, orchard_semi_spent: u64, ) -> TransactionRecord { - let spend = Some((random_txid(), 112358)); - let semi_spend = Some((random_txid(), 853211)); + let spend = Some((random_txid(), Confirmed(112358.into()))); + let pending_spend = Some((random_txid(), Mempool(853211.into()))); TransactionRecordBuilder::default() .transparent_outputs( @@ -672,39 +727,39 @@ pub mod mocks { ) .transparent_outputs( TransparentOutputBuilder::default() - .spent(spend) + .spending_tx_status(spend) .value(transparent_spent) .clone(), ) .transparent_outputs( TransparentOutputBuilder::default() - .pending_spent(semi_spend) + .spending_tx_status(pending_spend) .value(transparent_semi_spent) .clone(), ) .sapling_notes(SaplingNoteBuilder::default().value(sapling_unspent).clone()) .sapling_notes( SaplingNoteBuilder::default() - .spent(spend) + .spending_tx_status(spend) .value(sapling_spent) .clone(), ) .sapling_notes( SaplingNoteBuilder::default() - .pending_spent(semi_spend) + .spending_tx_status(pending_spend) .value(sapling_semi_spent) .clone(), ) .orchard_notes(OrchardNoteBuilder::default().value(orchard_unspent).clone()) .orchard_notes( OrchardNoteBuilder::default() - .spent(spend) + .spending_tx_status(spend) .value(orchard_spent) .clone(), ) .orchard_notes( OrchardNoteBuilder::default() - .pending_spent(semi_spend) + .spending_tx_status(pending_spend) .value(orchard_semi_spent) .clone(), ) diff --git a/zingolib/src/wallet/transaction_records_by_id.rs b/zingolib/src/wallet/transaction_records_by_id.rs index c7a3fede01..24e6fd4961 100644 --- a/zingolib/src/wallet/transaction_records_by_id.rs +++ b/zingolib/src/wallet/transaction_records_by_id.rs @@ -20,11 +20,13 @@ use sapling_crypto::note_encryption::SaplingDomain; use zcash_client_backend::wallet::NoteId; use zcash_note_encryption::Domain; use zcash_primitives::consensus::BlockHeight; +use zingo_status::confirmation_status::ConfirmationStatus; -use zcash_primitives::transaction::TxId; -use zingoconfig::{ +use crate::config::{ ChainType, ZENNIES_FOR_ZINGO_DONATION_ADDRESS, ZENNIES_FOR_ZINGO_REGTEST_ADDRESS, + ZENNIES_FOR_ZINGO_TESTNET_ADDRESS, }; +use zcash_primitives::transaction::TxId; pub mod trait_inputsource; @@ -112,7 +114,7 @@ impl TransactionRecordsById { pub fn insert_transaction_record(&mut self, transaction_record: TransactionRecord) { self.insert(transaction_record.txid, transaction_record); } - /// Invalidates all transactions from a given height including the block with block height `reorg_height` + /// Invalidates all transactions from a given height including the block with block height `reorg_height`. /// /// All information above a certain height is invalidated during a reorg. pub fn invalidate_all_transactions_after_or_at_height(&mut self, reorg_height: BlockHeight) { @@ -120,12 +122,8 @@ impl TransactionRecordsById { let txids_to_remove = self .values() .filter_map(|transaction_metadata| { - if transaction_metadata - .status - .is_confirmed_after_or_at(&reorg_height) - || transaction_metadata - .status - .is_pending_after_or_at(&reorg_height) + // doesnt matter the status: if it happen after a reorg, eliminate it + if transaction_metadata.status.get_height() >= reorg_height // TODO: why dont we only remove confirmed transactions. pending transactions may still be valid in the mempool and may later confirm or expire. { Some(transaction_metadata.txid) @@ -163,14 +161,13 @@ impl TransactionRecordsById { .transparent_outputs .iter_mut() .for_each(|utxo| { - if utxo.is_spent() && invalidated_txids.contains(&utxo.spent().unwrap().0) { - *utxo.spent_mut() = None; - } - - if utxo.pending_spent.is_some() - && invalidated_txids.contains(&utxo.pending_spent.unwrap().0) + // Mark utxo as unspent if the txid being removed spent it. + if utxo + .spending_tx_status() + .filter(|(txid, _status)| invalidated_txids.contains(txid)) + .is_some() { - utxo.pending_spent = None; + *utxo.spending_tx_status_mut() = None; } }) }); @@ -191,22 +188,111 @@ impl TransactionRecordsById { OutputSpendStatusQuery::spentish(), ) .iter_mut() - .for_each(|nd| { + .for_each(|note| { // Mark note as unspent if the txid being removed spent it. - if nd.spent().is_some() && invalidated_txids.contains(&nd.spent().unwrap().0) { - *nd.spent_mut() = None; - } - - // Remove pending spends too - if nd.pending_spent().is_some() - && invalidated_txids.contains(&nd.pending_spent().unwrap().0) + if note + .spending_tx_status() + .filter(|(txid, _status)| invalidated_txids.contains(txid)) + .is_some() { - *nd.pending_spent_mut() = None; + *note.spending_tx_status_mut() = None; } }); }); } + /// Finds orchard note with given nullifier and updates its spend status + /// Currently only used for updating through pending statuses + /// For marking spent see [`crate::wallet::tx_map::TxMap::mark_note_as_spent`]i + // TODO: verify there is logic to mark pending notes back to unspent during invalidation + fn update_orchard_note_spend_status( + &mut self, + nullifier: &orchard::note::Nullifier, + spend_status: Option<(TxId, ConfirmationStatus)>, + ) { + let source_txid = self + .values() + .find(|tx| { + tx.orchard_notes() + .iter() + .flat_map(|note| note.nullifier) + .any(|nf| nf == *nullifier) + }) + .map(|tx| tx.txid); + + if let Some(txid) = source_txid { + let source_tx = self.get_mut(&txid).expect("transaction should exist"); + *source_tx + .orchard_notes + .iter_mut() + .find(|note| { + if let Some(nf) = note.nullifier() { + nf == *nullifier + } else { + false + } + }) + .expect("spend must exist") + .spending_tx_status_mut() = spend_status; + } + } + /// Finds sapling note with given nullifier and updates its spend status + /// Currently only used for updating through pending statuses + /// For marking spent see [`crate::wallet::tx_map::TxMap::mark_note_as_spent`]i + // TODO: verify there is logic to mark pending notes back to unspent during invalidation + fn update_sapling_note_spend_status( + &mut self, + nullifier: &sapling_crypto::Nullifier, + spend_status: Option<(TxId, ConfirmationStatus)>, + ) { + let source_txid = self + .values() + .find(|tx| { + tx.sapling_notes() + .iter() + .flat_map(|note| note.nullifier) + .any(|nf| nf == *nullifier) + }) + .map(|tx| tx.txid); + + if let Some(txid) = source_txid { + let source_tx = self.get_mut(&txid).expect("transaction should exist"); + *source_tx + .sapling_notes + .iter_mut() + .find(|note| { + if let Some(nf) = note.nullifier() { + nf == *nullifier + } else { + false + } + }) + .expect("spend must exist") + .spending_tx_status_mut() = spend_status; + } + } + + /// Updates notes spent in spending transaction to the given spend status + /// + /// Panics if spending transaction doesn't exist in wallet data, intended to be called after `scan_full_tx` + pub(crate) fn update_note_spend_statuses( + &mut self, + spending_txid: TxId, + spend_status: Option<(TxId, ConfirmationStatus)>, + ) { + if let Some(spending_tx) = self.get(&spending_txid) { + let orchard_nullifiers = spending_tx.spent_orchard_nullifiers.clone(); + let sapling_nullifiers = spending_tx.spent_sapling_nullifiers.clone(); + + orchard_nullifiers + .iter() + .for_each(|nf| self.update_orchard_note_spend_status(nf, spend_status)); + sapling_nullifiers + .iter() + .for_each(|nf| self.update_sapling_note_spend_status(nf, spend_status)); + } + } + fn find_sapling_spend(&self, nullifier: &sapling_crypto::Nullifier) -> Option<&SaplingNote> { self.values() .flat_map(|wallet_transaction_record| wallet_transaction_record.sapling_notes()) @@ -317,8 +403,7 @@ impl TransactionRecordsById { self.get_all_transparent_outputs() .into_iter() .filter(|o| { - (*o.spent()).map_or(false, |(txid, _)| txid == query_record.txid) - || (*o.pending_spent()).map_or(false, |(txid, _)| txid == query_record.txid) + (*o.spending_tx_status()).map_or(false, |(txid, _)| txid == query_record.txid) }) .collect() } @@ -359,7 +444,7 @@ impl TransactionRecordsById { /// Invalidates all those transactions which were broadcast but never 'confirmed' accepted by a miner. pub(crate) fn clear_expired_mempool(&mut self, latest_height: u64) { let cutoff = BlockHeight::from_u32( - (latest_height.saturating_sub(zingoconfig::MAX_REORG as u64)) as u32, + (latest_height.saturating_sub(crate::config::MAX_REORG as u64)) as u32, ); let txids_to_remove = self @@ -388,7 +473,7 @@ impl TransactionRecordsById { ) -> TransactionKind { let zfz_address = match chain { ChainType::Mainnet => ZENNIES_FOR_ZINGO_DONATION_ADDRESS, - ChainType::Testnet => unimplemented!(), + ChainType::Testnet => ZENNIES_FOR_ZINGO_TESTNET_ADDRESS, ChainType::Regtest(_) => ZENNIES_FOR_ZINGO_REGTEST_ADDRESS, }; @@ -471,10 +556,10 @@ impl TransactionRecordsById { } }); } - pub(crate) fn create_modify_get_transaction_metadata( + pub(crate) fn create_modify_get_transaction_record( &mut self, txid: &TxId, - status: zingo_status::confirmation_status::ConfirmationStatus, + status: ConfirmationStatus, datetime: Option, ) -> &'_ mut TransactionRecord { // check if there is already a confirmed transaction with the same txid @@ -493,7 +578,7 @@ impl TransactionRecordsById { }); // prevent confirmed transaction from being overwritten by pending transaction - if existing_tx_confirmed && status.is_pending() { + if existing_tx_confirmed && !status.is_confirmed() { self.get_mut(txid) .expect("previous check proves this tx exists") } else { @@ -512,12 +597,12 @@ impl TransactionRecordsById { pub fn add_taddr_spent( &mut self, txid: TxId, - status: zingo_status::confirmation_status::ConfirmationStatus, + status: ConfirmationStatus, timestamp: Option, total_transparent_value_spent: u64, ) { let transaction_metadata = - self.create_modify_get_transaction_metadata(&txid, status, timestamp); + self.create_modify_get_transaction_record(&txid, status, timestamp); transaction_metadata.total_transparent_value_spent = total_transparent_value_spent; } @@ -528,7 +613,7 @@ impl TransactionRecordsById { spent_txid: TxId, output_num: u32, source_txid: TxId, - spending_tx_status: zingo_status::confirmation_status::ConfirmationStatus, + spending_tx_status: ConfirmationStatus, ) -> u64 { // Find the UTXO let value = if let Some(utxo_transacion_metadata) = self.get_mut(&spent_txid) { @@ -537,15 +622,8 @@ impl TransactionRecordsById { .iter_mut() .find(|u| u.txid == spent_txid && u.output_index == output_num as u64) { - if spending_tx_status.is_confirmed() { - // Mark this utxo as spent - *spent_utxo.spent_mut() = - Some((source_txid, spending_tx_status.get_height().into())); - spent_utxo.pending_spent = None; - } else { - spent_utxo.pending_spent = - Some((source_txid, u32::from(spending_tx_status.get_height()))); - } + // Mark this utxo as spent + *spent_utxo.spending_tx_status_mut() = Some((source_txid, spending_tx_status)); spent_utxo.value } else { @@ -567,14 +645,14 @@ impl TransactionRecordsById { &mut self, txid: TxId, taddr: String, - status: zingo_status::confirmation_status::ConfirmationStatus, + status: ConfirmationStatus, timestamp: Option, vout: &zcash_primitives::transaction::components::TxOut, output_num: u32, ) { // Read or create the current TxId let transaction_metadata = - self.create_modify_get_transaction_metadata(&txid, status, timestamp); + self.create_modify_get_transaction_record(&txid, status, timestamp); // Add this UTXO if it doesn't already exist if transaction_metadata @@ -592,73 +670,16 @@ impl TransactionRecordsById { vout.script_pubkey.0.clone(), u64::from(vout.value), None, - None, ), ); } } - pub(crate) fn update_output_index( - &mut self, - txid: TxId, - status: zingo_status::confirmation_status::ConfirmationStatus, - timestamp: Option, - note: D::Note, - output_index: usize, - ) { - let transaction_record = - self.create_modify_get_transaction_metadata(&txid, status, timestamp); - - if let Some(n) = D::WalletNote::transaction_metadata_notes_mut(transaction_record) - .iter_mut() - .find(|n| n.note() == ¬e) - { - if n.output_index().is_none() { - *n.output_index_mut() = Some(output_index as u32) - } - } - } - pub(crate) fn add_pending_note( - &mut self, - txid: TxId, - height: BlockHeight, - timestamp: Option, - note: D::Note, - to: D::Recipient, - output_index: usize, - ) { - let status = zingo_status::confirmation_status::ConfirmationStatus::Pending(height); - let transaction_record = - self.create_modify_get_transaction_metadata(&txid, status, timestamp); - match D::WalletNote::get_record_outputs(transaction_record) - .iter_mut() - .find(|n| n.note() == ¬e) - { - None => { - let nd = D::WalletNote::from_parts( - to.diversifier(), - note, - None, - None, - None, - None, - None, - // if this is change, we'll mark it later in check_notes_mark_change - false, - false, - Some(output_index as u32), - ); - - D::WalletNote::transaction_metadata_notes_mut(transaction_record).push(nd); - } - Some(_) => {} - } - } #[allow(clippy::too_many_arguments)] pub(crate) fn add_new_note( &mut self, txid: TxId, - status: zingo_status::confirmation_status::ConfirmationStatus, + status: ConfirmationStatus, timestamp: Option, note: ::Note, to: D::Recipient, @@ -670,7 +691,7 @@ impl TransactionRecordsById { position: incrementalmerkletree::Position, ) { let transaction_metadata = - self.create_modify_get_transaction_metadata(&txid, status, timestamp); + self.create_modify_get_transaction_record(&txid, status, timestamp); let nd = D::WalletNote::from_parts( D::Recipient::diversifier(&to), @@ -679,7 +700,6 @@ impl TransactionRecordsById { nullifier, None, None, - None, // if this is change, we'll mark it later in check_notes_mark_change false, have_spending_key, @@ -814,7 +834,7 @@ mod tests { use sapling_crypto::note_encryption::SaplingDomain; use zcash_client_backend::{wallet::ReceivedNote, ShieldedProtocol}; use zcash_primitives::{consensus::BlockHeight, transaction::TxId}; - use zingo_status::confirmation_status::ConfirmationStatus::Confirmed; + use zingo_status::confirmation_status::ConfirmationStatus::{self, Confirmed}; #[test] fn invalidate_all_transactions_after_or_at_height() { @@ -825,27 +845,24 @@ mod tests { .build(); let spending_txid = transaction_record_later.txid; + let spend_in_known_tx = Some((spending_txid, Confirmed(15.into()))); + let transaction_record_early = TransactionRecordBuilder::default() .randomize_txid() .status(Confirmed(5.into())) .transparent_outputs( TransparentOutputBuilder::default() - .spent(Some((spending_txid, 15))) + .spending_tx_status(spend_in_known_tx) .clone(), ) .sapling_notes( SaplingNoteBuilder::default() - .spent(Some((spending_txid, 15))) + .spending_tx_status(spend_in_known_tx) .clone(), ) .orchard_notes( OrchardNoteBuilder::default() - .spent(Some((spending_txid, 15))) - .clone(), - ) - .sapling_notes( - SaplingNoteBuilder::default() - .spent(Some((random_txid(), 15))) + .spending_tx_status(spend_in_known_tx) .clone(), ) .orchard_notes(OrchardNoteBuilder::default()) @@ -873,29 +890,23 @@ mod tests { transaction_record_cvnwis, query_for_spentish_notes, ); - assert_eq!(spentish_sapling_notes_in_tx_cvnwis.len(), 1); - // ^ so there is one spent note still in this transaction - assert_ne!( - spentish_sapling_notes_in_tx_cvnwis.first().unwrap().spent(), - &Some((spending_txid, 15u32)) - ); - // ^ but it was not spent in the deleted txid + assert_eq!(spentish_sapling_notes_in_tx_cvnwis.len(), 0); } // TODO: move this into an associated fn of TransparentOutputBuilder fn spent_transparent_output_builder( amount: u64, - sent: (TxId, u32), + sent: (TxId, ConfirmationStatus), ) -> TransparentOutputBuilder { TransparentOutputBuilder::default() .value(amount) - .spent(Some(sent)) + .spending_tx_status(Some(sent)) .to_owned() } fn spent_sapling_note_builder( amount: u64, - sent: (TxId, u32), + sent: (TxId, ConfirmationStatus), sapling_nullifier: &sapling_crypto::Nullifier, ) -> SaplingNoteBuilder { SaplingNoteBuilder::default() @@ -904,13 +915,13 @@ mod tests { .value(sapling_crypto::value::NoteValue::from_raw(amount)) .to_owned(), ) - .spent(Some(sent)) + .spending_tx_status(Some(sent)) .nullifier(Some(*sapling_nullifier)) .to_owned() } fn spent_orchard_note_builder( amount: u64, - sent: (TxId, u32), + sent: (TxId, ConfirmationStatus), orchard_nullifier: &orchard::note::Nullifier, ) -> OrchardNoteBuilder { OrchardNoteBuilder::default() @@ -919,7 +930,7 @@ mod tests { .value(orchard::value::NoteValue::from_raw(amount)) .to_owned(), ) - .spent(Some(sent)) + .spending_tx_status(Some(sent)) .nullifier(Some(*orchard_nullifier)) .to_owned() } @@ -947,28 +958,29 @@ mod tests { // t-note + s-note + o-note + outgoing_tx_data let expected_output_value: u64 = 100_000 + 200_000 + 800_000 + 50_000; // 1_150_000 + let spent_in_sent_txid = (sent_txid, Confirmed(15.into())); let first_received_transaction_record = TransactionRecordBuilder::default() .randomize_txid() .status(Confirmed(5.into())) .sapling_notes(spent_sapling_note_builder( 175_000, - (sent_txid, 15), + spent_in_sent_txid, &first_sapling_nullifier, )) .sapling_notes(spent_sapling_note_builder( 325_000, - (sent_txid, 15), + spent_in_sent_txid, &second_sapling_nullifier, )) .orchard_notes(spent_orchard_note_builder( 500_000, - (sent_txid, 15), + spent_in_sent_txid, &first_orchard_nullifier, )) - .transparent_outputs(spent_transparent_output_builder(30_000, (sent_txid, 15))) // 100_000 + .transparent_outputs(spent_transparent_output_builder(30_000, spent_in_sent_txid)) // 100_000 .sapling_notes( - SaplingNoteBuilder::default() // 200_000 - .spent(Some((random_txid(), 12))) + SaplingNoteBuilder::default() + .spending_tx_status(Some((random_txid(), Confirmed(12.into())))) .to_owned(), ) .orchard_notes(OrchardNoteBuilder::default()) // 800_000 @@ -979,14 +991,14 @@ mod tests { .status(Confirmed(7.into())) .orchard_notes(spent_orchard_note_builder( 200_000, - (sent_txid, 15), + spent_in_sent_txid, &second_orchard_nullifier, )) .transparent_outputs(TransparentOutputBuilder::default()) .sapling_notes(SaplingNoteBuilder::default().clone()) .orchard_notes( OrchardNoteBuilder::default() - .spent(Some((random_txid(), 13))) + .spending_tx_status(Some((random_txid(), Confirmed(13.into())))) .to_owned(), ) .set_output_indexes() @@ -1059,7 +1071,7 @@ mod tests { .value(sapling_crypto::value::NoteValue::from_raw(175_000)) .to_owned(), ) - .spent(Some((sent_txid, 15))) + .spending_tx_status(Some((sent_txid, Confirmed(15.into())))) .nullifier(Some(sapling_nullifier)) .to_owned(), ) @@ -1123,10 +1135,11 @@ mod tests { ) .build(); let sent_txid = transaction_record.txid; + let spent_in_sent_txid = (sent_txid, Confirmed(15.into())); let transparent_funding_tx = TransactionRecordBuilder::default() .randomize_txid() .status(Confirmed(7.into())) - .transparent_outputs(spent_transparent_output_builder(20_000, (sent_txid, 15))) + .transparent_outputs(spent_transparent_output_builder(20_000, spent_in_sent_txid)) .set_output_indexes() .build(); diff --git a/zingolib/src/wallet/transaction_records_by_id/trait_inputsource.rs b/zingolib/src/wallet/transaction_records_by_id/trait_inputsource.rs index a37a800a5b..7bd99680c1 100644 --- a/zingolib/src/wallet/transaction_records_by_id/trait_inputsource.rs +++ b/zingolib/src/wallet/transaction_records_by_id/trait_inputsource.rs @@ -16,12 +16,8 @@ use zcash_primitives::{ }, }; -use crate::wallet::{ - notes::{query::OutputSpendStatusQuery, OutputInterface}, - transaction_records_by_id::TransactionRecordsById, -}; +use crate::wallet::{notes::OutputInterface, transaction_records_by_id::TransactionRecordsById}; -// error type use std::fmt::Debug; use thiserror::Error; @@ -149,10 +145,10 @@ impl InputSource for TransactionRecordsById { } #[allow(rustdoc::private_intra_doc_links)] - /// the trait method below is used as a TxMapAndMaybeTrees trait method by propose_transaction. + /// the trait method below is used as a TxMap trait method by propose_transaction. /// this function is used inside a loop that calculates a fee and balances change /// this algorithm influences strategy for user fee minimization - /// see [crate::lightclient::LightClient::create_send_proposal] + /// see [crate::wallet::LightWallet::create_send_proposal] /// TRAIT DOCUMENTATION /// Returns a list of spendable notes sufficient to cover the specified target value, if /// possible. Only spendable notes corresponding to the specified shielded protocol will @@ -179,7 +175,8 @@ impl InputSource for TransactionRecordsById { .collect::, _>>() .map_err(InputSourceError::InvalidValue)?; unselected.sort_by_key(|(_id, value)| *value); // from smallest to largest - let dust_spendable_index = unselected.partition_point(|(_id, value)| *value < MARGINAL_FEE); + let dust_spendable_index = + unselected.partition_point(|(_id, value)| *value <= MARGINAL_FEE); let _dust_notes: Vec<_> = unselected.drain(..dust_spendable_index).collect(); let mut selected = vec![]; let mut index_of_unselected = 0; @@ -282,120 +279,63 @@ impl InputSource for TransactionRecordsById { Ok(SpendableNotes::new(selected_sapling, selected_orchard)) } - /// Fetches a spendable transparent output. - /// - /// Returns `Ok(None)` if the UTXO is not known to belong to the wallet or is not - /// spendable. - /// IMPL: Implemented and tested + /// not implemented fn get_unspent_transparent_output( &self, - outpoint: &zcash_primitives::transaction::components::OutPoint, + _outpoint: &zcash_primitives::transaction::components::OutPoint, ) -> Result, Self::Error> { - let Some((height, output)) = self.values().find_map(|transaction_record| { - transaction_record - .transparent_outputs - .iter() - .find_map(|output| { - if &output.to_outpoint() == outpoint { - transaction_record - .status - .get_confirmed_height() - .map(|height| (height, output)) - } else { - None - } - }) - .filter(|(_height, output)| { - output.spend_status_query(OutputSpendStatusQuery::only_unspent()) - }) - }) else { - return Ok(None); - }; - let value = - NonNegativeAmount::from_u64(output.value).map_err(InputSourceError::InvalidValue)?; - - let script_pubkey = Script(output.script.clone()); - - Ok(WalletTransparentOutput::from_parts( - outpoint.clone(), - TxOut { - value, - script_pubkey, - }, - height, - )) + unimplemented!() } - /// Returns a list of unspent transparent UTXOs that appear in the chain at heights up to and - /// including `max_height`. - /// IMPL: Implemented and tested. address is unused, we select all outputs available to the wallet. - /// IMPL: _address skipped because Zingo uses 1 account. - fn get_unspent_transparent_outputs( + + fn get_spendable_transparent_outputs( &self, - // I don't understand what this argument is for. Is the Trait's intent to only shield - // utxos from one address at a time? Is this needed? _address: &zcash_primitives::legacy::TransparentAddress, - max_height: zcash_primitives::consensus::BlockHeight, - exclude: &[zcash_primitives::transaction::components::OutPoint], - ) -> Result, Self::Error> { - self.values() - .filter_map(|transaction_record| { - transaction_record - .status + target_height: zcash_primitives::consensus::BlockHeight, + _min_confirmations: u32, + ) -> Result, Self::Error> { + // TODO: rewrite to include addresses and min confirmations + let transparent_outputs: Vec = self + .values() + .filter(|tx| { + tx.status .get_confirmed_height() - .map(|height| (transaction_record, height)) - .filter(|(_, height)| height <= &max_height) + .map_or(false, |height| height <= target_height) }) - .flat_map(|(transaction_record, confirmed_height)| { - transaction_record - .transparent_outputs - .iter() - .filter(|output| { - exclude - .iter() - .all(|excluded| excluded != &output.to_outpoint()) - }) - .filter(|output| { - output.spend_status_query(OutputSpendStatusQuery::only_unspent()) - }) - .filter_map(move |output| { - let value = match NonNegativeAmount::from_u64(output.value) - .map_err(InputSourceError::InvalidValue) - { - Ok(v) => v, - Err(e) => return Some(Err(e)), - }; - - let script_pubkey = Script(output.script.clone()); - Ok(WalletTransparentOutput::from_parts( + .flat_map(|tx| { + tx.transparent_outputs().iter().filter_map(|output| { + if output.spending_tx_status().is_none() { + WalletTransparentOutput::from_parts( output.to_outpoint(), TxOut { - value, - script_pubkey, + value: NonNegativeAmount::from_u64(output.value()) + .expect("value should be in valid range of zatoshis"), + script_pubkey: Script(output.script.clone()), }, - confirmed_height, - )) - .transpose() - }) + Some(tx.status.get_height()), + ) + } else { + None + } + }) }) - .collect() + .collect(); + + Ok(transparent_outputs) } } #[cfg(test)] mod tests { use proptest::{prop_assert_eq, proptest}; - use zcash_client_backend::{data_api::InputSource as _, ShieldedProtocol}; + use zcash_client_backend::ShieldedProtocol; use zcash_primitives::{ - consensus::BlockHeight, legacy::TransparentAddress, - transaction::components::amount::NonNegativeAmount, + consensus::BlockHeight, transaction::components::amount::NonNegativeAmount, }; use zip32::AccountId; use crate::wallet::{ - notes::{orchard::mocks::OrchardNoteBuilder, transparent::mocks::TransparentOutputBuilder}, - transaction_record::mocks::{ - nine_note_transaction_record_default, TransactionRecordBuilder, - }, + notes::orchard::mocks::OrchardNoteBuilder, + transaction_record::mocks::TransactionRecordBuilder, transaction_records_by_id::TransactionRecordsById, }; @@ -474,88 +414,4 @@ mod tests { prop_assert_eq!(spendable_notes.sapling().len() + spendable_notes.orchard().len(), expected_len); } } - - #[test] - fn get_unspent_transparent_output() { - let mut transaction_records_by_id = TransactionRecordsById::new(); - - let transaction_record = nine_note_transaction_record_default(); - - transaction_records_by_id.insert_transaction_record(transaction_record); - - let transparent_output = transaction_records_by_id - .0 - .values() - .next() - .unwrap() - .transparent_outputs - .first() - .unwrap(); - let record_height = transaction_records_by_id - .0 - .values() - .next() - .unwrap() - .status - .get_confirmed_height(); - - let wto = transaction_records_by_id - .get_unspent_transparent_output( - &TransparentOutputBuilder::default().build().to_outpoint(), - ) - .unwrap() - .unwrap(); - - assert_eq!(wto.outpoint(), &transparent_output.to_outpoint()); - assert_eq!(wto.txout().value.into_u64(), transparent_output.value); - assert_eq!(wto.txout().script_pubkey.0, transparent_output.script); - assert_eq!(Some(wto.height()), record_height) - } - - #[test] - fn get_unspent_transparent_outputs() { - let mut transaction_records_by_id = TransactionRecordsById::new(); - transaction_records_by_id.insert_transaction_record(nine_note_transaction_record_default()); - - let transparent_output = transaction_records_by_id - .0 - .values() - .next() - .unwrap() - .transparent_outputs - .first() - .unwrap(); - let record_height = transaction_records_by_id - .0 - .values() - .next() - .unwrap() - .status - .get_confirmed_height(); - - let selected_outputs = transaction_records_by_id - .get_unspent_transparent_outputs( - &TransparentAddress::ScriptHash([0; 20]), - BlockHeight::from_u32(10), - &[], - ) - .unwrap(); - assert_eq!(selected_outputs.len(), 1); - assert_eq!( - selected_outputs.first().unwrap().outpoint(), - &transparent_output.to_outpoint() - ); - assert_eq!( - selected_outputs.first().unwrap().txout().value.into_u64(), - transparent_output.value - ); - assert_eq!( - selected_outputs.first().unwrap().txout().script_pubkey.0, - transparent_output.script - ); - assert_eq!( - Some(selected_outputs.first().unwrap().height()), - record_height - ) - } } diff --git a/zingolib/src/wallet/tx_map.rs b/zingolib/src/wallet/tx_map.rs new file mode 100644 index 0000000000..c1709d186c --- /dev/null +++ b/zingolib/src/wallet/tx_map.rs @@ -0,0 +1,145 @@ +//! This mod should be called tx_map_and_maybe_trees.rs. it contains +//! struct TxMap +//! implementations for TxMap +//! associated types for TxMap that have no relevance elsewhere. + +use crate::{ + data::witness_trees::WitnessTrees, + wallet::{ + error::KeyError, + transaction_records_by_id::{trait_inputsource::InputSourceError, TransactionRecordsById}, + }, +}; +use getset::{Getters, MutGetters}; +use spending_data::SpendingData; +use std::{fmt::Debug, sync::Arc}; +use thiserror::Error; +use zcash_client_backend::wallet::TransparentAddressMetadata; +use zcash_primitives::legacy::{keys::EphemeralIvk, TransparentAddress}; + +/// HashMap of all transactions in a wallet, keyed by txid. +/// Note that the parent is expected to hold a RwLock, so we will assume that all accesses to +/// this struct are threadsafe/locked properly. +#[derive(Getters, MutGetters)] +pub struct TxMap { + /// TODO: Doc-comment! + pub transaction_records_by_id: TransactionRecordsById, + #[getset(get = "pub(crate)", get_mut = "pub(crate)")] + spending_data: Option, + // as below + pub(crate) transparent_child_addresses: + Arc>, + // TODO: rename (ephemeral_transparent_addresses?) + pub(crate) transparent_child_ephemeral_addresses: + Arc>, +} + +pub mod get; +pub mod read_write; +pub mod recording; + +pub mod spending_data; + +impl TxMap { + pub(crate) fn new_with_witness_trees( + transparent_child_addresses: Arc< + append_only_vec::AppendOnlyVec<(usize, TransparentAddress)>, + >, + transparent_child_ephemeral_addresses: Arc< + append_only_vec::AppendOnlyVec<(TransparentAddress, TransparentAddressMetadata)>, + >, + transparent_ephemeral_ivk: EphemeralIvk, + ) -> TxMap { + Self { + transaction_records_by_id: TransactionRecordsById::new(), + spending_data: Some(SpendingData::new( + WitnessTrees::default(), + transparent_ephemeral_ivk, + )), + transparent_child_addresses, + transparent_child_ephemeral_addresses, + } + } + pub(crate) fn new_treeless( + transparent_child_addresses: Arc< + append_only_vec::AppendOnlyVec<(usize, TransparentAddress)>, + >, + ) -> TxMap { + Self { + transaction_records_by_id: TransactionRecordsById::new(), + spending_data: None, + transparent_child_addresses, + transparent_child_ephemeral_addresses: Arc::new(append_only_vec::AppendOnlyVec::new()), + } + } + /// TODO: Doc-comment! + pub fn witness_trees(&self) -> Option<&WitnessTrees> { + self.spending_data + .as_ref() + .map(|spending_data| spending_data.witness_trees()) + } + pub(crate) fn witness_trees_mut(&mut self) -> Option<&mut WitnessTrees> { + self.spending_data + .as_mut() + .map(|spending_data| spending_data.witness_trees_mut()) + } + /// TODO: Doc-comment! + pub fn clear(&mut self) { + self.transaction_records_by_id.clear(); + self.witness_trees_mut().map(WitnessTrees::clear); + } +} +#[allow(missing_docs)] // error types document themselves +#[derive(Debug, Error)] +pub enum TxMapTraitError { + #[error("No witness trees. This is viewkey watch, not a spendkey wallet.")] + NoSpendCapability, + #[error("{0:?}")] + InputSource(InputSourceError), + #[error("{0:?}")] + TransactionWrite(std::io::Error), + #[error("{0}")] + TexSendError(KeyError), +} + +pub mod trait_stub_inputsource; +pub mod trait_stub_walletcommitmenttrees; +pub mod trait_walletread; +pub mod trait_walletwrite; + +#[cfg(test)] +impl TxMap { + /// For any unit tests that don't require a WalletCapability, where the addresses come from + pub(crate) fn new_with_witness_trees_address_free() -> TxMap { + // The first 32 bytes are a BIP32/44 chain code, the subsequent 33 bytes are a pubkey + // https://github.com/zancas/zcash-test-vectors/blob/db9c9b9519a32859a46bbbc60368e8741fe629c4/test-vectors/rust/zip_0316.rs#L10 + const EXTENDED_PUBKEY: [u8; 65] = [ + 0x5d, 0x7a, 0x8f, 0x73, 0x9a, 0x2d, 0x9e, 0x94, 0x5b, 0x0c, 0xe1, 0x52, 0xa8, 0x04, + 0x9e, 0x29, 0x4c, 0x4d, 0x6e, 0x66, 0xb1, 0x64, 0x93, 0x9d, 0xaf, 0xfa, 0x2e, 0xf6, + 0xee, 0x69, 0x21, 0x48, 0x02, 0x16, 0x88, 0x4f, 0x1d, 0xbc, 0x92, 0x90, 0x89, 0xa4, + 0x17, 0x6e, 0x84, 0x0b, 0xb5, 0x81, 0xc8, 0x0e, 0x16, 0xe9, 0xb1, 0xab, 0xd6, 0x54, + 0xe6, 0x2c, 0x8b, 0x0b, 0x95, 0x70, 0x20, 0xb7, 0x48, + ]; + Self { + transaction_records_by_id: TransactionRecordsById::new(), + spending_data: Some(SpendingData::new( + WitnessTrees::default(), + zcash_primitives::legacy::keys::AccountPubKey::deserialize(&EXTENDED_PUBKEY) + .unwrap() + .derive_ephemeral_ivk() + .unwrap(), + )), + transparent_child_addresses: Arc::new(append_only_vec::AppendOnlyVec::new()), + transparent_child_ephemeral_addresses: Arc::new(append_only_vec::AppendOnlyVec::new()), + } + } + /// For any unit tests that don't require a WalletCapability, where the addresses come from + pub(crate) fn new_treeless_address_free() -> TxMap { + Self { + transaction_records_by_id: TransactionRecordsById::new(), + spending_data: None, + transparent_child_addresses: Arc::new(append_only_vec::AppendOnlyVec::new()), + transparent_child_ephemeral_addresses: Arc::new(append_only_vec::AppendOnlyVec::new()), + } + } +} diff --git a/zingolib/src/wallet/tx_map_and_maybe_trees/get.rs b/zingolib/src/wallet/tx_map/get.rs similarity index 94% rename from zingolib/src/wallet/tx_map_and_maybe_trees/get.rs rename to zingolib/src/wallet/tx_map/get.rs index 633be28217..06f29d8cc4 100644 --- a/zingolib/src/wallet/tx_map_and_maybe_trees/get.rs +++ b/zingolib/src/wallet/tx_map/get.rs @@ -1,4 +1,4 @@ -//! contains associated methods for asking TxMapAndMaybeTrees about the data it contains +//! contains associated methods for asking TxMap about the data it contains //! Does not contain trait implementations use zcash_note_encryption::Domain; @@ -12,9 +12,9 @@ use crate::wallet::{ traits::{DomainWalletExt, Recipient}, }; -use super::TxMapAndMaybeTrees; +use super::TxMap; -impl TxMapAndMaybeTrees { +impl TxMap { /// TODO: Doc-comment! pub fn get_notes_for_updating( &self, @@ -32,7 +32,7 @@ impl TxMapAndMaybeTrees { .iter() .filter_map(move |sapling_note_description| { if sapling_note_description.have_spending_key - && sapling_note_description.spent.is_none() + && !sapling_note_description.is_spent_confirmed() { Some(( *txid, @@ -50,7 +50,7 @@ impl TxMapAndMaybeTrees { .chain(transaction_metadata.orchard_notes.iter().filter_map( move |orchard_note_description| { if orchard_note_description.have_spending_key - && orchard_note_description.spent.is_none() + && !orchard_note_description.is_spent_confirmed() { Some(( *txid, @@ -139,7 +139,7 @@ impl TxMapAndMaybeTrees { #[test] fn test_get_some_txid_from_highest_wallet_block() { - let mut tms = TxMapAndMaybeTrees::new_treeless_address_free(); + let mut tms = TxMap::new_treeless_address_free(); assert_eq!(tms.get_some_txid_from_highest_wallet_block(), None); let txid_bytes_1 = [0u8; 32]; let txid_bytes_2 = [1u8; 32]; @@ -149,7 +149,7 @@ fn test_get_some_txid_from_highest_wallet_block() { let txid_3 = TxId::from_bytes(txid_bytes_3); tms.transaction_records_by_id .insert_transaction_record(TransactionRecord::new( - zingo_status::confirmation_status::ConfirmationStatus::Pending(BlockHeight::from_u32( + zingo_status::confirmation_status::ConfirmationStatus::Mempool(BlockHeight::from_u32( 3_200_000, )), 100, @@ -176,7 +176,7 @@ fn test_get_some_txid_from_highest_wallet_block() { } #[cfg(feature = "lightclient-deprecated")] -impl TxMapAndMaybeTrees { +impl TxMap { /// TODO: Doc-comment! pub fn get_fee_by_txid(&self, txid: &TxId) -> u64 { let transaction_record = self diff --git a/zingolib/src/wallet/tx_map_and_maybe_trees/read_write.rs b/zingolib/src/wallet/tx_map/read_write.rs similarity index 88% rename from zingolib/src/wallet/tx_map_and_maybe_trees/read_write.rs rename to zingolib/src/wallet/tx_map/read_write.rs index 62c198d141..111378061e 100644 --- a/zingolib/src/wallet/tx_map_and_maybe_trees/read_write.rs +++ b/zingolib/src/wallet/tx_map/read_write.rs @@ -1,4 +1,4 @@ -//! contains associated methods for writing TxMapAndMaybeTrees to disk and reading TxMapAndMaybeTrees from disk +//! contains associated methods for writing TxMap to disk and reading TxMap from disk use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; use std::{ @@ -13,8 +13,8 @@ use crate::{ wallet::{data::TransactionRecord, keys::unified::WalletCapability}, }; -use super::{TransactionRecordsById, TxMapAndMaybeTrees}; -impl TxMapAndMaybeTrees { +use super::{spending_data::SpendingData, TransactionRecordsById, TxMap}; +impl TxMap { /// TODO: Doc-comment! pub fn serialized_version() -> u64 { 22 @@ -68,8 +68,13 @@ impl TxMapAndMaybeTrees { Ok(Self { transaction_records_by_id: map, - witness_trees, + spending_data: witness_trees + .zip(wallet_capability.ephemeral_ivk().ok()) + .map(|(trees, key)| SpendingData::new(trees, key)), transparent_child_addresses: wallet_capability.transparent_child_addresses().clone(), + transparent_child_ephemeral_addresses: wallet_capability + .transparent_child_ephemeral_addresses() + .clone(), }) } @@ -135,8 +140,13 @@ impl TxMapAndMaybeTrees { Ok(Self { transaction_records_by_id: TransactionRecordsById::from_map(map), - witness_trees, + spending_data: witness_trees + .zip(wallet_capability.ephemeral_ivk().ok()) + .map(|(trees, key)| SpendingData::new(trees, key)), transparent_child_addresses: wallet_capability.transparent_child_addresses().clone(), + transparent_child_ephemeral_addresses: wallet_capability + .transparent_child_ephemeral_addresses() + .clone(), }) } @@ -163,7 +173,7 @@ impl TxMapAndMaybeTrees { })?; } - Optional::write(writer, self.witness_trees.as_mut(), |w, t| t.write(w)) + Optional::write(writer, self.witness_trees_mut(), |w, t| t.write(w)) } } #[cfg(test)] @@ -173,7 +183,7 @@ mod tests { #[tokio::test] async fn test_write() { - let mut tms = TxMapAndMaybeTrees::new_with_witness_trees_address_free(); + let mut tms = TxMap::new_with_witness_trees_address_free(); let mut buffer = Cursor::new(Vec::new()); // Perform the write operation diff --git a/zingolib/src/wallet/tx_map_and_maybe_trees/recording.rs b/zingolib/src/wallet/tx_map/recording.rs similarity index 79% rename from zingolib/src/wallet/tx_map_and_maybe_trees/recording.rs rename to zingolib/src/wallet/tx_map/recording.rs index f582471213..38cedd1d4b 100644 --- a/zingolib/src/wallet/tx_map_and_maybe_trees/recording.rs +++ b/zingolib/src/wallet/tx_map/recording.rs @@ -1,4 +1,4 @@ -//! contains associated methods for modifying and updating TxMapAndMaybeTrees +//! contains associated methods for modifying and updating TxMap use incrementalmerkletree::Position; use orchard::note_encryption::OrchardDomain; @@ -19,7 +19,7 @@ use crate::{ }; /// Witness tree requiring methods, each method is noted with *HOW* it requires witness trees. -impl super::TxMapAndMaybeTrees { +impl super::TxMap { /// During reorgs, we need to remove all txns at a given height, and all spends that refer to any removed txns. pub fn invalidate_all_transactions_after_or_at_height(&mut self, reorg_height: u64) { let reorg_height = BlockHeight::from_u32(reorg_height as u32); @@ -27,7 +27,7 @@ impl super::TxMapAndMaybeTrees { self.transaction_records_by_id .invalidate_all_transactions_after_or_at_height(reorg_height); - if let Some(ref mut t) = self.witness_trees { + if let Some(ref mut t) = self.witness_trees_mut() { t.witness_tree_sapling .truncate_removing_checkpoint(&(reorg_height - 1)) .expect("Infallible"); @@ -101,7 +101,7 @@ impl super::TxMapAndMaybeTrees { // Record this Tx as having spent some funds let transaction_metadata = self .transaction_records_by_id - .create_modify_get_transaction_metadata(&spending_txid, status, timestamp); + .create_modify_get_transaction_record(&spending_txid, status, timestamp); if !::Nullifier::get_nullifiers_spent_in_transaction( transaction_metadata, @@ -130,54 +130,31 @@ impl super::TxMapAndMaybeTrees { ::Note: PartialEq + Clone, ::Recipient: traits::Recipient, { - Ok(if let Some(height) = status.get_confirmed_height() { + if let Some(height) = status.get_confirmed_height() { // ie remove_witness_mark_sapling or _orchard self.remove_witness_mark::(height, spending_txid, source_txid, output_index)?; - if let Some(transaction_spent_from) = - self.transaction_records_by_id.get_mut(&source_txid) - { - if let Some(confirmed_spent_note) = - D::WalletNote::get_record_to_outputs_mut(transaction_spent_from) - .iter_mut() - .find(|note| note.nullifier() == Some(spent_nullifier)) - { - *confirmed_spent_note.spent_mut() = Some((spending_txid, height.into())); - *confirmed_spent_note.pending_spent_mut() = None; + } - confirmed_spent_note.value() - } else { - ZingoLibError::NoSuchNullifierInTx(spending_txid).handle()? - } - } else { - ZingoLibError::NoSuchTxId(spending_txid).handle()? - } - } else if let Some(height) = status.get_pending_height() { - // Mark the pending_spent. Confirmed spends are already handled in update_notes - if let Some(transaction_spent_from) = - self.transaction_records_by_id.get_mut(&source_txid) + if let Some(transaction_spent_from) = self.transaction_records_by_id.get_mut(&source_txid) { + if let Some(spent_note) = + D::WalletNote::get_record_to_outputs_mut(transaction_spent_from) + .iter_mut() + .find(|note| note.nullifier() == Some(spent_nullifier)) { - if let Some(pending_spent_note) = - D::WalletNote::get_record_to_outputs_mut(transaction_spent_from) - .iter_mut() - .find(|note| note.nullifier() == Some(spent_nullifier)) - { - *pending_spent_note.pending_spent_mut() = - Some((spending_txid, u32::from(height))); - pending_spent_note.value() - } else { - ZingoLibError::NoSuchNullifierInTx(spending_txid).handle()? - } + *spent_note.spending_tx_status_mut() = Some((spending_txid, status)); + + Ok(spent_note.value()) } else { - ZingoLibError::NoSuchTxId(spending_txid).handle()? + ZingoLibError::NoSuchNullifierInTx(spending_txid).handle()? } } else { - ZingoLibError::UnknownError.handle()? - }) // todO add special error variant + ZingoLibError::NoSuchTxId(spending_txid).handle()? + } } } // shardtree -impl crate::wallet::tx_map_and_maybe_trees::TxMapAndMaybeTrees { +impl crate::wallet::tx_map::TxMap { /// A mark designates a leaf as non-ephemeral, mark removal causes /// the leaf to eventually transition to the ephemeral state pub fn remove_witness_mark( @@ -212,7 +189,8 @@ impl crate::wallet::tx_map_and_maybe_trees::TxMapAndMaybeTrees { { match maybe_note { Ok(note_datum) => { - *note_datum.spent_mut() = Some((txid, height.into())); + // *note_datum.spending_tx_status_mut() = Some((txid, ConfirmationStatus::Confirmed(height))); + // TODO WTF why is this line here? if let Some(position) = *note_datum.witnessed_position() { if let Some(ref mut tree) = D::transaction_metadata_set_to_shardtree_mut(self) diff --git a/zingolib/src/wallet/tx_map/spending_data.rs b/zingolib/src/wallet/tx_map/spending_data.rs new file mode 100644 index 0000000000..12e6753f70 --- /dev/null +++ b/zingolib/src/wallet/tx_map/spending_data.rs @@ -0,0 +1,28 @@ +//! the subsection of TxMap that only applies to spending wallets + +use getset::{Getters, MutGetters}; + +use zcash_primitives::{legacy::keys::EphemeralIvk, transaction::TxId}; + +use crate::data::witness_trees::WitnessTrees; + +/// the subsection of TxMap that only applies to spending wallets +#[derive(Getters, MutGetters)] +pub(crate) struct SpendingData { + #[getset(get = "pub(crate)", get_mut = "pub(crate)")] + witness_trees: WitnessTrees, + #[getset(get = "pub(crate)", get_mut = "pub(crate)")] + cached_raw_transactions: Vec<(TxId, Vec)>, + #[getset(get = "pub(crate)", get_mut = "pub(crate)")] + transparent_ephemeral_ivk: EphemeralIvk, +} + +impl SpendingData { + pub fn new(witness_trees: WitnessTrees, transparent_ephemeral_ivk: EphemeralIvk) -> Self { + SpendingData { + witness_trees, + cached_raw_transactions: Vec::new(), + transparent_ephemeral_ivk, + } + } +} diff --git a/zingolib/src/wallet/tx_map_and_maybe_trees/trait_stub_inputsource.rs b/zingolib/src/wallet/tx_map/trait_stub_inputsource.rs similarity index 68% rename from zingolib/src/wallet/tx_map_and_maybe_trees/trait_stub_inputsource.rs rename to zingolib/src/wallet/tx_map/trait_stub_inputsource.rs index 57bebcc9cb..8dda51e1e3 100644 --- a/zingolib/src/wallet/tx_map_and_maybe_trees/trait_stub_inputsource.rs +++ b/zingolib/src/wallet/tx_map/trait_stub_inputsource.rs @@ -5,13 +5,13 @@ use zcash_client_backend::{ wallet::NoteId, }; -use super::{TxMapAndMaybeTrees, TxMapAndMaybeTreesTraitError}; +use super::{TxMap, TxMapTraitError}; /// A trait representing the capability to query a data store for unspent transaction outputs belonging to a wallet. /// combining this with WalletRead unlocks propose_transaction /// all implementations in this file redirect to transaction_records_by_id -impl InputSource for TxMapAndMaybeTrees { - type Error = TxMapAndMaybeTreesTraitError; +impl InputSource for TxMap { + type Error = TxMapTraitError; type AccountId = zcash_primitives::zip32::AccountId; type NoteRef = NoteId; @@ -42,26 +42,24 @@ impl InputSource for TxMapAndMaybeTrees { ) -> Result, Self::Error> { self.transaction_records_by_id .select_spendable_notes(account, target_value, sources, anchor_height, exclude) - .map_err(TxMapAndMaybeTreesTraitError::InputSource) + .map_err(TxMapTraitError::InputSource) } fn get_unspent_transparent_output( &self, - outpoint: &zcash_primitives::transaction::components::OutPoint, + _outpoint: &zcash_primitives::transaction::components::OutPoint, ) -> Result, Self::Error> { - self.transaction_records_by_id - .get_unspent_transparent_output(outpoint) - .map_err(TxMapAndMaybeTreesTraitError::InputSource) + unimplemented!() } - fn get_unspent_transparent_outputs( + fn get_spendable_transparent_outputs( &self, - address: &zcash_primitives::legacy::TransparentAddress, - max_height: zcash_primitives::consensus::BlockHeight, - exclude: &[zcash_primitives::transaction::components::OutPoint], + _address: &zcash_primitives::legacy::TransparentAddress, + target_height: zcash_primitives::consensus::BlockHeight, + _min_confirmations: u32, ) -> Result, Self::Error> { self.transaction_records_by_id - .get_unspent_transparent_outputs(address, max_height, exclude) - .map_err(TxMapAndMaybeTreesTraitError::InputSource) + .get_spendable_transparent_outputs(_address, target_height, _min_confirmations) + .map_err(TxMapTraitError::InputSource) } } diff --git a/zingolib/src/wallet/tx_map_and_maybe_trees/trait_stub_walletcommitmenttrees.rs b/zingolib/src/wallet/tx_map/trait_stub_walletcommitmenttrees.rs similarity index 95% rename from zingolib/src/wallet/tx_map_and_maybe_trees/trait_stub_walletcommitmenttrees.rs rename to zingolib/src/wallet/tx_map/trait_stub_walletcommitmenttrees.rs index 9cedeff019..f57d17a542 100644 --- a/zingolib/src/wallet/tx_map_and_maybe_trees/trait_stub_walletcommitmenttrees.rs +++ b/zingolib/src/wallet/tx_map/trait_stub_walletcommitmenttrees.rs @@ -7,9 +7,9 @@ use zcash_client_backend::data_api::{chain::CommitmentTreeRoot, WalletCommitment use crate::data::witness_trees::OrchStore; use crate::data::witness_trees::SapStore; -use crate::wallet::tx_map_and_maybe_trees::TxMapAndMaybeTrees; +use crate::wallet::tx_map::TxMap; -impl WalletCommitmentTrees for TxMapAndMaybeTrees { +impl WalletCommitmentTrees for TxMap { // review! could this be a zingolib error? type Error = Infallible; diff --git a/zingolib/src/wallet/tx_map_and_maybe_trees/trait_walletread.rs b/zingolib/src/wallet/tx_map/trait_walletread.rs similarity index 89% rename from zingolib/src/wallet/tx_map_and_maybe_trees/trait_walletread.rs rename to zingolib/src/wallet/tx_map/trait_walletread.rs index 6112b583d7..0fd0278099 100644 --- a/zingolib/src/wallet/tx_map_and_maybe_trees/trait_walletread.rs +++ b/zingolib/src/wallet/tx_map/trait_walletread.rs @@ -1,8 +1,8 @@ -//! in this mod, we implement an LRZ type on the TxMapAndMaybeTrees +//! in this mod, we implement an LRZ type on the TxMap use crate::wallet::notes::{query::OutputSpendStatusQuery, Output, OutputInterface}; -use super::{TxMapAndMaybeTrees, TxMapAndMaybeTreesTraitError}; +use super::{TxMap, TxMapTraitError}; use secrecy::SecretVec; use shardtree::store::ShardStore; use zcash_client_backend::{ @@ -54,8 +54,8 @@ fn has_unspent_shielded_outputs( /// some of these functions, initially those required for calculate_transaction, will be implemented /// every doc-comment on a trait method is copied from the trait declaration in zcash_client_backend /// except those doc-comments starting with IMPL: -impl WalletRead for TxMapAndMaybeTrees { - type Error = TxMapAndMaybeTreesTraitError; +impl WalletRead for TxMap { + type Error = TxMapTraitError; type AccountId = AccountId; type Account = ZingoAccount; @@ -87,7 +87,7 @@ impl WalletRead for TxMapAndMaybeTrees { )>, Self::Error, > { - match self.witness_trees.as_ref() { + match self.witness_trees() { Some(trees) => { let opt_max_downloaded_height = match trees.witness_tree_orchard.store().max_checkpoint_id() { @@ -108,7 +108,7 @@ impl WalletRead for TxMapAndMaybeTrees { ) })) } - None => Err(TxMapAndMaybeTreesTraitError::NoSpendCapability), + None => Err(TxMapTraitError::NoSpendCapability), } } @@ -153,7 +153,7 @@ impl WalletRead for TxMapAndMaybeTrees { } fn get_account_ids(&self) -> Result, Self::Error> { - unimplemented!() + Ok(vec![(AccountId::ZERO)]) } fn get_account( &self, @@ -203,7 +203,7 @@ impl WalletRead for TxMapAndMaybeTrees { &self, ) -> Result, Self::Error> { self.witness_trees() - .ok_or(TxMapAndMaybeTreesTraitError::NoSpendCapability)? + .ok_or(TxMapTraitError::NoSpendCapability)? .witness_tree_orchard .store() .max_checkpoint_id() @@ -325,6 +325,30 @@ impl WalletRead for TxMapAndMaybeTrees { > { Ok(std::collections::HashMap::new()) } + + fn transaction_data_requests( + &self, + ) -> Result, Self::Error> { + unimplemented!() + } + + fn get_known_ephemeral_addresses( + &self, + _account: Self::AccountId, + _index_range: Option>, + ) -> Result< + Vec<( + zcash_primitives::legacy::TransparentAddress, + TransparentAddressMetadata, + )>, + Self::Error, + > { + Ok(self + .transparent_child_ephemeral_addresses + .iter() + .cloned() + .collect()) + } } #[cfg(test)] @@ -335,6 +359,7 @@ mod tests { use zcash_client_backend::data_api::WalletRead; use zcash_primitives::consensus::BlockHeight; use zingo_status::confirmation_status::ConfirmationStatus::Confirmed; + use zingo_status::confirmation_status::ConfirmationStatus::Mempool; use crate::{ mocks::default_txid, @@ -347,17 +372,17 @@ mod tests { }, }; - use super::TxMapAndMaybeTrees; - use super::TxMapAndMaybeTreesTraitError; + use super::TxMap; + use super::TxMapTraitError; #[test] fn get_target_and_anchor_heights() { - let mut transaction_records_and_maybe_trees = - TxMapAndMaybeTrees::new_with_witness_trees_address_free(); + let mut transaction_records_and_maybe_trees = TxMap::new_with_witness_trees_address_free(); transaction_records_and_maybe_trees - .witness_trees + .spending_data .as_mut() .unwrap() + .witness_trees_mut() .add_checkpoint(8421.into()); assert_eq!( @@ -371,8 +396,7 @@ mod tests { #[test] fn get_target_and_anchor_heights_none() { - let transaction_records_and_maybe_trees = - TxMapAndMaybeTrees::new_with_witness_trees_address_free(); + let transaction_records_and_maybe_trees = TxMap::new_with_witness_trees_address_free(); assert_eq!( transaction_records_and_maybe_trees .get_target_and_anchor_heights(NonZeroU32::new(10).unwrap()) @@ -383,20 +407,20 @@ mod tests { #[test] fn get_target_and_anchor_heights_err() { - let transaction_records_and_maybe_trees = TxMapAndMaybeTrees::new_treeless_address_free(); - assert_eq!( + let transaction_records_and_maybe_trees = TxMap::new_treeless_address_free(); + assert!(matches!( transaction_records_and_maybe_trees .get_target_and_anchor_heights(NonZeroU32::new(10).unwrap()) .err() .unwrap(), - TxMapAndMaybeTreesTraitError::NoSpendCapability - ); + TxMapTraitError::NoSpendCapability + )); } proptest! { #[test] fn get_min_unspent_height(sapling_height: u32, orchard_height: u32) { - let mut transaction_records_and_maybe_trees = TxMapAndMaybeTrees::new_with_witness_trees_address_free(); + let mut transaction_records_and_maybe_trees = TxMap::new_with_witness_trees_address_free(); // these first three outputs will not trigger min_unspent_note transaction_records_and_maybe_trees @@ -407,12 +431,13 @@ mod tests { .status(Confirmed(1000000.into())) .build(), ); - let spend = Some((default_txid(), 112358)); + let spend = Some((default_txid(), Confirmed(112358.into()))); + let mempool_spend = Some((default_txid(), Mempool(112357.into()))); transaction_records_and_maybe_trees .transaction_records_by_id .insert_transaction_record( TransactionRecordBuilder::default() - .sapling_notes(SaplingNoteBuilder::default().spent(spend).clone()) + .sapling_notes(SaplingNoteBuilder::default().spending_tx_status(spend).clone()) .status(Confirmed(2000000.into())) .randomize_txid() .build(), @@ -421,7 +446,7 @@ mod tests { .transaction_records_by_id .insert_transaction_record( TransactionRecordBuilder::default() - .orchard_notes(OrchardNoteBuilder::default().pending_spent(spend).clone()) + .orchard_notes(OrchardNoteBuilder::default().spending_tx_status(mempool_spend).clone()) .status(Confirmed(3000000.into())) .randomize_txid() .build(), @@ -452,7 +477,7 @@ mod tests { #[test] fn get_tx_height(tx_height: u32) { - let mut transaction_records_and_maybe_trees = TxMapAndMaybeTrees::new_with_witness_trees_address_free(); + let mut transaction_records_and_maybe_trees = TxMap::new_with_witness_trees_address_free(); let transaction_record = TransactionRecordBuilder::default().randomize_txid().status(Confirmed(tx_height.into())) .build(); diff --git a/zingolib/src/wallet/tx_map/trait_walletwrite.rs b/zingolib/src/wallet/tx_map/trait_walletwrite.rs new file mode 100644 index 0000000000..f35c5481c2 --- /dev/null +++ b/zingolib/src/wallet/tx_map/trait_walletwrite.rs @@ -0,0 +1,138 @@ +//! currently only implementing one method of WalletWrite + +use std::iter; + +use zcash_client_backend::data_api::WalletWrite; + +use super::{TxMap, TxMapTraitError}; + +impl WalletWrite for TxMap { + type UtxoRef = u32; + + fn create_account( + &mut self, + _seed: &secrecy::SecretVec, + _birthday: &zcash_client_backend::data_api::AccountBirthday, + ) -> Result<(Self::AccountId, zcash_keys::keys::UnifiedSpendingKey), Self::Error> { + unimplemented!() + } + + fn get_next_available_address( + &mut self, + _account: Self::AccountId, + _request: zcash_keys::keys::UnifiedAddressRequest, + ) -> Result, Self::Error> { + unimplemented!() + } + + fn update_chain_tip( + &mut self, + _tip_height: zcash_primitives::consensus::BlockHeight, + ) -> Result<(), Self::Error> { + unimplemented!() + } + + fn put_blocks( + &mut self, + _from_state: &zcash_client_backend::data_api::chain::ChainState, + _blocks: Vec>, + ) -> Result<(), Self::Error> { + unimplemented!() + } + + fn put_received_transparent_utxo( + &mut self, + _output: &zcash_client_backend::wallet::WalletTransparentOutput, + ) -> Result { + unimplemented!() + } + + fn store_decrypted_tx( + &mut self, + _received_tx: zcash_client_backend::data_api::DecryptedTransaction, + ) -> Result<(), Self::Error> { + unimplemented!() + } + + fn store_transactions_to_be_sent( + &mut self, + transactions: &[zcash_client_backend::data_api::SentTransaction], + ) -> Result<(), Self::Error> { + for tx in transactions { + let tx = tx.tx(); + let mut raw_tx = vec![]; + tx.write(&mut raw_tx) + .map_err(TxMapTraitError::TransactionWrite)?; + + if let Some(spending_data) = self.spending_data_mut() { + spending_data + .cached_raw_transactions_mut() + .push((tx.txid(), raw_tx)); + } else { + return Err(TxMapTraitError::NoSpendCapability); + } + } + Ok(()) + } + + fn truncate_to_height( + &mut self, + _block_height: zcash_primitives::consensus::BlockHeight, + ) -> Result<(), Self::Error> { + unimplemented!() + } + + fn import_account_hd( + &mut self, + _seed: &secrecy::SecretVec, + _account_index: zip32::AccountId, + _birthday: &zcash_client_backend::data_api::AccountBirthday, + ) -> Result<(Self::Account, zcash_keys::keys::UnifiedSpendingKey), Self::Error> { + unimplemented!() + } + + fn import_account_ufvk( + &mut self, + _unified_key: &zcash_keys::keys::UnifiedFullViewingKey, + _birthday: &zcash_client_backend::data_api::AccountBirthday, + _purpose: zcash_client_backend::data_api::AccountPurpose, + ) -> Result { + unimplemented!() + } + + fn set_transaction_status( + &mut self, + _txid: zcash_primitives::transaction::TxId, + _status: zcash_client_backend::data_api::TransactionStatus, + ) -> Result<(), Self::Error> { + unimplemented!() + } + + fn reserve_next_n_ephemeral_addresses( + &mut self, + _account_id: Self::AccountId, + n: usize, + ) -> Result< + Vec<( + zcash_primitives::legacy::TransparentAddress, + zcash_client_backend::wallet::TransparentAddressMetadata, + )>, + Self::Error, + > { + self.spending_data() + .as_ref() + .map(|spending_data| { + iter::repeat_with(|| { + crate::wallet::data::new_persistent_ephemeral_address( + &self.transparent_child_ephemeral_addresses, + spending_data.transparent_ephemeral_ivk(), + ) + .map_err(TxMapTraitError::TexSendError) + }) + .take(n) + .collect::>() + }) + .transpose()? + .ok_or(TxMapTraitError::NoSpendCapability) + } +} diff --git a/zingolib/src/wallet/tx_map_and_maybe_trees.rs b/zingolib/src/wallet/tx_map_and_maybe_trees.rs deleted file mode 100644 index d6d576a04c..0000000000 --- a/zingolib/src/wallet/tx_map_and_maybe_trees.rs +++ /dev/null @@ -1,100 +0,0 @@ -//! This mod should be called tx_map_and_maybe_trees.rs. it contains -//! struct TxMapAndMaybeTrees -//! implementations for TxMapAndMaybeTrees -//! associated types for TxMapAndMaybeTrees that have no relevance elsewhere. - -use crate::{ - data::witness_trees::WitnessTrees, - wallet::transaction_records_by_id::{ - trait_inputsource::InputSourceError, TransactionRecordsById, - }, -}; -use std::{fmt::Debug, sync::Arc}; -use thiserror::Error; -use zcash_primitives::legacy::TransparentAddress; - -/// HashMap of all transactions in a wallet, keyed by txid. -/// Note that the parent is expected to hold a RwLock, so we will assume that all accesses to -/// this struct are threadsafe/locked properly. -pub struct TxMapAndMaybeTrees { - /// TODO: Doc-comment! - pub transaction_records_by_id: TransactionRecordsById, - witness_trees: Option, - pub(crate) transparent_child_addresses: - Arc>, -} - -pub mod get; -pub mod read_write; -pub mod recording; - -impl TxMapAndMaybeTrees { - pub(crate) fn new_with_witness_trees( - transparent_child_addresses: Arc< - append_only_vec::AppendOnlyVec<(usize, TransparentAddress)>, - >, - ) -> TxMapAndMaybeTrees { - Self { - transaction_records_by_id: TransactionRecordsById::new(), - witness_trees: Some(WitnessTrees::default()), - transparent_child_addresses, - } - } - pub(crate) fn new_treeless( - transparent_child_addresses: Arc< - append_only_vec::AppendOnlyVec<(usize, TransparentAddress)>, - >, - ) -> TxMapAndMaybeTrees { - Self { - transaction_records_by_id: TransactionRecordsById::new(), - witness_trees: None, - transparent_child_addresses, - } - } - /// TODO: Doc-comment! - pub fn witness_trees(&self) -> Option<&WitnessTrees> { - self.witness_trees.as_ref() - } - pub(crate) fn witness_trees_mut(&mut self) -> Option<&mut WitnessTrees> { - self.witness_trees.as_mut() - } - /// TODO: Doc-comment! - pub fn clear(&mut self) { - self.transaction_records_by_id.clear(); - self.witness_trees.as_mut().map(WitnessTrees::clear); - } -} -#[cfg(test)] -impl TxMapAndMaybeTrees { - /// For any unit tests that don't require a WalletCapability, where the addresses come from - pub(crate) fn new_with_witness_trees_address_free() -> TxMapAndMaybeTrees { - Self { - transaction_records_by_id: TransactionRecordsById::new(), - witness_trees: Some(WitnessTrees::default()), - transparent_child_addresses: Arc::new(append_only_vec::AppendOnlyVec::new()), - } - } - /// For any unit tests that don't require a WalletCapability, where the addresses come from - pub(crate) fn new_treeless_address_free() -> TxMapAndMaybeTrees { - Self { - transaction_records_by_id: TransactionRecordsById::new(), - witness_trees: None, - transparent_child_addresses: Arc::new(append_only_vec::AppendOnlyVec::new()), - } - } -} - -/// TODO: Doc-comment! -#[derive(Debug, PartialEq, Error)] -pub enum TxMapAndMaybeTreesTraitError { - /// TODO: Doc-comment! - #[error("No witness trees. This is viewkey watch, not a spendkey wallet.")] - NoSpendCapability, - /// TODO: Doc-comment! - #[error("{0:?}")] - InputSource(InputSourceError), -} - -pub mod trait_stub_inputsource; -pub mod trait_stub_walletcommitmenttrees; -pub mod trait_walletread; diff --git a/zingolib/test-data/README b/zingolib/test-data/README deleted file mode 100644 index 01a2999940..0000000000 --- a/zingolib/test-data/README +++ /dev/null @@ -1 +0,0 @@ -By default, failing test state is not erased. diff --git a/zingolib/test-data/openssl_cfg b/zingolib/test-data/openssl_cfg deleted file mode 100644 index 253a47e8b2..0000000000 --- a/zingolib/test-data/openssl_cfg +++ /dev/null @@ -1,8 +0,0 @@ -[dn] -CN=localhost -[req] -distinguished_name = dn -[EXT] -subjectAltName=DNS:localhost -keyUsage=digitalSignature -extendedKeyUsage=serverAuth,clientAuth \ No newline at end of file