From ea1d68fd0304b5a2a455bd30f9512e9ad28b0346 Mon Sep 17 00:00:00 2001 From: Anynomous Date: Thu, 18 Sep 2025 14:56:43 +0200 Subject: [PATCH] Normalize line endings, attempt 2 --- .github/ISSUE_TEMPLATE/bug_report.md | 31 + .github/ISSUE_TEMPLATE/feature_request.md | 20 + .github/workflows/cd.yaml | 73 + .github/workflows/ci.yaml | 33 + .gitignore | 15 + .hooks/pre-commit | 50 + CODE_OF_CONDUCT.md | 3 + Cargo.lock | 4626 +++++++++++++++++ Cargo.toml | 79 + LICENSE | 177 + README.md | 25 + api/Cargo.toml | 57 + api/src/foreign.rs | 515 ++ api/src/foreign_rpc.rs | 569 ++ api/src/lib.rs | 59 + api/src/owner.rs | 2643 ++++++++++ api/src/owner_rpc.rs | 2726 ++++++++++ api/src/types.rs | 340 ++ api/tests/slate_versioning.rs | 134 + api/tests/slates/v1_req.slate | 1037 ++++ api/tests/slates/v1_res.slate | 1955 +++++++ config/Cargo.toml | 45 + config/src/comments.rs | 396 ++ config/src/config.rs | 478 ++ config/src/lib.rs | 38 + config/src/types.rs | 273 + controller/Cargo.toml | 88 + controller/src/command.rs | 1482 ++++++ controller/src/controller.rs | 875 ++++ controller/src/display.rs | 630 +++ controller/src/error.rs | 105 + controller/src/lib.rs | 38 + controller/tests/accounts.rs | 276 + controller/tests/build_chain.rs | 171 + controller/tests/build_output.rs | 101 + controller/tests/check.rs | 878 ++++ controller/tests/common/mod.rs | 178 + controller/tests/file.rs | 320 ++ controller/tests/invoice.rs | 305 ++ controller/tests/late_lock.rs | 158 + controller/tests/mwixnet.rs | 190 + controller/tests/no_change.rs | 212 + controller/tests/payment_proofs.rs | 172 + controller/tests/repost.rs | 268 + controller/tests/revert.rs | 379 ++ controller/tests/self_send.rs | 148 + controller/tests/slatepack.rs | 587 +++ controller/tests/transaction.rs | 601 +++ controller/tests/ttl_cutoff.rs | 185 + controller/tests/tx_list_filter.rs | 360 ++ controller/tests/updater_thread.rs | 123 + doc/design/design.md | 89 + doc/design/goals.md | 82 + doc/design/wallet-arch.png | Bin 0 -> 115352 bytes doc/design/wallet-arch.puml | 110 + doc/samples/v3_api_node/package-lock.json | 115 + doc/samples/v3_api_node/package.json | 14 + doc/samples/v3_api_node/readme.md | 28 + doc/samples/v3_api_node/src/index.js | 134 + doc/tls-setup.md | 74 + doc/transaction/basic-transaction-wf.png | Bin 0 -> 157285 bytes doc/transaction/basic-transaction-wf.puml | 97 + impls/Cargo.toml | 81 + impls/src/adapters/file.rs | 73 + impls/src/adapters/http.rs | 292 ++ impls/src/adapters/mod.rs | 58 + impls/src/adapters/slatepack.rs | 179 + impls/src/backends/lmdb.rs | 767 +++ impls/src/backends/mod.rs | 17 + impls/src/client_utils/client.rs | 289 + impls/src/client_utils/json_rpc.rs | 264 + impls/src/client_utils/mod.rs | 18 + impls/src/error.rs | 104 + impls/src/lib.rs | 91 + impls/src/lifecycle/default.rs | 395 ++ impls/src/lifecycle/mod.rs | 18 + impls/src/lifecycle/seed.rs | 385 ++ impls/src/node_clients/http.rs | 436 ++ impls/src/node_clients/mod.rs | 18 + impls/src/node_clients/resp_types.rs | 31 + impls/src/test_framework/mod.rs | 276 + impls/src/test_framework/testclient.rs | 644 +++ impls/src/tor/bridge.rs | 660 +++ impls/src/tor/config.rs | 382 ++ impls/src/tor/mod.rs | 18 + impls/src/tor/process.rs | 288 + impls/src/tor/proxy.rs | 191 + integration/Cargo.toml | 39 + integration/src/lib.rs | 21 + integration/tests/api.rs | 485 ++ integration/tests/dandelion.rs | 157 + integration/tests/framework.rs | 682 +++ integration/tests/simulnet.rs | 1007 ++++ integration/tests/stratum.rs | 177 + libwallet/Cargo.toml | 73 + libwallet/src/address.rs | 49 + libwallet/src/api_impl.rs | 27 + libwallet/src/api_impl/foreign.rs | 233 + libwallet/src/api_impl/owner.rs | 1478 ++++++ libwallet/src/api_impl/owner_updater.rs | 146 + libwallet/src/api_impl/types.rs | 346 ++ libwallet/src/error.rs | 363 ++ libwallet/src/internal.rs | 28 + libwallet/src/internal/keys.rs | 129 + libwallet/src/internal/scan.rs | 645 +++ libwallet/src/internal/selection.rs | 716 +++ libwallet/src/internal/tx.rs | 683 +++ libwallet/src/internal/updater.rs | 885 ++++ libwallet/src/lib.rs | 90 + libwallet/src/mwixnet/mod.rs | 24 + libwallet/src/mwixnet/onion/crypto/comsig.rs | 230 + libwallet/src/mwixnet/onion/crypto/dalek.rs | 297 ++ libwallet/src/mwixnet/onion/crypto/mod.rs | 21 + libwallet/src/mwixnet/onion/crypto/secp.rs | 98 + libwallet/src/mwixnet/onion/mod.rs | 207 + libwallet/src/mwixnet/onion/onion.rs | 438 ++ libwallet/src/mwixnet/onion/util.rs | 185 + libwallet/src/mwixnet/types.rs | 43 + libwallet/src/slate.rs | 1101 ++++ libwallet/src/slate_versions/mod.rs | 122 + libwallet/src/slate_versions/ser.rs | 660 +++ libwallet/src/slate_versions/v4.rs | 387 ++ libwallet/src/slate_versions/v4_bin.rs | 592 +++ libwallet/src/slatepack/address.rs | 287 + libwallet/src/slatepack/armor.rs | 198 + libwallet/src/slatepack/mod.rs | 25 + libwallet/src/slatepack/packer.rs | 121 + libwallet/src/slatepack/types.rs | 850 +++ libwallet/src/types.rs | 1138 ++++ libwallet/tests/libwallet.rs | 527 ++ libwallet/tests/slate_versioning.rs | 96 + libwallet/tests/slates/v2.slate | 54 + rustfmt.toml | 2 + src/bin/grin-wallet.rs | 166 + src/bin/grin-wallet.yml | 448 ++ src/build/build.rs | 48 + src/cli/cli.rs | 312 ++ src/cli/mod.rs | 17 + src/cmd/mod.rs | 18 + src/cmd/wallet.rs | 78 + src/cmd/wallet_args.rs | 1299 +++++ src/lib.rs | 24 + tests/cmd_line_basic.rs | 726 +++ tests/common/mod.rs | 497 ++ tests/data/v2_reqs/init_send_tx.req.json | 21 + tests/data/v2_reqs/retrieve_info.req.json | 9 + tests/data/v3_reqs/change_password.req.json | 10 + tests/data/v3_reqs/close_wallet.req.json | 8 + tests/data/v3_reqs/create_config.req.json | 11 + tests/data/v3_reqs/create_wallet.req.json | 11 + .../v3_reqs/create_wallet_invalid_mn.req.json | 11 + .../v3_reqs/create_wallet_valid_mn.req.json | 11 + tests/data/v3_reqs/delete_wallet.req.json | 8 + tests/data/v3_reqs/get_top_level.req.json | 7 + tests/data/v3_reqs/init_secure_api.req.json | 8 + tests/data/v3_reqs/init_send_tx.req.json | 23 + tests/data/v3_reqs/open_wallet.req.json | 9 + tests/data/v3_reqs/retrieve_info.req.json | 10 + tests/owner_v3_init_secure.rs | 245 + tests/owner_v3_lifecycle.rs | 623 +++ tests/tor_dev_helper.rs | 100 + util/Cargo.toml | 40 + util/src/byte_ser.rs | 384 ++ util/src/lib.rs | 31 + util/src/ov3.rs | 204 + 165 files changed, 53524 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/workflows/cd.yaml create mode 100644 .github/workflows/ci.yaml create mode 100644 .gitignore create mode 100644 .hooks/pre-commit create mode 100644 CODE_OF_CONDUCT.md create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 api/Cargo.toml create mode 100644 api/src/foreign.rs create mode 100644 api/src/foreign_rpc.rs create mode 100644 api/src/lib.rs create mode 100644 api/src/owner.rs create mode 100644 api/src/owner_rpc.rs create mode 100644 api/src/types.rs create mode 100644 api/tests/slate_versioning.rs create mode 100644 api/tests/slates/v1_req.slate create mode 100644 api/tests/slates/v1_res.slate create mode 100644 config/Cargo.toml create mode 100644 config/src/comments.rs create mode 100644 config/src/config.rs create mode 100644 config/src/lib.rs create mode 100644 config/src/types.rs create mode 100644 controller/Cargo.toml create mode 100644 controller/src/command.rs create mode 100644 controller/src/controller.rs create mode 100644 controller/src/display.rs create mode 100644 controller/src/error.rs create mode 100644 controller/src/lib.rs create mode 100644 controller/tests/accounts.rs create mode 100644 controller/tests/build_chain.rs create mode 100644 controller/tests/build_output.rs create mode 100644 controller/tests/check.rs create mode 100644 controller/tests/common/mod.rs create mode 100644 controller/tests/file.rs create mode 100644 controller/tests/invoice.rs create mode 100644 controller/tests/late_lock.rs create mode 100644 controller/tests/mwixnet.rs create mode 100644 controller/tests/no_change.rs create mode 100644 controller/tests/payment_proofs.rs create mode 100644 controller/tests/repost.rs create mode 100644 controller/tests/revert.rs create mode 100644 controller/tests/self_send.rs create mode 100644 controller/tests/slatepack.rs create mode 100644 controller/tests/transaction.rs create mode 100644 controller/tests/ttl_cutoff.rs create mode 100644 controller/tests/tx_list_filter.rs create mode 100644 controller/tests/updater_thread.rs create mode 100644 doc/design/design.md create mode 100644 doc/design/goals.md create mode 100644 doc/design/wallet-arch.png create mode 100644 doc/design/wallet-arch.puml create mode 100644 doc/samples/v3_api_node/package-lock.json create mode 100644 doc/samples/v3_api_node/package.json create mode 100644 doc/samples/v3_api_node/readme.md create mode 100644 doc/samples/v3_api_node/src/index.js create mode 100644 doc/tls-setup.md create mode 100644 doc/transaction/basic-transaction-wf.png create mode 100644 doc/transaction/basic-transaction-wf.puml create mode 100644 impls/Cargo.toml create mode 100644 impls/src/adapters/file.rs create mode 100644 impls/src/adapters/http.rs create mode 100644 impls/src/adapters/mod.rs create mode 100644 impls/src/adapters/slatepack.rs create mode 100644 impls/src/backends/lmdb.rs create mode 100644 impls/src/backends/mod.rs create mode 100644 impls/src/client_utils/client.rs create mode 100644 impls/src/client_utils/json_rpc.rs create mode 100644 impls/src/client_utils/mod.rs create mode 100644 impls/src/error.rs create mode 100644 impls/src/lib.rs create mode 100644 impls/src/lifecycle/default.rs create mode 100644 impls/src/lifecycle/mod.rs create mode 100644 impls/src/lifecycle/seed.rs create mode 100644 impls/src/node_clients/http.rs create mode 100644 impls/src/node_clients/mod.rs create mode 100644 impls/src/node_clients/resp_types.rs create mode 100644 impls/src/test_framework/mod.rs create mode 100644 impls/src/test_framework/testclient.rs create mode 100644 impls/src/tor/bridge.rs create mode 100644 impls/src/tor/config.rs create mode 100644 impls/src/tor/mod.rs create mode 100644 impls/src/tor/process.rs create mode 100644 impls/src/tor/proxy.rs create mode 100644 integration/Cargo.toml create mode 100644 integration/src/lib.rs create mode 100644 integration/tests/api.rs create mode 100644 integration/tests/dandelion.rs create mode 100644 integration/tests/framework.rs create mode 100644 integration/tests/simulnet.rs create mode 100644 integration/tests/stratum.rs create mode 100644 libwallet/Cargo.toml create mode 100644 libwallet/src/address.rs create mode 100644 libwallet/src/api_impl.rs create mode 100644 libwallet/src/api_impl/foreign.rs create mode 100644 libwallet/src/api_impl/owner.rs create mode 100644 libwallet/src/api_impl/owner_updater.rs create mode 100644 libwallet/src/api_impl/types.rs create mode 100644 libwallet/src/error.rs create mode 100644 libwallet/src/internal.rs create mode 100644 libwallet/src/internal/keys.rs create mode 100644 libwallet/src/internal/scan.rs create mode 100644 libwallet/src/internal/selection.rs create mode 100644 libwallet/src/internal/tx.rs create mode 100644 libwallet/src/internal/updater.rs create mode 100644 libwallet/src/lib.rs create mode 100644 libwallet/src/mwixnet/mod.rs create mode 100644 libwallet/src/mwixnet/onion/crypto/comsig.rs create mode 100644 libwallet/src/mwixnet/onion/crypto/dalek.rs create mode 100644 libwallet/src/mwixnet/onion/crypto/mod.rs create mode 100644 libwallet/src/mwixnet/onion/crypto/secp.rs create mode 100644 libwallet/src/mwixnet/onion/mod.rs create mode 100644 libwallet/src/mwixnet/onion/onion.rs create mode 100644 libwallet/src/mwixnet/onion/util.rs create mode 100644 libwallet/src/mwixnet/types.rs create mode 100644 libwallet/src/slate.rs create mode 100644 libwallet/src/slate_versions/mod.rs create mode 100644 libwallet/src/slate_versions/ser.rs create mode 100644 libwallet/src/slate_versions/v4.rs create mode 100644 libwallet/src/slate_versions/v4_bin.rs create mode 100644 libwallet/src/slatepack/address.rs create mode 100644 libwallet/src/slatepack/armor.rs create mode 100644 libwallet/src/slatepack/mod.rs create mode 100644 libwallet/src/slatepack/packer.rs create mode 100644 libwallet/src/slatepack/types.rs create mode 100644 libwallet/src/types.rs create mode 100644 libwallet/tests/libwallet.rs create mode 100644 libwallet/tests/slate_versioning.rs create mode 100644 libwallet/tests/slates/v2.slate create mode 100644 rustfmt.toml create mode 100644 src/bin/grin-wallet.rs create mode 100644 src/bin/grin-wallet.yml create mode 100644 src/build/build.rs create mode 100644 src/cli/cli.rs create mode 100644 src/cli/mod.rs create mode 100644 src/cmd/mod.rs create mode 100644 src/cmd/wallet.rs create mode 100644 src/cmd/wallet_args.rs create mode 100644 src/lib.rs create mode 100644 tests/cmd_line_basic.rs create mode 100644 tests/common/mod.rs create mode 100644 tests/data/v2_reqs/init_send_tx.req.json create mode 100644 tests/data/v2_reqs/retrieve_info.req.json create mode 100644 tests/data/v3_reqs/change_password.req.json create mode 100644 tests/data/v3_reqs/close_wallet.req.json create mode 100644 tests/data/v3_reqs/create_config.req.json create mode 100644 tests/data/v3_reqs/create_wallet.req.json create mode 100644 tests/data/v3_reqs/create_wallet_invalid_mn.req.json create mode 100644 tests/data/v3_reqs/create_wallet_valid_mn.req.json create mode 100644 tests/data/v3_reqs/delete_wallet.req.json create mode 100644 tests/data/v3_reqs/get_top_level.req.json create mode 100644 tests/data/v3_reqs/init_secure_api.req.json create mode 100644 tests/data/v3_reqs/init_send_tx.req.json create mode 100644 tests/data/v3_reqs/open_wallet.req.json create mode 100644 tests/data/v3_reqs/retrieve_info.req.json create mode 100644 tests/owner_v3_init_secure.rs create mode 100644 tests/owner_v3_lifecycle.rs create mode 100644 tests/tor_dev_helper.rs create mode 100644 util/Cargo.toml create mode 100644 util/src/byte_ser.rs create mode 100644 util/src/lib.rs create mode 100644 util/src/ov3.rs diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..94ddb90 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,31 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..bbcbbe7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/cd.yaml b/.github/workflows/cd.yaml new file mode 100644 index 0000000..956814e --- /dev/null +++ b/.github/workflows/cd.yaml @@ -0,0 +1,73 @@ +name: Continuous Deployment + +on: + push: + tags: + - "v*.*.*" + +jobs: + linux-release: + name: Linux Release + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Build + run: cargo build --release + - name: Archive + working-directory: target/release + run: tar -czvf grin-wallet-${{ github.ref_name }}-linux-x86_64.tar.gz grin-wallet + - name: Create Checksum + working-directory: target/release + run: openssl sha256 grin-wallet-${{ github.ref_name }}-linux-x86_64.tar.gz > grin-wallet-${{ github.ref_name }}-linux-x86_64-sha256sum.txt + - name: Release + uses: softprops/action-gh-release@v1 + with: + generate_release_notes: true + files: | + target/release/grin-wallet-${{ github.ref_name }}-linux-x86_64.tar.gz + target/release/grin-wallet-${{ github.ref_name }}-linux-x86_64-sha256sum.txt + + macos-release: + name: macOS Release + runs-on: macos-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Build + run: cargo build --release + - name: Archive + working-directory: target/release + run: tar -czvf grin-wallet-${{ github.ref_name }}-macos-x86_64.tar.gz grin-wallet + - name: Create Checksum + working-directory: target/release + run: openssl sha256 grin-wallet-${{ github.ref_name }}-macos-x86_64.tar.gz > grin-wallet-${{ github.ref_name }}-macos-x86_64-sha256sum.txt + - name: Release + uses: softprops/action-gh-release@v1 + with: + files: | + target/release/grin-wallet-${{ github.ref_name }}-macos-x86_64.tar.gz + target/release/grin-wallet-${{ github.ref_name }}-macos-x86_64-sha256sum.txt + + windows-release: + name: Windows Release + runs-on: windows-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Build + run: cargo build --release + - name: Archive + uses: vimtor/action-zip@v1 + with: + files: target/release/grin-wallet.exe + dest: target/release/grin-wallet-${{ github.ref_name }}-win-x86_64.zip + - name: Create Checksum + working-directory: target/release + shell: pwsh + run: get-filehash -algorithm sha256 grin-wallet-${{ github.ref_name }}-win-x86_64.zip | Format-List |  Out-String | ForEach-Object { $_.Trim() } > grin-wallet-${{ github.ref_name }}-win-x86_64-sha256sum.txt + - name: Release + uses: softprops/action-gh-release@v1 + with: + files: | + target/release/grin-wallet-${{ github.ref_name }}-win-x86_64.zip + target/release/grin-wallet-${{ github.ref_name }}-win-x86_64-sha256sum.txt \ No newline at end of file diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..783258e --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,33 @@ +name: Continuous Integration +on: [push, pull_request] + +jobs: + linux-tests: + name: Linux Tests + runs-on: ubuntu-latest + strategy: + matrix: + job_args: [api, config, controller, impls, libwallet, .] + steps: + - uses: actions/checkout@v3 + - name: Test ${{ matrix.job_args }} + working-directory: ${{ matrix.job_args }} + run: cargo test --release + + macos-tests: + name: macOS Tests + runs-on: macos-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Tests + run: cargo test --release --all + + windows-tests: + name: Windows Tests + runs-on: windows-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Tests + run: cargo test --release --all \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a662463 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +*.swp +.DS_Store +.grin* +node* +!node_clients +!node_clients.rs +target +*/Cargo.lock +*.iml +grin.log +wallet.seed +test_output +.idea/ +*.vs +.gitattributes diff --git a/.hooks/pre-commit b/.hooks/pre-commit new file mode 100644 index 0000000..8056d6c --- /dev/null +++ b/.hooks/pre-commit @@ -0,0 +1,50 @@ +#!/usr/bin/env bash + +# Copyright 2018 The Grin Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +rustfmt --version &>/dev/null +if [ $? != 0 ]; then + printf "[pre_commit] \033[0;31merror\033[0m: \"rustfmt\" not available. \n" + printf "[pre_commit] \033[0;31merror\033[0m: rustfmt can be installed via - \n" + printf "[pre_commit] $ rustup component add rustfmt-preview \n" + exit 1 +fi + +problem_files=() + +# first collect all the files that need reformatting +for file in $(git diff --name-only --cached); do + if [ ${file: -3} == ".rs" ]; then + rustfmt --check $file &>/dev/null + if [ $? != 0 ]; then + problem_files+=($file) + fi + fi +done + +if [ ${#problem_files[@]} == 0 ]; then + # nothing to do + printf "[pre_commit] rustfmt \033[0;32mok\033[0m \n" +else + # reformat the files that need it and re-stage them. + printf "[pre_commit] the following files were rustfmt'd before commit: \n" + for file in ${problem_files[@]}; do + rustfmt $file + git add $file + printf "\033[0;32m $file\033[0m \n" + done +fi + +exit 0 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..a32b302 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,3 @@ +# Code of Conduct + +The Code of Conduct for this repository [can be found online](https://grin.mw/policies/code_of_conduct). \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..bd3c379 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,4626 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "aead" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877" +dependencies = [ + "generic-array 0.14.7", +] + +[[package]] +name = "age" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23100453ca2a1bbda9bfc6deac1bebb828d7e66ba481ebccfedfddf29321b6b9" +dependencies = [ + "age-core", + "base64 0.13.1", + "bech32 0.8.1", + "chacha20poly1305", + "cookie-factory", + "hkdf", + "hmac 0.11.0", + "i18n-embed", + "i18n-embed-fl", + "lazy_static", + "nom", + "pin-project", + "rand 0.7.3", + "rand 0.8.5", + "rust-embed", + "scrypt", + "sha2 0.9.9", + "subtle", + "x25519-dalek 1.1.1", + "zeroize", +] + +[[package]] +name = "age-core" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70afa630ef12a4fc666277713efbe6da2bc87bb3f3af0f1149415b701362c615" +dependencies = [ + "base64 0.13.1", + "chacha20poly1305", + "cookie-factory", + "hkdf", + "nom", + "rand 0.8.5", + "secrecy 0.8.0", + "sha2 0.9.9", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "ansi_term" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "anyhow" +version = "1.0.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74f37166d7d48a0284b99dd824694c26119c700b53bf0d1540cdb147dbdaaf13" + +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd9fd44efafa8690358b7408d253adf110036b88f55672a933f01d616ad9b1b9" +dependencies = [ + "nodrop", +] + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "autocfg" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dde43e75fd43e8a1bf86103336bc699aa8d17ad1be60c76c0bdfd4828e19b78" +dependencies = [ + "autocfg 1.4.0", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if 1.0.0", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", +] + +[[package]] +name = "base64" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "489d6c0ed21b11d038c31b6ceccca973e65d73ba3bd8ecb9a2babf5546164643" +dependencies = [ + "byteorder", + "safemem", +] + +[[package]] +name = "base64" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" + +[[package]] +name = "base64" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "basic-toml" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "823388e228f614e9558c6804262db37960ec8821856535f5c3f59913140558f8" +dependencies = [ + "serde", +] + +[[package]] +name = "bech32" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dabbe35f96fb9507f7330793dc490461b2962659ac5d427181e451a623751d1" + +[[package]] +name = "bech32" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9ff0bbfd639f15c74af777d81383cf53efb7c93613f6cab67c6c11e05bbf8b" + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitflags" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4efd02e230a02e18f92fc2735f44597385ed02ad8f831e7c1c1156ee5e1ab3a5" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "blake2-rfc" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6d530bdd2d52966a6d03b7a964add7ae1a288d25214066fd4b600f0f796400" +dependencies = [ + "arrayvec 0.4.12", + "constant_time_eq", +] + +[[package]] +name = "blake2b_simd" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587" +dependencies = [ + "arrayref", + "arrayvec 0.5.2", + "constant_time_eq", +] + +[[package]] +name = "block-buffer" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" +dependencies = [ + "block-padding", + "byte-tools", + "byteorder", + "generic-array 0.12.4", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array 0.14.7", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array 0.14.7", +] + +[[package]] +name = "block-padding" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" +dependencies = [ + "byte-tools", +] + +[[package]] +name = "bs58" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "476e9cd489f9e121e02ffa6014a8ef220ecb15c05ed23fc34cca13925dc283fb" + +[[package]] +name = "built" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c360505aed52b7ec96a3636c3f039d99103c37d1d9b4f7a8c743d3ea9ffcd03b" +dependencies = [ + "git2", +] + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "byte-tools" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c" +dependencies = [ + "byteorder", + "iovec", +] + +[[package]] +name = "bytes" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" + +[[package]] +name = "bytes" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" + +[[package]] +name = "cc" +version = "1.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chacha20" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c80e5460aa66fe3b91d40bcbdab953a597b60053e34d684ac6903f863b680a6" +dependencies = [ + "cfg-if 1.0.0", + "cipher", + "cpufeatures", + "zeroize", +] + +[[package]] +name = "chacha20poly1305" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18446b09be63d457bbec447509e85f662f32952b035ce892290396bc0b0cff5" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits 0.2.19", + "serde", + "wasm-bindgen", + "windows-targets", +] + +[[package]] +name = "cipher" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" +dependencies = [ + "generic-array 0.14.7", +] + +[[package]] +name = "clap" +version = "2.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +dependencies = [ + "ansi_term 0.12.1", + "atty", + "bitflags 1.3.2", + "strsim 0.8.0", + "textwrap", + "unicode-width", + "vec_map", + "yaml-rust", +] + +[[package]] +name = "cloudabi" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "cookie-factory" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2" +dependencies = [ + "futures 0.3.31", +] + +[[package]] +name = "core-foundation" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171" +dependencies = [ + "core-foundation-sys 0.7.0", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys 0.8.7", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "croaring" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "611eaefca84c93e431ad82dfb848f6e05a99e25148384f45a3852b0fbe1c8086" +dependencies = [ + "byteorder", + "croaring-sys", +] + +[[package]] +name = "croaring-sys" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e5fed89265a702f0085844237a7ebbadf8a7c42de6304fddca30a5013f9aecb" +dependencies = [ + "cc", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array 0.14.7", + "typenum", +] + +[[package]] +name = "crypto-mac" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" +dependencies = [ + "generic-array 0.14.7", + "subtle", +] + +[[package]] +name = "csv" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe" +dependencies = [ + "csv-core", + "itoa 1.0.11", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70" +dependencies = [ + "memchr", +] + +[[package]] +name = "ct-logs" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d3686f5fa27dbc1d76c751300376e167c5a43387f44bb451fd1c24776e49113" +dependencies = [ + "sct", +] + +[[package]] +name = "ctor" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096" +dependencies = [ + "quote 1.0.37", + "syn 1.0.109", +] + +[[package]] +name = "curve25519-dalek" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9b85542f99a2dfa2a1b8e192662741c9859a846b296bef1c92ef9b58b5a216" +dependencies = [ + "byteorder", + "digest 0.8.1", + "rand_core 0.5.1", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" +dependencies = [ + "byteorder", + "digest 0.9.0", + "rand_core 0.5.1", + "subtle", + "zeroize", +] + +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if 1.0.0", + "hashbrown 0.14.5", + "lock_api 0.4.12", + "once_cell", + "parking_lot_core 0.9.10", +] + +[[package]] +name = "data-encoding" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 1.0.109", +] + +[[package]] +name = "destructure_traitobject" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c877555693c14d2f84191cfd3ad8582790fc52b5e2274b40b59cf5f5cea25c7" + +[[package]] +name = "difference" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" + +[[package]] +name = "digest" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" +dependencies = [ + "generic-array 0.12.4", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array 0.14.7", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common", + "subtle", +] + +[[package]] +name = "dirs" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fd78930633bd1c6e35c4b42b1df7b0cbc6bc191146e512bb3bedf243fcc3901" +dependencies = [ + "libc", + "redox_users 0.3.5", + "winapi 0.3.9", +] + +[[package]] +name = "dirs" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13aea89a5c93364a98e9b37b2fa237effbb694d5cfe01c5b70941f7eb087d5e3" +dependencies = [ + "cfg-if 0.1.10", + "dirs-sys", +] + +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-next" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf36e65a80337bea855cd4ef9b8401ffce06a7baedf2e85ec467b1ac3f6e82b6" +dependencies = [ + "cfg-if 1.0.0", + "dirs-sys-next", +] + +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if 1.0.0", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users 0.4.6", + "winapi 0.3.9", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users 0.4.6", + "winapi 0.3.9", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.86", +] + +[[package]] +name = "easy-jsonrpc-mw" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3b1a91569d50e3bba3c9febb22ef54d78c6e8a8d8dd91ae859896c8ba05f4e3" +dependencies = [ + "easy-jsonrpc-proc-macro-mw", + "jsonrpc-core", + "rand 0.6.5", + "serde", + "serde_json", +] + +[[package]] +name = "easy-jsonrpc-proc-macro-mw" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6368dbd2c6685fb84fc6e6a4749917ddc98905793fd06341c7e11a2504f2724" +dependencies = [ + "heck", + "proc-macro2 0.4.30", + "quote 0.6.13", + "syn 0.15.44", +] + +[[package]] +name = "ed25519" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7" +dependencies = [ + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" +dependencies = [ + "curve25519-dalek 3.2.0", + "ed25519", + "rand 0.7.3", + "serde", + "sha2 0.9.9", + "zeroize", +] + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "enum_primitive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4551092f4d519593039259a9ed8daedf0da12e5109c5280338073eaeb81180" +dependencies = [ + "num-traits 0.1.43", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "fastrand" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" + +[[package]] +name = "find-crate" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59a98bbaacea1c0eb6a0876280051b892eb73594fd90cf3b20e9c817029c57d2" +dependencies = [ + "toml", +] + +[[package]] +name = "flate2" +version = "1.0.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fluent" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb74634707bebd0ce645a981148e8fb8c7bccd4c33c652aeffd28bf2f96d555a" +dependencies = [ + "fluent-bundle", + "unic-langid", +] + +[[package]] +name = "fluent-bundle" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe0a21ee80050c678013f82edf4b705fe2f26f1f9877593d13198612503f493" +dependencies = [ + "fluent-langneg", + "fluent-syntax", + "intl-memoizer", + "intl_pluralrules", + "rustc-hash", + "self_cell 0.10.3", + "smallvec", + "unic-langid", +] + +[[package]] +name = "fluent-langneg" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4ad0989667548f06ccd0e306ed56b61bd4d35458d54df5ec7587c0e8ed5e94" +dependencies = [ + "unic-langid", +] + +[[package]] +name = "fluent-syntax" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a530c4694a6a8d528794ee9bbd8ba0122e779629ac908d15ad5a7ae7763a33d" +dependencies = [ + "thiserror", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + +[[package]] +name = "fuchsia-zircon" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" +dependencies = [ + "bitflags 1.3.2", + "fuchsia-zircon-sys", +] + +[[package]] +name = "fuchsia-zircon-sys" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" + +[[package]] +name = "futures" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a471a38ef8ed83cd6e40aa59c1ffe17db6855c18e3604d9c4ed8c08ebc28678" + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.86", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite 0.2.15", + "pin-utils", + "slab", +] + +[[package]] +name = "gcc" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" + +[[package]] +name = "generic-array" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" +dependencies = [ + "typenum", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "git2" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b903b73e45dc0c6c596f2d37eccece7c1c8bb6e4407b001096387c63d0d93724" +dependencies = [ + "bitflags 2.6.0", + "libc", + "libgit2-sys", + "log", + "url", +] + +[[package]] +name = "grin_api" +version = "5.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6443239e75a15c517ce24fe4dd3ef257898ae0224e6b7c993db28794e13ecf3e" +dependencies = [ + "bytes 0.5.6", + "easy-jsonrpc-mw", + "futures 0.3.31", + "grin_chain", + "grin_core", + "grin_p2p", + "grin_pool", + "grin_store", + "grin_util", + "http", + "hyper", + "hyper-rustls 0.20.0", + "hyper-timeout", + "lazy_static", + "log", + "regex", + "ring", + "rustls 0.17.0", + "serde", + "serde_derive", + "serde_json", + "thiserror", + "tokio", + "tokio-rustls 0.13.1", + "url", +] + +[[package]] +name = "grin_chain" +version = "5.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdedcee9e20e0cc8dc1460b37b3951306c774a6aa5788e3f45addc56e60f7fbe" +dependencies = [ + "bit-vec", + "bitflags 1.3.2", + "byteorder", + "chrono", + "croaring", + "enum_primitive", + "grin_core", + "grin_keychain", + "grin_store", + "grin_util", + "lazy_static", + "log", + "lru-cache", + "serde", + "serde_derive", + "thiserror", +] + +[[package]] +name = "grin_core" +version = "5.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4fbf242a2a23a6554dd4c430574d2563993cf5f44921f953635e8a177cf2506" +dependencies = [ + "blake2-rfc", + "byteorder", + "bytes 0.5.6", + "chrono", + "croaring", + "enum_primitive", + "grin_keychain", + "grin_util", + "lazy_static", + "log", + "lru-cache", + "num", + "num-bigint", + "rand 0.6.5", + "serde", + "serde_derive", + "siphasher", + "thiserror", + "zeroize", +] + +[[package]] +name = "grin_keychain" +version = "5.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abeb1c22f48269a0bf463de589f8961d8bbb554def5e5e405710ead9a1b4c71a" +dependencies = [ + "blake2-rfc", + "byteorder", + "digest 0.9.0", + "grin_util", + "hmac 0.11.0", + "lazy_static", + "log", + "pbkdf2 0.8.0", + "rand 0.6.5", + "ripemd160", + "serde", + "serde_derive", + "serde_json", + "sha2 0.9.9", + "zeroize", +] + +[[package]] +name = "grin_p2p" +version = "5.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6b0ecc5597244d698ec4e1ea2e7d04c3b209f496db16731a04f06af41572c54" +dependencies = [ + "bitflags 1.3.2", + "bytes 0.5.6", + "chrono", + "enum_primitive", + "grin_chain", + "grin_core", + "grin_store", + "grin_util", + "log", + "lru-cache", + "num", + "rand 0.6.5", + "serde", + "serde_derive", + "tempfile", +] + +[[package]] +name = "grin_pool" +version = "5.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14b758b202f034a03b894e732c716ddece8994fae5d8da1e074b1a9e07219f2b" +dependencies = [ + "blake2-rfc", + "chrono", + "grin_core", + "grin_keychain", + "grin_util", + "log", + "rand 0.6.5", + "serde", + "serde_derive", + "thiserror", +] + +[[package]] +name = "grin_secp256k1zkp" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06832645c3d28079245827908045946db48e5d62b4c54b40701c7e21327e2571" +dependencies = [ + "arrayvec 0.7.6", + "cc", + "libc", + "rand 0.5.6", + "serde", + "serde_json", + "zeroize", +] + +[[package]] +name = "grin_store" +version = "5.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "708e53758263fa17dfd3fc46419ded59a84d07818e1ee7526b0d47ba268f1362" +dependencies = [ + "byteorder", + "croaring", + "grin_core", + "grin_util", + "libc", + "lmdb-zero", + "log", + "memmap", + "serde", + "serde_derive", + "tempfile", + "thiserror", +] + +[[package]] +name = "grin_util" +version = "5.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1433d76d0e95c2ed20e1008e9891adfae64812d1bdc5ec71ed5499dcaca2986" +dependencies = [ + "anyhow", + "backtrace", + "base64 0.12.3", + "byteorder", + "grin_secp256k1zkp", + "lazy_static", + "log", + "log4rs", + "parking_lot 0.10.2", + "rand 0.6.5", + "serde", + "serde_derive", + "walkdir", + "zeroize", + "zip", +] + +[[package]] +name = "grin_wallet" +version = "5.4.0-alpha.1" +dependencies = [ + "built", + "clap", + "easy-jsonrpc-mw", + "grin_api", + "grin_core", + "grin_keychain", + "grin_util", + "grin_wallet_api", + "grin_wallet_config", + "grin_wallet_controller", + "grin_wallet_impls", + "grin_wallet_libwallet", + "grin_wallet_util", + "lazy_static", + "linefeed", + "log", + "prettytable-rs", + "remove_dir_all", + "rpassword", + "rustyline", + "semver", + "serde", + "serde_derive", + "serde_json", + "thiserror", + "url", +] + +[[package]] +name = "grin_wallet_api" +version = "5.4.0-alpha.1" +dependencies = [ + "base64 0.12.3", + "chrono", + "easy-jsonrpc-mw", + "ed25519-dalek", + "grin_core", + "grin_keychain", + "grin_util", + "grin_wallet_config", + "grin_wallet_impls", + "grin_wallet_libwallet", + "grin_wallet_util", + "log", + "rand 0.6.5", + "ring", + "serde", + "serde_derive", + "serde_json", + "tempfile", + "uuid", +] + +[[package]] +name = "grin_wallet_config" +version = "5.4.0-alpha.1" +dependencies = [ + "dirs 2.0.2", + "grin_core", + "grin_util", + "grin_wallet_util", + "log", + "pretty_assertions", + "rand 0.6.5", + "serde", + "serde_derive", + "toml", +] + +[[package]] +name = "grin_wallet_controller" +version = "5.4.0-alpha.1" +dependencies = [ + "chrono", + "easy-jsonrpc-mw", + "ed25519-dalek", + "futures 0.3.31", + "grin_api", + "grin_chain", + "grin_core", + "grin_keychain", + "grin_util", + "grin_wallet_api", + "grin_wallet_config", + "grin_wallet_impls", + "grin_wallet_libwallet", + "grin_wallet_util", + "hyper", + "lazy_static", + "log", + "prettytable-rs", + "qr_code", + "rand 0.7.3", + "remove_dir_all", + "ring", + "serde", + "serde_derive", + "serde_json", + "term 0.6.1", + "thiserror", + "tokio", + "url", + "uuid", +] + +[[package]] +name = "grin_wallet_impls" +version = "5.4.0-alpha.1" +dependencies = [ + "base64 0.12.3", + "blake2-rfc", + "byteorder", + "chrono", + "data-encoding", + "ed25519-dalek", + "futures 0.3.31", + "grin_api", + "grin_chain", + "grin_core", + "grin_keychain", + "grin_store", + "grin_util", + "grin_wallet_config", + "grin_wallet_libwallet", + "grin_wallet_util", + "lazy_static", + "log", + "rand 0.6.5", + "regex", + "remove_dir_all", + "reqwest", + "ring", + "serde", + "serde_derive", + "serde_json", + "sysinfo", + "thiserror", + "timer", + "tokio", + "url", + "uuid", + "x25519-dalek 0.6.0", +] + +[[package]] +name = "grin_wallet_libwallet" +version = "5.4.0-alpha.1" +dependencies = [ + "age", + "base64 0.9.3", + "bech32 0.7.3", + "blake2-rfc", + "bs58", + "byteorder", + "chacha20", + "chrono", + "curve25519-dalek 2.1.3", + "ed25519-dalek", + "grin_core", + "grin_keychain", + "grin_store", + "grin_util", + "grin_wallet_config", + "grin_wallet_util", + "hmac 0.12.1", + "lazy_static", + "log", + "num-bigint", + "rand 0.6.5", + "regex", + "secrecy 0.6.0", + "serde", + "serde_derive", + "serde_json", + "sha2 0.10.8", + "strum", + "strum_macros", + "thiserror", + "uuid", + "x25519-dalek 0.6.0", +] + +[[package]] +name = "grin_wallet_util" +version = "5.4.0-alpha.1" +dependencies = [ + "data-encoding", + "ed25519-dalek", + "grin_util", + "pretty_assertions", + "rand 0.6.5", + "serde", + "serde_derive", + "sha3", + "thiserror", +] + +[[package]] +name = "h2" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e4728fd124914ad25e99e3d15a9361a879f6620f63cb56bbb08f95abb97a535" +dependencies = [ + "bytes 0.5.6", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap 1.9.3", + "slab", + "tokio", + "tokio-util", + "tracing", + "tracing-futures", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" + +[[package]] +name = "hkdf" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01706d578d5c281058480e673ae4086a9f4710d8df1ad80a5b03e39ece5f886b" +dependencies = [ + "digest 0.9.0", + "hmac 0.11.0", +] + +[[package]] +name = "hmac" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" +dependencies = [ + "crypto-mac", + "digest 0.9.0", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes 1.8.0", + "fnv", + "itoa 1.0.11", +] + +[[package]] +name = "http-body" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13d5ff830006f7646652e057693569bfe0d51760c0085a071769d142a205111b" +dependencies = [ + "bytes 0.5.6", + "http", +] + +[[package]] +name = "httparse" +version = "1.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" + +[[package]] +name = "httpdate" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494b4d60369511e7dea41cf646832512a94e542f68bb9c49e54518e0f468eb47" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "hyper" +version = "0.13.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a6f157065790a3ed2f88679250419b5cdd96e714a0d65f7797fd337186e96bb" +dependencies = [ + "bytes 0.5.6", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa 0.4.8", + "pin-project", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac965ea399ec3a25ac7d13b8affd4b8f39325cca00858ddf5eb29b79e6b14b08" +dependencies = [ + "bytes 0.5.6", + "ct-logs", + "futures-util", + "hyper", + "log", + "rustls 0.17.0", + "rustls-native-certs", + "tokio", + "tokio-rustls 0.13.1", + "webpki", +] + +[[package]] +name = "hyper-rustls" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37743cc83e8ee85eacfce90f2f4102030d9ff0a95244098d781e9bee4a90abb6" +dependencies = [ + "bytes 0.5.6", + "futures-util", + "hyper", + "log", + "rustls 0.18.1", + "tokio", + "tokio-rustls 0.14.1", + "webpki", +] + +[[package]] +name = "hyper-timeout" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d1f9b0b8258e3ef8f45928021d3ef14096c2b93b99e4b8cfcabf1f58ec84b0a" +dependencies = [ + "bytes 0.5.6", + "hyper", + "tokio", + "tokio-io-timeout", +] + +[[package]] +name = "hyper-tls" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d979acc56dcb5b8dddba3917601745e877576475aa046df3226eabdecef78eed" +dependencies = [ + "bytes 0.5.6", + "hyper", + "native-tls", + "tokio", + "tokio-tls", +] + +[[package]] +name = "i18n-config" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e88074831c0be5b89181b05e6748c4915f77769ecc9a4c372f88b169a8509c9" +dependencies = [ + "basic-toml", + "log", + "serde", + "serde_derive", + "thiserror", + "unic-langid", +] + +[[package]] +name = "i18n-embed" +version = "0.13.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92a86226a7a16632de6723449ee5fe70bac5af718bc642ee9ca2f0f6e14fa1fa" +dependencies = [ + "arc-swap", + "fluent", + "fluent-langneg", + "fluent-syntax", + "i18n-embed-impl", + "intl-memoizer", + "lazy_static", + "log", + "parking_lot 0.12.3", + "rust-embed", + "thiserror", + "unic-langid", + "walkdir", +] + +[[package]] +name = "i18n-embed-fl" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26a3d3569737dfaac7fc1c4078e6af07471c3060b8e570bcd83cdd5f4685395" +dependencies = [ + "dashmap", + "find-crate", + "fluent", + "fluent-syntax", + "i18n-config", + "i18n-embed", + "lazy_static", + "proc-macro-error", + "proc-macro2 1.0.89", + "quote 1.0.37", + "strsim 0.10.0", + "syn 2.0.86", + "unic-langid", +] + +[[package]] +name = "i18n-embed-impl" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f2cc0e0523d1fe6fc2c6f66e5038624ea8091b3e7748b5e8e0c84b1698db6c2" +dependencies = [ + "find-crate", + "i18n-config", + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.86", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys 0.8.7", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg 1.4.0", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +dependencies = [ + "equivalent", + "hashbrown 0.15.0", +] + +[[package]] +name = "intl-memoizer" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe22e020fce238ae18a6d5d8c502ee76a52a6e880d99477657e6acc30ec57bda" +dependencies = [ + "type-map", + "unic-langid", +] + +[[package]] +name = "intl_pluralrules" +version = "7.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078ea7b7c29a2b4df841a7f6ac8775ff6074020c6776d48491ce2268e068f972" +dependencies = [ + "unic-langid", +] + +[[package]] +name = "iovec" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" +dependencies = [ + "libc", +] + +[[package]] +name = "ipnet" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" + +[[package]] +name = "is-terminal" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" +dependencies = [ + "hermit-abi 0.4.0", + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + +[[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.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "jsonrpc-core" +version = "10.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc15eef5f8b6bef5ac5f7440a957ff95d036e2f98706947741bfc93d1976db4c" +dependencies = [ + "futures 0.1.31", + "log", + "serde", + "serde_derive", + "serde_json", +] + +[[package]] +name = "keccak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "kernel32-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.161" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" + +[[package]] +name = "libgit2-sys" +version = "0.17.0+1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10472326a8a6477c3c20a64547b0059e4b0d086869eee31e6d7da728a8eb7224" +dependencies = [ + "cc", + "libc", + "libz-sys", + "pkg-config", +] + +[[package]] +name = "liblmdb-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "feed38a3a580f60bf61aaa067b0ff4123395966839adeaf67258a9e50c4d2e49" +dependencies = [ + "gcc", + "libc", +] + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.6.0", + "libc", +] + +[[package]] +name = "libz-sys" +version = "1.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2d16453e800a8cf6dd2fc3eb4bc99b786a9b90c663b8559a5b1a041bf89e472" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linefeed" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28715d08e35c6c074f9ae6b2e6a2420bac75d050c66ecd669d7d5b98e2caa036" +dependencies = [ + "dirs 1.0.5", + "mortal", + "winapi 0.3.9", +] + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "lmdb-zero" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13416eee745b087c22934f35f1f24da22da41ba2a5ce197143d168ce055cc58d" +dependencies = [ + "bitflags 0.9.1", + "libc", + "liblmdb-sys", + "supercow", +] + +[[package]] +name = "lock_api" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4da24a77a3d8a6d4862d95f72e6fdb9c09a643ecdb402d754004a557f2bec75" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg 1.4.0", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +dependencies = [ + "serde", +] + +[[package]] +name = "log-mdc" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a94d21414c1f4a51209ad204c1776a3d0765002c76c6abcb602a6f09f1e881c7" + +[[package]] +name = "log4rs" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0816135ae15bd0391cf284eab37e6e3ee0a6ee63d2ceeb659862bd8d0a984ca6" +dependencies = [ + "anyhow", + "arc-swap", + "chrono", + "derivative", + "flate2", + "fnv", + "humantime", + "libc", + "log", + "log-mdc", + "once_cell", + "parking_lot 0.12.3", + "rand 0.8.5", + "serde", + "serde-value", + "serde_json", + "serde_yaml", + "thiserror", + "thread-id", + "typemap-ors", + "winapi 0.3.9", +] + +[[package]] +name = "lru-cache" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memmap" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6585fd95e7bb50d6cc31e20d4cf9afb4e2ba16c5846fc76793f11218da9c475b" +dependencies = [ + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "0.6.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4" +dependencies = [ + "cfg-if 0.1.10", + "fuchsia-zircon", + "fuchsia-zircon-sys", + "iovec", + "kernel32-sys", + "libc", + "log", + "miow 0.2.2", + "net2", + "slab", + "winapi 0.2.8", +] + +[[package]] +name = "mio-named-pipes" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0840c1c50fd55e521b247f949c241c9997709f23bd7f023b9762cd561e935656" +dependencies = [ + "log", + "mio", + "miow 0.3.7", + "winapi 0.3.9", +] + +[[package]] +name = "mio-uds" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afcb699eb26d4332647cc848492bbc15eafb26f08d0304550d5aa1f612e066f0" +dependencies = [ + "iovec", + "libc", + "mio", +] + +[[package]] +name = "miow" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d" +dependencies = [ + "kernel32-sys", + "net2", + "winapi 0.2.8", + "ws2_32-sys", +] + +[[package]] +name = "miow" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "mortal" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c624fa1b7aab6bd2aff6e9b18565cc0363b6d45cbcd7465c9ed5e3740ebf097" +dependencies = [ + "bitflags 2.6.0", + "libc", + "nix 0.26.4", + "smallstr", + "terminfo", + "unicode-normalization", + "unicode-width", + "winapi 0.3.9", +] + +[[package]] +name = "native-tls" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework 2.11.1", + "security-framework-sys 2.12.0", + "tempfile", +] + +[[package]] +name = "net2" +version = "0.2.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b13b648036a2339d06de780866fbdfda0dde886de7b3af2ddeba8b14f4ee34ac" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "nix" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83450fe6a6142ddd95fb064b746083fc4ef1705fe81f64a64e1d4b39f54a1055" +dependencies = [ + "bitflags 1.3.2", + "cc", + "cfg-if 0.1.10", + "libc", +] + +[[package]] +name = "nix" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +dependencies = [ + "bitflags 1.3.2", + "cfg-if 1.0.0", + "libc", +] + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "ntapi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "num" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8536030f9fea7127f841b45bb6243b27255787fb4eb83958aa1ef9d2fdc0c36" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits 0.2.19", +] + +[[package]] +name = "num-bigint" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304" +dependencies = [ + "autocfg 1.4.0", + "num-integer", + "num-traits 0.2.19", +] + +[[package]] +name = "num-complex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b19411a9719e753aff12e5187b74d60d3dc449ec3f4dc21e3989c3f554bc95" +dependencies = [ + "autocfg 1.4.0", + "num-traits 0.2.19", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits 0.2.19", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg 1.4.0", + "num-integer", + "num-traits 0.2.19", +] + +[[package]] +name = "num-rational" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c000134b5dbf44adc5cb772486d335293351644b801551abe8f75c84cfa4aef" +dependencies = [ + "autocfg 1.4.0", + "num-bigint", + "num-integer", + "num-traits 0.2.19", +] + +[[package]] +name = "num-traits" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31" +dependencies = [ + "num-traits 0.2.19", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg 1.4.0", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi 0.3.9", + "libc", +] + +[[package]] +name = "object" +version = "0.36.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "opaque-debug" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openssl" +version = "0.10.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" +dependencies = [ + "bitflags 2.6.0", + "cfg-if 1.0.0", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.86", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits 0.2.19", +] + +[[package]] +name = "output_vt100" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "628223faebab4e3e40667ee0b2336d34a5b960ff60ea743ddfdbcf7770bcfb66" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "parking_lot" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3a704eb390aafdc107b0e392f56a82b668e3a71366993b5340f5833fd62505e" +dependencies = [ + "lock_api 0.3.4", + "parking_lot_core 0.7.3", +] + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api 0.4.12", + "parking_lot_core 0.9.10", +] + +[[package]] +name = "parking_lot_core" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b93f386bb233083c799e6e642a9d73db98c24a5deeb95ffc85bf281255dffc98" +dependencies = [ + "cfg-if 0.1.10", + "cloudabi", + "libc", + "redox_syscall 0.1.57", + "smallvec", + "winapi 0.3.9", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "redox_syscall 0.5.7", + "smallvec", + "windows-targets", +] + +[[package]] +name = "password-hash" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77e0b28ace46c5a396546bcf443bf422b57049617433d8854227352a4a9b24e7" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "pbkdf2" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95f5254224e617595d2cc3cc73ff0a5eaf2637519e25f03388154e9378b6ffa" +dependencies = [ + "base64ct", + "crypto-mac", + "hmac 0.11.0", + "password-hash", + "sha2 0.9.9", +] + +[[package]] +name = "pbkdf2" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271779f35b581956db91a3e55737327a03aa051e90b1c47aeb189508533adfd7" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared", + "rand 0.8.5", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be57f64e946e500c8ee36ef6331845d40a93055567ec57e8fae13efd33759b95" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c" +dependencies = [ + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.86", +] + +[[package]] +name = "pin-project-lite" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "257b64915a082f7811703966789728173279bdebb956b143dbcd23f6f970a777" + +[[package]] +name = "pin-project-lite" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + +[[package]] +name = "poly1305" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "048aeb476be11a4b6ca432ca569e375810de9294ae78f4774e78ea98a9246ede" +dependencies = [ + "cpufeatures", + "opaque-debug 0.3.1", + "universal-hash", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "pretty_assertions" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f81e1644e1b54f5a68959a29aa86cde704219254669da328ecfdf6a1f09d427" +dependencies = [ + "ansi_term 0.11.0", + "ctor", + "difference", + "output_vt100", +] + +[[package]] +name = "prettytable-rs" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eea25e07510aa6ab6547308ebe3c036016d162b8da920dbb079e3ba8acf3d95a" +dependencies = [ + "csv", + "encode_unicode", + "is-terminal", + "lazy_static", + "term 0.7.0", + "unicode-width", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2 1.0.89", + "quote 1.0.37", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "proc-macro2" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "qr_code" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5520fbcd7da152a449261c5a533a1c7fad044e9e8aa9528cfec3f464786c7926" + +[[package]] +name = "quote" +version = "0.6.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" +dependencies = [ + "proc-macro2 0.4.30", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2 1.0.89", +] + +[[package]] +name = "rand" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c618c47cd3ebd209790115ab837de41425723956ad3ce2e6a7f09890947cacb9" +dependencies = [ + "cloudabi", + "fuchsia-cprng", + "libc", + "rand_core 0.3.1", + "winapi 0.3.9", +] + +[[package]] +name = "rand" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" +dependencies = [ + "autocfg 0.1.8", + "libc", + "rand_chacha 0.1.1", + "rand_core 0.4.2", + "rand_hc 0.1.0", + "rand_isaac", + "rand_jitter", + "rand_os", + "rand_pcg", + "rand_xorshift", + "winapi 0.3.9", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc 0.2.0", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" +dependencies = [ + "autocfg 0.1.8", + "rand_core 0.3.1", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.15", +] + +[[package]] +name = "rand_hc" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_isaac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rand_jitter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" +dependencies = [ + "libc", + "rand_core 0.4.2", + "winapi 0.3.9", +] + +[[package]] +name = "rand_os" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" +dependencies = [ + "cloudabi", + "fuchsia-cprng", + "libc", + "rand_core 0.4.2", + "rdrand", + "winapi 0.3.9", +] + +[[package]] +name = "rand_pcg" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" +dependencies = [ + "autocfg 0.1.8", + "rand_core 0.4.2", +] + +[[package]] +name = "rand_xorshift" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "redox_syscall" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" + +[[package]] +name = "redox_syscall" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +dependencies = [ + "bitflags 2.6.0", +] + +[[package]] +name = "redox_users" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d" +dependencies = [ + "getrandom 0.1.16", + "redox_syscall 0.1.57", + "rust-argon2", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.15", + "libredox", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "remove_dir_all" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "882f368737489ea543bc5c340e6f3d34a28c39980bd9a979e47322b26f60ac40" +dependencies = [ + "libc", + "log", + "num_cpus", + "rayon", + "winapi 0.3.9", +] + +[[package]] +name = "reqwest" +version = "0.10.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0718f81a8e14c4dbb3b34cf23dc6aaf9ab8a0dfec160c534b3dbca1aaa21f47c" +dependencies = [ + "base64 0.13.1", + "bytes 0.5.6", + "encoding_rs", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "hyper-rustls 0.21.0", + "hyper-tls", + "ipnet", + "js-sys", + "lazy_static", + "log", + "mime", + "mime_guess", + "native-tls", + "percent-encoding", + "pin-project-lite 0.2.15", + "rustls 0.18.1", + "serde", + "serde_urlencoded", + "tokio", + "tokio-rustls 0.14.1", + "tokio-socks", + "tokio-tls", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", + "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", + "untrusted", + "web-sys", + "winapi 0.3.9", +] + +[[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 0.3.1", +] + +[[package]] +name = "rpassword" +version = "4.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99371657d3c8e4d816fb6221db98fa408242b0b53bac08f8676a41f8554fe99f" +dependencies = [ + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "rust-argon2" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb" +dependencies = [ + "base64 0.13.1", + "blake2b_simd", + "constant_time_eq", + "crossbeam-utils", +] + +[[package]] +name = "rust-embed" +version = "6.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a36224c3276f8c4ebc8c20f158eca7ca4359c8db89991c4925132aaaf6702661" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "6.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49b94b81e5b2c284684141a2fb9e2a31be90638caf040bf9afbc5a0416afe1ac" +dependencies = [ + "proc-macro2 1.0.89", + "quote 1.0.37", + "rust-embed-utils", + "syn 2.0.86", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "7.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d38ff6bf570dc3bb7100fce9f7b60c33fa71d80e88da3f2580df4ff2bdded74" +dependencies = [ + "sha2 0.10.8", + "walkdir", +] + +[[package]] +name = "rustc-demangle" +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.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa260229e6538e52293eeb577aabd09945a09d6d9cc0fc550ed7529056c2e32a" +dependencies = [ + "bitflags 2.6.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0d4a31f5d68413404705d6982529b0e11a9aacd4839d1d6222ee3b8cb4015e1" +dependencies = [ + "base64 0.11.0", + "log", + "ring", + "sct", + "webpki", +] + +[[package]] +name = "rustls" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d1126dcf58e93cee7d098dbda643b5f92ed724f1f6a63007c1116eed6700c81" +dependencies = [ + "base64 0.12.3", + "log", + "ring", + "sct", + "webpki", +] + +[[package]] +name = "rustls-native-certs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75ffeb84a6bd9d014713119542ce415db3a3e4748f0bfce1e1416cd224a23a5" +dependencies = [ + "openssl-probe", + "rustls 0.17.0", + "schannel", + "security-framework 0.4.4", +] + +[[package]] +name = "rustversion" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" + +[[package]] +name = "rustyline" +version = "6.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f0d5e7b0219a3eadd5439498525d4765c59b7c993ef0c12244865cd2d988413" +dependencies = [ + "cfg-if 0.1.10", + "dirs-next 1.0.2", + "libc", + "log", + "memchr", + "nix 0.18.0", + "scopeguard", + "unicode-segmentation", + "unicode-width", + "utf8parse", + "winapi 0.3.9", +] + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "safemem" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" + +[[package]] +name = "salsa20" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0fbb5f676da676c260ba276a8f43a8dc67cf02d1438423aeb1c677a7212686" +dependencies = [ + "cipher", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01227be5826fa0690321a2ba6c5cd57a19cf3f6a09e76973b58e61de6ab9d1c1" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "scrypt" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e73d6d7c6311ebdbd9184ad6c4447b2f36337e327bda107d3ba9e3c374f9d325" +dependencies = [ + "hmac 0.12.1", + "pbkdf2 0.10.1", + "salsa20", + "sha2 0.10.8", +] + +[[package]] +name = "sct" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "secrecy" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9182278ed645df3477a9c27bfee0621c621aa16f6972635f7f795dae3d81070f" +dependencies = [ + "zeroize", +] + +[[package]] +name = "secrecy" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e" +dependencies = [ + "zeroize", +] + +[[package]] +name = "security-framework" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64808902d7d99f78eaddd2b4e2509713babc3dc3c85ad6f4c447680f3c01e535" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.7.0", + "core-foundation-sys 0.7.0", + "libc", + "security-framework-sys 0.4.3", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.6.0", + "core-foundation 0.9.4", + "core-foundation-sys 0.8.7", + "libc", + "security-framework-sys 2.12.0", +] + +[[package]] +name = "security-framework-sys" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17bf11d99252f512695eb468de5516e5cf75455521e69dfe343f3b74e4748405" +dependencies = [ + "core-foundation-sys 0.7.0", + "libc", +] + +[[package]] +name = "security-framework-sys" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6" +dependencies = [ + "core-foundation-sys 0.8.7", + "libc", +] + +[[package]] +name = "self_cell" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14e4d63b804dc0c7ec4a1e52bcb63f02c7ac94476755aa579edac21e01f915d" +dependencies = [ + "self_cell 1.0.4", +] + +[[package]] +name = "self_cell" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d369a96f978623eb3dc28807c4852d6cc617fed53da5d3c400feff1ef34a714a" + +[[package]] +name = "semver" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "394cec28fa623e00903caf7ba4fa6fb9a0e260280bb8cdbbba029611108a0190" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + +[[package]] +name = "serde" +version = "1.0.214" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float", + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.214" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" +dependencies = [ + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.86", +] + +[[package]] +name = "serde_json" +version = "1.0.132" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" +dependencies = [ + "itoa 1.0.11", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa 1.0.11", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap 2.6.0", + "itoa 1.0.11", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if 1.0.0", + "cpufeatures", + "digest 0.9.0", + "opaque-debug 0.3.1", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if 1.0.0", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha3" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd26bc0e7a2e3a7c959bc494caf58b72ee0c71d67704e9520f736ca7e4853ecf" +dependencies = [ + "block-buffer 0.7.3", + "byte-tools", + "digest 0.8.1", + "keccak", + "opaque-debug 0.2.3", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "1.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg 1.4.0", +] + +[[package]] +name = "smallstr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e922794d168678729ffc7e07182721a14219c65814e66e91b839a272fe5ae4f" +dependencies = [ + "smallvec", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "strum" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57bd81eb48f4c437cadc685403cad539345bf703d78e63707418431cecd4522b" + +[[package]] +name = "strum_macros" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87c85aa3f8ea653bfd3ddf25f7ee357ee4d204731f6aa9ad04002306f6e2774c" +dependencies = [ + "heck", + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 1.0.109", +] + +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + +[[package]] +name = "supercow" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "171758edb47aa306a78dfa4ab9aeb5167405bd4e3dc2b64e88f6a84bbe98bd63" + +[[package]] +name = "syn" +version = "0.15.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5" +dependencies = [ + "proc-macro2 0.4.30", + "quote 0.6.13", + "unicode-xid", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2 1.0.89", + "quote 1.0.37", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89275301d38033efb81a6e60e3497e734dfcc62571f2854bf4b16690398824c" +dependencies = [ + "proc-macro2 1.0.89", + "quote 1.0.37", + "unicode-ident", +] + +[[package]] +name = "sysinfo" +version = "0.29.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd727fc423c2060f6c92d9534cef765c65a6ed3f428a03d7def74a8c4348e666" +dependencies = [ + "cfg-if 1.0.0", + "core-foundation-sys 0.8.7", + "libc", + "ntapi", + "once_cell", + "rayon", + "winapi 0.3.9", +] + +[[package]] +name = "tempfile" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" +dependencies = [ + "cfg-if 1.0.0", + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "term" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0863a3345e70f61d613eab32ee046ccd1bcc5f9105fe402c61fcd0c13eeb8b5" +dependencies = [ + "dirs 2.0.2", + "winapi 0.3.9", +] + +[[package]] +name = "term" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +dependencies = [ + "dirs-next 2.0.0", + "rustversion", + "winapi 0.3.9", +] + +[[package]] +name = "terminfo" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666cd3a6681775d22b200409aad3b089c5b99fb11ecdd8a204d9d62f8148498f" +dependencies = [ + "dirs 4.0.0", + "fnv", + "nom", + "phf", + "phf_codegen", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "thiserror" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d171f59dbaa811dbbb1aee1e73db92ec2b122911a48e1390dfe327a821ddede" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b08be0f17bd307950653ce45db00cd31200d82b624b36e181337d9c7d92765b5" +dependencies = [ + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.86", +] + +[[package]] +name = "thread-id" +version = "4.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe8f25bbdd100db7e1d34acf7fd2dc59c4bf8f7483f505eaa7d4f12f76cc0ea" +dependencies = [ + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "timer" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31d42176308937165701f50638db1c31586f183f1aab416268216577aec7306b" +dependencies = [ + "chrono", +] + +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", +] + +[[package]] +name = "tinyvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6703a273949a90131b290be1fe7b039d0fc884aa1935860dfcbe056f28cd8092" +dependencies = [ + "bytes 0.5.6", + "fnv", + "futures-core", + "iovec", + "lazy_static", + "libc", + "memchr", + "mio", + "mio-named-pipes", + "mio-uds", + "num_cpus", + "pin-project-lite 0.1.12", + "signal-hook-registry", + "slab", + "tokio-macros", + "winapi 0.3.9", +] + +[[package]] +name = "tokio-io-timeout" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9390a43272c8a6ac912ed1d1e2b6abeafd5047e05530a2fa304deee041a06215" +dependencies = [ + "bytes 0.5.6", + "tokio", +] + +[[package]] +name = "tokio-macros" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e44da00bfc73a25f814cd8d7e57a68a5c31b74b3152a0a1d1f590c97ed06265a" +dependencies = [ + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 1.0.109", +] + +[[package]] +name = "tokio-rustls" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15cb62a0d2770787abc96e99c1cd98fcf17f94959f3af63ca85bdfb203f051b4" +dependencies = [ + "futures-core", + "rustls 0.17.0", + "tokio", + "webpki", +] + +[[package]] +name = "tokio-rustls" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e12831b255bcfa39dc0436b01e19fea231a37db570686c06ee72c423479f889a" +dependencies = [ + "futures-core", + "rustls 0.18.1", + "tokio", + "webpki", +] + +[[package]] +name = "tokio-socks" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d611fd5d241872372d52a0a3d309c52d0b95a6a67671a6c8f7ab2c4a37fb2539" +dependencies = [ + "bytes 0.4.12", + "either", + "futures 0.3.31", + "thiserror", + "tokio", +] + +[[package]] +name = "tokio-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a70f4fcd7b3b24fb194f837560168208f669ca8cb70d0c4b862944452396343" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be8242891f2b6cbef26a2d7e8605133c2c554cd35b3e4948ea892d6d68436499" +dependencies = [ + "bytes 0.5.6", + "futures-core", + "futures-sink", + "log", + "pin-project-lite 0.1.12", + "tokio", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "log", + "pin-project-lite 0.2.15", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tracing-futures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" +dependencies = [ + "pin-project", + "tracing", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "type-map" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deb68604048ff8fa93347f02441e4487594adc20bb8a084f9e564d2b827a0a9f" +dependencies = [ + "rustc-hash", +] + +[[package]] +name = "typemap-ors" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a68c24b707f02dd18f1e4ccceb9d49f2058c2fb86384ef9972592904d7a28867" +dependencies = [ + "unsafe-any-ors", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unic-langid" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23dd9d1e72a73b25e07123a80776aae3e7b0ec461ef94f9151eed6ec88005a44" +dependencies = [ + "unic-langid-impl", +] + +[[package]] +name = "unic-langid-impl" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a5422c1f65949306c99240b81de9f3f15929f5a8bfe05bb44b034cc8bf593e5" +dependencies = [ + "serde", + "tinystr", +] + +[[package]] +name = "unicase" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" + +[[package]] +name = "unicode-bidi" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" + +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-xid" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" + +[[package]] +name = "universal-hash" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05" +dependencies = [ + "generic-array 0.14.7", + "subtle", +] + +[[package]] +name = "unsafe-any-ors" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a303d30665362d9680d7d91d78b23f5f899504d4f08b3c4cf08d055d87c0ad" +dependencies = [ + "destructure_traitobject", +] + +[[package]] +name = "unsafe-libyaml" +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 = "url" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom 0.2.15", + "serde", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" +dependencies = [ + "cfg-if 1.0.0", + "once_cell", + "serde", + "serde_json", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.86", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" +dependencies = [ + "cfg-if 1.0.0", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" +dependencies = [ + "quote 1.0.37", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" +dependencies = [ + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.86", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" + +[[package]] +name = "web-sys" +version = "0.3.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki" +version = "0.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "webpki-roots" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f20dea7535251981a9670857150d571846545088359b28e4951d350bdaf179f" +dependencies = [ + "webpki", +] + +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winreg" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "ws2_32-sys" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + +[[package]] +name = "x25519-dalek" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "637ff90c9540fa3073bb577e65033069e4bae7c79d49d74aa3ffdf5342a53217" +dependencies = [ + "curve25519-dalek 2.1.3", + "rand_core 0.5.1", + "zeroize", +] + +[[package]] +name = "x25519-dalek" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a0c105152107e3b96f6a00a65e86ce82d9b125230e1c4302940eca58ff71f4f" +dependencies = [ + "curve25519-dalek 3.2.0", + "rand_core 0.5.1", + "zeroize", +] + +[[package]] +name = "yaml-rust" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e66366e18dc58b46801afbf2ca7661a9f59cc8c5962c29892b6039b4f86fa992" + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.86", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.86", +] + +[[package]] +name = "zip" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93ab48844d61251bb3835145c521d88aa4031d7139e8485990f60ca911fa0815" +dependencies = [ + "byteorder", + "crc32fast", + "thiserror", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..a70b09a --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,79 @@ +[package] +name = "grin_wallet" +version = "5.4.0-alpha.1" +authors = ["Grin Developers "] +description = "Simple, private and scalable cryptocurrency implementation based on the MimbleWimble chain format." +license = "Apache-2.0" +repository = "https://github.com/mimblewimble/grin-wallet" +keywords = [ "crypto", "grin", "mimblewimble" ] +readme = "README.md" +exclude = ["**/*.grin", "**/*.grin2"] +build = "src/build/build.rs" +edition = "2018" + +[[bin]] +name = "grin-wallet" +path = "src/bin/grin-wallet.rs" + +[workspace] +members = ["api", "config", "controller", "impls", "libwallet", "util"] +exclude = ["integration"] + +[dependencies] +clap = { version = "2.33", features = ["yaml"] } +rpassword = "4.0" +thiserror = "1" +prettytable-rs = "0.10" +log = "0.4" +linefeed = "0.6" +semver = "0.10" +rustyline = "6" +lazy_static = "1" + +grin_wallet_api = { path = "./api", version = "5.4.0-alpha.1" } +grin_wallet_impls = { path = "./impls", version = "5.4.0-alpha.1" } +grin_wallet_libwallet = { path = "./libwallet", version = "5.4.0-alpha.1" } +grin_wallet_controller = { path = "./controller", version = "5.4.0-alpha.1" } +grin_wallet_config = { path = "./config", version = "5.4.0-alpha.1" } +grin_wallet_util = { path = "./util", version = "5.4.0-alpha.1" } + + +##### Grin Imports + +# For Release +grin_core = "5.3.3" +grin_keychain = "5.3.3" +grin_util = "5.3.3" +grin_api = "5.3.3" + +# For beta release + +# grin_core = { git = "https://github.com/mimblewimble/grin", tag = "v5.2.0-beta.3"} +# grin_keychain = { git = "https://github.com/mimblewimble/grin", tag = "v5.2.0-beta.3" } +# grin_util = { git = "https://github.com/mimblewimble/grin", tag = "v5.2.0-beta.3" } +# grin_api = { git = "https://github.com/mimblewimble/grin", tag = "v5.2.0-beta.3" } + +# For bleeding edge +# grin_core = { git = "https://github.com/mimblewimble/grin", branch = "master" } +# grin_keychain = { git = "https://github.com/mimblewimble/grin", branch = "master" } +# grin_util = { git = "https://github.com/mimblewimble/grin", branch = "master" } +# grin_api = { git = "https://github.com/mimblewimble/grin", branch = "master" } + +# For local testing +# grin_core = { path = "../grin/core"} +# grin_keychain = { path = "../grin/keychain"} +# grin_util = { path = "../grin/util"} +# grin_api = { path = "../grin/api"} + +###### + +[build-dependencies] +built = { version = "0.7", features = ["git2"]} + +[dev-dependencies] +url = "2.1" +serde = "1" +serde_derive = "1" +serde_json = "1" +remove_dir_all = "0.7" +easy-jsonrpc-mw = "0.5.4" \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f433b1a --- /dev/null +++ b/LICENSE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/README.md b/README.md new file mode 100644 index 0000000..0554c4a --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +[![Continuous Integration](https://github.com/mimblewimble/grin-wallet/actions/workflows/ci.yaml/badge.svg)](https://github.com/mimblewimble/grin-wallet/actions/workflows/ci.yaml) +[![Coverage Status](https://img.shields.io/codecov/c/github/mimblewimble/grin-wallet/master.svg)](https://codecov.io/gh/mimblewimble/grin-wallet) +[![Chat](https://img.shields.io/gitter/room/grin_community/Lobby.svg)](https://gitter.im/grin_community/Lobby) +[![Support](https://img.shields.io/badge/support-on%20gitter-brightgreen.svg)](https://gitter.im/grin_community/support) +[![](https://img.shields.io/badge/dynamic/json.svg?label=docs&uri=https%3A%2F%2Fcrates.io%2Fapi%2Fv1%2Fcrates%2Fgrin-wallet%2Fversions&query=%24.versions%5B0%5D.num&colorB=4F74A6)](https://docs.rs/releases/search?query=grin_wallet) +[![Release Version](https://img.shields.io/github/release/mimblewimble/grin-wallet.svg)](https://github.com/mimblewimble/grin-wallet/releases) +[![License](https://img.shields.io/github/license/mimblewimble/grin-wallet.svg)](https://github.com/mimblewimble/grin-wallet/blob/master/LICENSE) + +# Grin Wallet + +This is the reference implementation of [Grin's](https://github.com/mimblewimble/grin) wallet. It consists of 2 major pieces: + +* The Grin Wallet APIs, which are intended for use by Grin community wallet developers. The wallet APIs can be directly linked into other projects or invoked via a JSON-RPC interface. + +* A reference command-line wallet, which provides a baseline wallet for Grin and demonstrates how the wallet APIs should be called. + +# Usage + +To use the command-line wallet, we recommend using the latest release from the [Releases page](https://github.com/mimblewimble/grin-wallet/releases). There are distributions for Linux, MacOS and Windows. + +Full documentation outlining how to use the command line wallet can be found on [Grin's Wiki](https://github.com/mimblewimble/docs/wiki/Wallet-User-Guide) + +# License + +Apache License v2.0 diff --git a/api/Cargo.toml b/api/Cargo.toml new file mode 100644 index 0000000..d2f1f2f --- /dev/null +++ b/api/Cargo.toml @@ -0,0 +1,57 @@ +[package] +name = "grin_wallet_api" +version = "5.4.0-alpha.1" +authors = ["Grin Developers "] +description = "Grin Wallet API" +license = "Apache-2.0" +repository = "https://github.com/mimblewimble/grin-wallet" +keywords = [ "crypto", "grin", "mimblewimble" ] +exclude = ["**/*.grin", "**/*.grin2"] +edition = "2018" + +[dependencies] +log = "0.4" +uuid = { version = "0.8", features = ["serde", "v4"] } +serde = "1" +rand = "0.6" +serde_derive = "1" +serde_json = "1" +easy-jsonrpc-mw = "0.5.4" +chrono = { version = "0.4.11", features = ["serde"] } +ring = "0.16" +base64 = "0.12" +ed25519-dalek = "1.0.0-pre.4" + +grin_wallet_libwallet = { path = "../libwallet", version = "5.4.0-alpha.1" } +grin_wallet_config = { path = "../config", version = "5.4.0-alpha.1" } +grin_wallet_impls = { path = "../impls", version = "5.4.0-alpha.1" } +grin_wallet_util = { path = "../util", version = "5.4.0-alpha.1" } + +##### Grin Imports + +# For Release +grin_core = "5.3.3" +grin_keychain = "5.3.3" +grin_util = "5.3.3" + +# For beta release + +# grin_core = { git = "https://github.com/mimblewimble/grin", tag = "v5.2.0-beta.3"} +# grin_keychain = { git = "https://github.com/mimblewimble/grin", tag = "v5.2.0-beta.3" } +# grin_util = { git = "https://github.com/mimblewimble/grin", tag = "v5.2.0-beta.3" } + +# For bleeding edge +# grin_core = { git = "https://github.com/mimblewimble/grin", branch = "master" } +# grin_keychain = { git = "https://github.com/mimblewimble/grin", branch = "master" } +# grin_util = { git = "https://github.com/mimblewimble/grin", branch = "master" } + +# For local testing +# grin_core = { path = "../../grin/core"} +# grin_keychain = { path = "../../grin/keychain"} +# grin_util = { path = "../../grin/util"} + +##### + +[dev-dependencies] +serde_json = "1" +tempfile = "3.1" diff --git a/api/src/foreign.rs b/api/src/foreign.rs new file mode 100644 index 0000000..77c6575 --- /dev/null +++ b/api/src/foreign.rs @@ -0,0 +1,515 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Foreign API External Definition + +use crate::config::TorConfig; +use crate::keychain::Keychain; +use crate::libwallet::api_impl::foreign; +use crate::libwallet::{ + BlockFees, CbData, Error, NodeClient, NodeVersionInfo, Slate, VersionInfo, WalletInst, + WalletLCProvider, +}; +use crate::try_slatepack_sync_workflow; +use crate::util::secp::key::SecretKey; +use crate::util::Mutex; +use std::sync::Arc; + +/// ForeignAPI Middleware Check callback +pub type ForeignCheckMiddleware = + fn(ForeignCheckMiddlewareFn, Option, Option<&Slate>) -> Result<(), Error>; + +/// Middleware Identifiers for each function +pub enum ForeignCheckMiddlewareFn { + /// check_version + CheckVersion, + /// build_coinbase + BuildCoinbase, + /// verify_slate_messages + VerifySlateMessages, + /// receive_tx + ReceiveTx, + /// finalize_tx + FinalizeTx, +} + +/// Main interface into all wallet API functions. +/// Wallet APIs are split into two seperate blocks of functionality +/// called the ['Owner'](struct.Owner.html) and ['Foreign'](struct.Foreign.html) APIs +/// +/// * The 'Foreign' API contains methods that other wallets will +/// use to interact with the owner's wallet. This API can be exposed +/// to the outside world, with the consideration as to how that can +/// be done securely up to the implementor. +/// +/// Methods in both APIs are intended to be 'single use', that is to say each +/// method will 'open' the wallet (load the keychain with its master seed), perform +/// its operation, then 'close' the wallet (unloading references to the keychain and master +/// seed). + +pub struct Foreign<'a, L, C, K> +where + L: WalletLCProvider<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + /// Wallet instance + pub wallet_inst: Arc>>>, + /// Flag to normalize some output during testing. Can mostly be ignored. + pub doctest_mode: bool, + /// foreign check middleware + middleware: Option, + /// Stored keychain mask (in case the stored wallet seed is tokenized) + keychain_mask: Option, + /// Optional TOR configuration, holding address of sender and + /// data directory + tor_config: Mutex>, +} + +impl<'a, L, C, K> Foreign<'a, L, C, K> +where + L: WalletLCProvider<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + /// Create a new API instance with the given wallet instance. All subsequent + /// API calls will operate on this instance of the wallet. + /// + /// Each method will call the [`WalletBackend`](../grin_wallet_libwallet/types/trait.WalletBackend.html)'s + /// [`open_with_credentials`](../grin_wallet_libwallet/types/trait.WalletBackend.html#tymethod.open_with_credentials) + /// (initialising a keychain with the master seed), perform its operation, then close the keychain + /// with a call to [`close`](../grin_wallet_libwallet/types/trait.WalletBackend.html#tymethod.close) + /// + /// # Arguments + /// * `wallet_in` - A reference-counted mutex containing an implementation of the + /// [`WalletBackend`](../grin_wallet_libwallet/types/trait.WalletBackend.html) trait. + /// * `keychain_mask` - Mask value stored internally to use when calling a wallet + /// whose seed has been XORed with a token value (such as when running the foreign + /// and owner listeners in the same instance) + /// * middleware - Option middleware which containts the NodeVersionInfo and can call + /// a predefined function with the slate to check if the operation should continue + /// + /// # Returns + /// * An instance of the ForeignApi holding a reference to the provided wallet + /// + /// # Example + /// ``` + /// use grin_keychain as keychain; + /// use grin_util as util; + /// use grin_core; + /// use grin_wallet_api as api; + /// use grin_wallet_config as config; + /// use grin_wallet_impls as impls; + /// use grin_wallet_libwallet as libwallet; + /// + /// use keychain::ExtKeychain; + /// use tempfile::tempdir; + /// + /// use std::sync::Arc; + /// use util::{Mutex, ZeroingString}; + /// + /// use grin_core::global; + /// + /// use api::Foreign; + /// use config::WalletConfig; + /// use impls::{DefaultWalletImpl, DefaultLCProvider, HTTPNodeClient}; + /// use libwallet::WalletInst; + /// + /// global::set_local_chain_type(global::ChainTypes::AutomatedTesting); + /// + /// let mut wallet_config = WalletConfig::default(); + /// # let dir = tempdir().map_err(|e| format!("{:#?}", e)).unwrap(); + /// # let dir = dir + /// # .path() + /// # .to_str() + /// # .ok_or("Failed to convert tmpdir path to string.".to_owned()) + /// # .unwrap(); + /// # wallet_config.data_file_dir = dir.to_owned(); + /// + /// // A NodeClient must first be created to handle communication between + /// // the wallet and the node. + /// let node_client = HTTPNodeClient::new(&wallet_config.check_node_api_http_addr, None).unwrap(); + /// + /// // impls::DefaultWalletImpl is provided for convenience in instantiating the wallet + /// // It contains the LMDBBackend, DefaultLCProvider (lifecycle) and ExtKeychain used + /// // by the reference wallet implementation. + /// // These traits can be replaced with alternative implementations if desired + /// + /// let mut wallet = Box::new(DefaultWalletImpl::<'static, HTTPNodeClient>::new(node_client.clone()).unwrap()) + /// as Box, HTTPNodeClient, ExtKeychain>>; + /// + /// // Wallet LifeCycle Provider provides all functions init wallet and work with seeds, etc... + /// let lc = wallet.lc_provider().unwrap(); + /// + /// // The top level wallet directory should be set manually (in the reference implementation, + /// // this is provided in the WalletConfig) + /// let _ = lc.set_top_level_directory(&wallet_config.data_file_dir); + /// + /// // Wallet must be opened with the password (TBD) + /// let pw = ZeroingString::from("wallet_password"); + /// lc.open_wallet(None, pw, false, false); + /// + /// // All wallet functions operate on an Arc::Mutex to allow multithreading where needed + /// let mut wallet = Arc::new(Mutex::new(wallet)); + /// + /// let api_foreign = Foreign::new(wallet.clone(), None, None, false); + /// // .. perform wallet operations + /// + /// ``` + + pub fn new( + wallet_inst: Arc>>>, + keychain_mask: Option, + middleware: Option, + doctest_mode: bool, + ) -> Self { + Foreign { + wallet_inst, + doctest_mode, + middleware, + keychain_mask, + tor_config: Mutex::new(None), + } + } + + /// Set the TOR configuration for this instance of the ForeignAPI, used during + /// `recieve_tx` when a return address is specified + /// + /// # Arguments + /// * `tor_config` - The optional [TorConfig](#) to use + /// # Returns + /// * Nothing + + pub fn set_tor_config(&self, tor_config: Option) { + let mut lock = self.tor_config.lock(); + *lock = tor_config; + } + + /// Return the version capabilities of the running ForeignApi Node + /// # Arguments + /// None + /// # Returns + /// * [`VersionInfo`](../grin_wallet_libwallet/api_impl/types/struct.VersionInfo.html) + /// # Example + /// Set up as in [`new`](struct.Foreign.html#method.new) method above. + /// ``` + /// # grin_wallet_api::doctest_helper_setup_doc_env_foreign!(wallet, wallet_config); + /// + /// let mut api_foreign = Foreign::new(wallet.clone(), None, None, false); + /// + /// let version_info = api_foreign.check_version(); + /// // check and proceed accordingly + /// ``` + + pub fn check_version(&self) -> Result { + if let Some(m) = self.middleware.as_ref() { + let mut w_lock = self.wallet_inst.lock(); + let w = w_lock.lc_provider()?.wallet_inst()?; + m( + ForeignCheckMiddlewareFn::CheckVersion, + w.w2n_client().get_version_info(), + None, + )?; + } + Ok(foreign::check_version()) + } + + /// Builds a new unconfirmed coinbase output in the wallet, generally for inclusion in a + /// potential new block's coinbase output during mining. + /// + /// All potential coinbase outputs are created as 'Unconfirmed' with the coinbase flag set. + /// If a potential coinbase output is found on the chain after a wallet update, it status + /// is set to `Unsent` and a [Transaction Log Entry](../grin_wallet_libwallet/types/struct.TxLogEntry.html) + /// will be created. Note the output will be unspendable until the coinbase maturity period + /// has expired. + /// + /// # Arguments + /// + /// * `block_fees` - A [`BlockFees`](../grin_wallet_libwallet/api_impl/types/struct.BlockFees.html) + /// struct, set up as follows: + /// + /// `fees` - should contain the sum of all transaction fees included in the potential + /// block + /// + /// `height` - should contain the block height being mined + /// + /// `key_id` - can optionally contain the corresponding keychain ID in the wallet to use + /// to create the output's blinding factor. If this is not provided, the next available key + /// id will be assigned + /// + /// # Returns + /// * `Ok`([`cb_data`](../grin_wallet_libwallet/api_impl/types/struct.CbData.html)`)` if successful. This + /// will contain the corresponding output, kernel and keyID used to create the coinbase output. + /// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered. + /// + /// # Example + /// Set up as in [`new`](struct.Foreign.html#method.new) method above. + /// ``` + /// # grin_wallet_api::doctest_helper_setup_doc_env_foreign!(wallet, wallet_config); + /// + /// let mut api_foreign = Foreign::new(wallet.clone(), None, None, false); + /// + /// let block_fees = BlockFees { + /// fees: 800000, + /// height: 234323, + /// key_id: None, + /// }; + /// // Build a new coinbase output + /// + /// let res = api_foreign.build_coinbase(&block_fees); + /// + /// if let Ok(cb_data) = res { + /// // cb_data is populated with coinbase output info + /// // ... + /// } + /// ``` + + pub fn build_coinbase(&self, block_fees: &BlockFees) -> Result { + let mut w_lock = self.wallet_inst.lock(); + let w = w_lock.lc_provider()?.wallet_inst()?; + if let Some(m) = self.middleware.as_ref() { + m( + ForeignCheckMiddlewareFn::BuildCoinbase, + w.w2n_client().get_version_info(), + None, + )?; + } + foreign::build_coinbase( + &mut **w, + (&self.keychain_mask).as_ref(), + block_fees, + self.doctest_mode, + ) + } + + /// Recieve a tranaction created by another party, returning the modified + /// [`Slate`](../grin_wallet_libwallet/slate/struct.Slate.html) object, modified with + /// the recipient's output for the transaction amount, and public signature data. This slate can + /// then be sent back to the sender to finalize the transaction via the + /// [Owner API's `finalize_tx`](struct.Owner.html#method.finalize_tx) method. + /// + /// This function creates a single output for the full amount, set to a status of + /// 'Awaiting finalization'. It will remain in this state until the wallet finds the + /// corresponding output on the chain, at which point it will become 'Unspent'. The slate + /// will be updated with the results of Signing round 1 and 2, adding the recipient's public + /// nonce, public excess value, and partial signature to the slate. + /// + /// Also creates a corresponding [Transaction Log Entry](../grin_wallet_libwallet/types/struct.TxLogEntry.html) + /// in the wallet's transaction log. + /// + /// # Arguments + /// * `slate` - The transaction [`Slate`](../grin_wallet_libwallet/slate/struct.Slate.html). + /// The slate should contain the results of the sender's round 1 (e.g, public nonce and public + /// excess value). + /// * `dest_acct_name` - The name of the account into which the slate should be received. If + /// `None`, the default account is used. + /// * `r_addr` - If included, attempt to send the slate back to the sender using the slatepack sync + /// send (TOR). If providing this argument, check the `state` field of the slate to see if the + /// sync_send was successful (it should be S3 if the synced send sent successfully). + /// + /// # Returns + /// * a result containing: + /// * `Ok`([`slate`](../grin_wallet_libwallet/slate/struct.Slate.html)`)` if successful, + /// containing the new slate updated with the recipient's output and public signing information. + /// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered. + /// + /// # Remarks + /// + /// * This method will store a partially completed transaction in the wallet's transaction log. + /// + /// # Example + /// Set up as in [new](struct.Foreign.html#method.new) method above. + /// ``` + /// # grin_wallet_api::doctest_helper_setup_doc_env_foreign!(wallet, wallet_config); + /// + /// let mut api_foreign = Foreign::new(wallet.clone(), None, None, false); + /// # let slate = Slate::blank(2, false); + /// + /// // . . . + /// // Obtain a sent slate somehow + /// let result = api_foreign.receive_tx(&slate, None, None); + /// + /// if let Ok(slate) = result { + /// // Send back to recipient somehow + /// // ... + /// } + /// ``` + + pub fn receive_tx( + &self, + slate: &Slate, + dest_acct_name: Option<&str>, + r_addr: Option, + ) -> Result { + let mut w_lock = self.wallet_inst.lock(); + let w = w_lock.lc_provider()?.wallet_inst()?; + if let Some(m) = self.middleware.as_ref() { + m( + ForeignCheckMiddlewareFn::ReceiveTx, + w.w2n_client().get_version_info(), + Some(slate), + )?; + } + let ret_slate = foreign::receive_tx( + &mut **w, + (&self.keychain_mask).as_ref(), + slate, + dest_acct_name, + self.doctest_mode, + )?; + match r_addr { + Some(a) => { + let tor_config_lock = self.tor_config.lock(); + let res = try_slatepack_sync_workflow( + &ret_slate, + &a, + tor_config_lock.clone(), + None, + true, + self.doctest_mode, + ); + match res { + Ok(s) => return Ok(s.unwrap()), + Err(_) => return Ok(ret_slate), + } + } + None => Ok(ret_slate), + } + } + + /// Finalizes a (standard or invoice) transaction initiated by this wallet's Owner api. + /// This step assumes the paying party has completed round 1 and 2 of slate + /// creation, and added their partial signatures. This wallet will verify + /// and add their partial sig, then create the finalized transaction, + /// ready to post to a node. + /// + /// This function posts to the node if the `post_automatically` + /// argument is sent to true. Posting can be done in separately via the + /// [`post_tx`](struct.Owner.html#method.post_tx) function. + /// + /// This function also stores the final transaction in the user's wallet files for retrieval + /// via the [`get_stored_tx`](struct.Owner.html#method.get_stored_tx) function. + /// + /// # Arguments + /// * `slate` - The transaction [`Slate`](../grin_wallet_libwallet/slate/struct.Slate.html). The + /// * `post_automatically` - If true, post the finalized transaction to the configured listening + /// node + /// + /// # Returns + /// * Ok([`slate`](../grin_wallet_libwallet/slate/struct.Slate.html)) if successful, + /// containing the new finalized slate. + /// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered. + /// + /// # Example + /// Set up as in [`new`](struct.Owner.html#method.new) method above. + /// ``` + /// # grin_wallet_api::doctest_helper_setup_doc_env_foreign!(wallet, wallet_config); + /// + /// let mut api_owner = Owner::new(wallet.clone(), None); + /// let mut api_foreign = Foreign::new(wallet.clone(), None, None, false); + /// + /// // . . . + /// // Issue the invoice tx via the owner API + /// let args = IssueInvoiceTxArgs { + /// amount: 10_000_000_000, + /// ..Default::default() + /// }; + /// let result = api_owner.issue_invoice_tx(None, args); + /// + /// // If result okay, send to payer, who will apply the transaction via their + /// // owner API, then send back the slate + /// // ... + /// # let slate = Slate::blank(2, true); + /// + /// let slate = api_foreign.finalize_tx(&slate, true); + /// // if okay, then post via the owner API + /// ``` + + pub fn finalize_tx(&self, slate: &Slate, post_automatically: bool) -> Result { + let mut w_lock = self.wallet_inst.lock(); + let w = w_lock.lc_provider()?.wallet_inst()?; + let post_automatically = match self.doctest_mode { + true => false, + false => post_automatically, + }; + foreign::finalize_tx( + &mut **w, + (&self.keychain_mask).as_ref(), + slate, + post_automatically, + ) + } +} + +#[doc(hidden)] +#[macro_export] +macro_rules! doctest_helper_setup_doc_env_foreign { + ($wallet:ident, $wallet_config:ident) => { + use grin_core; + use grin_keychain as keychain; + use grin_util as util; + use grin_wallet_api as api; + use grin_wallet_config as config; + use grin_wallet_impls as impls; + use grin_wallet_libwallet as libwallet; + + use grin_core::global; + use keychain::ExtKeychain; + use tempfile::tempdir; + + use std::sync::Arc; + use util::{Mutex, ZeroingString}; + + use api::{Foreign, Owner}; + use config::WalletConfig; + use impls::{DefaultLCProvider, DefaultWalletImpl, HTTPNodeClient}; + use libwallet::{BlockFees, IssueInvoiceTxArgs, Slate, WalletInst}; + + // don't run on windows CI, which gives very inconsistent results + if cfg!(windows) { + return; + } + + // Set our local chain_type for testing. + global::set_local_chain_type(global::ChainTypes::AutomatedTesting); + + let dir = tempdir().map_err(|e| format!("{:#?}", e)).unwrap(); + let dir = dir + .path() + .to_str() + .ok_or("Failed to convert tmpdir path to string.".to_owned()) + .unwrap(); + let mut wallet_config = WalletConfig::default(); + wallet_config.data_file_dir = dir.to_owned(); + let pw = ZeroingString::from(""); + + let node_client = + HTTPNodeClient::new(&wallet_config.check_node_api_http_addr, None).unwrap(); + let mut wallet = Box::new( + DefaultWalletImpl::<'static, HTTPNodeClient>::new(node_client.clone()).unwrap(), + ) + as Box< + WalletInst< + 'static, + DefaultLCProvider, + HTTPNodeClient, + ExtKeychain, + >, + >; + let lc = wallet.lc_provider().unwrap(); + let _ = lc.set_top_level_directory(&wallet_config.data_file_dir); + lc.open_wallet(None, pw, false, false); + let mut $wallet = Arc::new(Mutex::new(wallet)); + }; +} diff --git a/api/src/foreign_rpc.rs b/api/src/foreign_rpc.rs new file mode 100644 index 0000000..1d2b526 --- /dev/null +++ b/api/src/foreign_rpc.rs @@ -0,0 +1,569 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! JSON-RPC Stub generation for the Foreign API + +use crate::keychain::Keychain; +use crate::libwallet::{ + self, BlockFees, CbData, Error, InitTxArgs, IssueInvoiceTxArgs, NodeClient, NodeVersionInfo, + Slate, SlateVersion, VersionInfo, VersionedCoinbase, VersionedSlate, WalletLCProvider, +}; +use crate::{Foreign, ForeignCheckMiddlewareFn}; +use easy_jsonrpc_mw; + +/// Public definition used to generate Foreign jsonrpc api. +/// * When running `grin-wallet listen` with defaults, the V2 api is available at +/// `localhost:3415/v2/foreign` +/// * The endpoint only supports POST operations, with the json-rpc request as the body +#[easy_jsonrpc_mw::rpc] +pub trait ForeignRpc { + /** + Networked version of [Foreign::check_version](struct.Foreign.html#method.check_version). + + # Json rpc example + + ``` + # grin_wallet_api::doctest_helper_json_rpc_foreign_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "check_version", + "id": 1, + "params": [] + } + # "# + # , + # r#" + { + "id": 1, + "jsonrpc": "2.0", + "result": { + "Ok": { + "foreign_api_version": 2, + "supported_slate_versions": [ + "V4" + ] + } + } + } + # "# + # ,false, 0, false, false); + ``` + */ + fn check_version(&self) -> Result; + + /** + Networked Legacy (non-secure token) version of [Foreign::build_coinbase](struct.Foreign.html#method.build_coinbase). + + # Json rpc example + + ``` + # grin_wallet_api::doctest_helper_json_rpc_foreign_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "build_coinbase", + "id": 1, + "params": [ + { + "fees": 0, + "height": 0, + "key_id": null + } + ] + } + # "# + # , + # r#" + { + "id": 1, + "jsonrpc": "2.0", + "result": { + "Ok": { + "kernel": { + "excess": "08dfe86d732f2dd24bac36aa7502685221369514197c26d33fac03041d47e4b490", + "excess_sig": "8f07ddd5e9f5179cff19486034181ed76505baaad53e5d994064127b56c5841be02fa098c54c9bf638e0ee1ad5eb896caa11565f632be7b9cd65643ba371044f", + "features": "Coinbase" + }, + "key_id": "0300000000000000000000000400000000", + "output": { + "commit": "08fe198e525a5937d0c5d01fa354394d2679be6df5d42064a0f7550c332fce3d9d", + "features": "Coinbase", + "proof": "9d8488fcb43c9c0f683b9ce62f3c8e047b71f2b4cd94b99a3c9a36aef3bb8361ee17b4489eb5f6d6507250532911acb76f18664604c2ca4215347a5d5d8e417d00ca2d59ec29371286986428b0ec1177fc2e416339ea8542eff8186550ad0d65ffac35d761c38819601d331fd427576e2fff823bbc3faa04f49f5332bd4de46cd4f83d0fd46cdb1dfb87069e95974e4a45e0235db71f5efe5cec83bbb30e152ac50a010ef4e57e33aabbeb894b9114f90bb5c3bb03b009014e358aa3914b1a208eb9d8806fbb679c256d4c1a47b0fce3f1235d58192cb7f615bd7c5dab48486db8962c2a594e69ff70029784a810b4eb76b0516805f3417308cda8acb38b9a3ea061568f0c97f5b46a3beff556dc7ebb58c774f08be472b4b6f603e5f8309c2d1f8d6f52667cb86816b330eca5374148aa898f5bbaf3f23a3ebcdc359ee1e14d73a65596c0ddf51f123234969ac8b557ba9dc53255dd6f5c0d3dd2c035a6d1a1185102612fdca474d018b9f9e81acfa3965d42769f5a303bbaabb78d17e0c026b8be0039c55ad1378c8316101b5206359f89fd1ee239115dde458749a040997be43c039055594cab76f602a0a1ee4f5322f3ab1157342404239adbf8b6786544cd67d9891c2689530e65f2a4b8e52d8551b92ffefb812ffa4a472a10701884151d1fb77d8cdc0b1868cb31b564e98e4c035e0eaa26203b882552c7b69deb0d8ec67cf28d5ec044554f8a91a6cae87eb377d6d906bba6ec94dda24ebfd372727f68334af798b11256d88e17cef7c4fed092128215f992e712ed128db2a9da2f5e8fadea9395bddd294a524dce47f818794c56b03e1253bf0fb9cb8beebc5742e4acf19c24824aa1d41996e839906e24be120a0bdf6800da599ec9ec3d1c4c11571c9f143eadbb554fa3c8c9777994a3f3421d454e4ec54c11b97eea3e4e6ede2d97a2bc" + } + } + } + } + # "# + # ,false, 4, false, false); + ``` + */ + + fn build_coinbase(&self, block_fees: &BlockFees) -> Result; + + /** + ;Networked version of [Foreign::receive_tx](struct.Foreign.html#method.receive_tx). + + # Json rpc example + + ``` + # grin_wallet_api::doctest_helper_json_rpc_foreign_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "receive_tx", + "id": 1, + "params": [ + { + "amt": "6000000000", + "fee": "23500000", + "id": "0436430c-2b02-624c-2032-570501212b00", + "off": "d202964900000000d302964900000000d402964900000000d502964900000000", + "proof": { + "raddr": "32cdd63928854f8b2628b1dce4626ddcdf35d56cb7cfdf7d64cca5822b78d4d3", + "saddr": "32cdd63928854f8b2628b1dce4626ddcdf35d56cb7cfdf7d64cca5822b78d4d3" + }, + "sigs": [ + { + "nonce": "02b57c1f4fea69a3ee070309cf8f06082022fe06f25a9be1851b56ef0fa18f25d6", + "xs": "023878ce845727f3a4ec76ca3f3db4b38a2d05d636b8c3632108b857fed63c96de" + } + ], + "sta": "S1", + "ver": "4:2" + }, + null, + null + ] + } + # "# + # , + # r#" + { + "id": 1, + "jsonrpc": "2.0", + "result": { + "Ok": { + "coms": [ + { + "c": "091582c92b99943b57955e52b5ccf1223780c2a2e55995c00c86fca2bcb46b6b9f", + "p": "49972a8d5b7c088e7813c3988ebe0982f8f0b12b849b1788df7da07b549408b0d6c99f80c0e2335370c104225ef5d282d79966e9044c959bedc3be03af6246fa07fc13eb3c60c90213c9f3a7a5ecf9a34c8fbaddc1a72e49e12dba9495e5aaa53bb6ac6ed63d8774707c57ab604d6bdc46de18da57a731fe336c3ccef92b4dae967417ffdae2c7d75864d46d30e287dd9cc15882e15f296b9bab0040e4432f4024be33924f112dd26c90cc800ac09a327b0ac3a661f63da9945fb1bcc82a7777d61d97cbe657675e22d035d2cf9ea03a89cfa410960ebc18a0a18b1909f4c5bef20b0fd13ffcf5a818ad8768d354b1c0f2e9b16dd7a9cf0641546f57d1945a98b8684d067dd085b90b40457e4c14665fb1b94feecf30a90f508ded16ba1bba8080a6866dffd0b1f01738fff8c62ce5e38e677835752a1b4072124dd9ff14ba8ff92126baebbb5f6e14fbb052f5d5b09aec11bfd880d7d4640a295aa83f184034d26f00cbdbabf9b89fddd7a7c9cc8c5d4b53fc39971e4495a8d984ac9607be89780fde528ee3f2d6b912908b4caf04f5c93f64431517af6b32d0b9c18255959f6903c6696ec71f615a0c877630a2d871f3f8a107fc80f306a94b6ad5790070f7d2535163bad7feae9263a9d3558ea1acecc4e61ff4e05b0162f6aba1a3b299ff1c3bb85e4109e550ad870c328bedc45fed8b504f679bc3c1a25b2b65ede44602f21fac123ba7c5f132e7c786bf9420a27bae4d2559cf7779e77f96b747b6d3ad5c13b5e8c9b49a7083001b2f98bcf242d4644537bb5a3b5b41764812a93395b7ab372c18be575e02c3763b4170234e5fddeb43420aadb71cb80f75cc681c1e7ffee3e6a8868c6076fd1da539ab9a12fef1c8cbe271b6de60100c9f82d826dc97b47b57ee9804e60112f556c1dce4f12ecc91ef34d69090b8c9d2ae9cbae38994a955cb" + } + ], + "id": "0436430c-2b02-624c-2032-570501212b00", + "off": "a4f88ac429dee1d453ae33ed9f944417a52c7310477936e484fd83f0f22db483", + "proof": { + "raddr": "32cdd63928854f8b2628b1dce4626ddcdf35d56cb7cfdf7d64cca5822b78d4d3", + "rsig": "02357a13b304ba8e22f4896d5664b72ad6d1b824e88782e2b716686ea14ec47281ef5ee14c03ead84c3260f5b0c1529ad3ddae57f28f6b8b1b66532bfcb2ee0f", + "saddr": "32cdd63928854f8b2628b1dce4626ddcdf35d56cb7cfdf7d64cca5822b78d4d3" + }, + "sigs": [ + { + "nonce": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", + "part": "8f07ddd5e9f5179cff19486034181ed76505baaad53e5d994064127b56c5841be4f81215c8e678c7bd5f04f3562388948864d7a5a0374e220ab6dc5e02bae66f", + "xs": "02e3c128e436510500616fef3f9a22b15ca015f407c8c5cf96c9059163c873828f" + } + ], + "sta": "S2", + "ver": "4:2" + } + } + } + # "# + # ,false, 5, true, false); + ``` + */ + fn receive_tx( + &self, + slate: VersionedSlate, + dest_acct_name: Option, + dest: Option, + ) -> Result; + + /** + + Networked version of [Foreign::finalize_tx](struct.Foreign.html#method.finalize_tx). + + # Json rpc example + + ``` + # grin_wallet_api::doctest_helper_json_rpc_foreign_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "finalize_tx", + "id": 1, + "params": [{ + "ver": "4:2", + "id": "0436430c-2b02-624c-2032-570501212b00", + "sta": "I2", + "off": "383bc9df0dd332629520a0a72f8dd7f0e97d579dccb4dbdc8592aa3d424c846c", + "fee": "23500000", + "sigs": [ + { + "xs": "02e3c128e436510500616fef3f9a22b15ca015f407c8c5cf96c9059163c873828f", + "nonce": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", + "part": "8f07ddd5e9f5179cff19486034181ed76505baaad53e5d994064127b56c5841be7bf31d80494f5e4a3d656649b1610c61a268f9cafcfc604b5d9f25efb2aa3c5" + } + ], + "coms": [ + { + "f": 1, + "c": "087df32304c5d4ae8b2af0bc31e700019d722910ef87dd4eec3197b80b207e3045" + }, + { + "f": 1, + "c": "08e1da9e6dc4d6e808a718b2f110a991dd775d65ce5ae408a4e1f002a4961aa9e7" + }, + { + "c": "09ede20409d5ae0d1c0d3f3d2c68038a384cdd6b7cc5ca2aab670f570adc2dffc3", + "p": "6d86fe00220f8c6ac2ad4e338d80063dba5423af525bd273ecfac8ef6b509192732a8cd0c53d3313e663ac5ccece3d589fd2634e29f96e82b99ca6f8b953645a005d1bc73493f8c41f84fb8e327d4cbe6711dba194a60db30700df94a41e1fda7afe0619169389f8d8ee12bddf736c4bc86cd5b1809a5a27f195209147dc38d0de6f6710ce9350f3b8e7e6820bfe5182e6e58f0b41b82b6ec6bb01ffe1d8b3c2368ebf1e31dfdb9e00f0bc68d9119a38d19c038c29c7b37e31246e7bba56019bc88881d7d695d32557fc0e93635b5f24deffefc787787144e5de7e86281e79934e7e20d9408c34317c778e6b218ee26d0a5e56b8b84a883e3ddf8603826010234531281486454f8c2cf3fee074f242f9fc1da3c6636b86fb6f941eb8b633d6e3b3f87dfe5ae261a40190bd4636f433bcdd5e3400255594e282c5396db8999d95be08a35be9a8f70fdb7cf5353b90584523daee6e27e208b2ca0e5758b8a24b974dca00bab162505a2aa4bcefd8320f111240b62f861261f0ce9b35979f9f92da7dd6989fe1f41ec46049fd514d9142ce23755f52ec7e64df2af33579e9b8356171b91bc96b875511bef6062dd59ef3fe2ddcc152147554405b12c7c5231513405eb062aa8fa093e3414a144c544d551c4f1f9bf5d5d2ff5b50a3f296c800907704bed8d8ee948c0855eff65ad44413af641cdc68a06a7c855be7ed7dd64d5f623bbc9645763d48774ba2258240a83f8f89ef84d21c65bcb75895ebca08b0090b40aafb7ddef039fcaf4bad2dbbac72336c4412c600e854d368ed775597c15d2e66775ab47024ce7e62fd31bf90b183149990c10b5b678501dbac1af8b2897b67d085d87cab7af4036cba3bdcfdcc7548d7710511045813c6818d859e192e03adc0d6a6b30c4cbac20a0d6f8719c7a9c3ad46d62eec464c4c44b58fca463fea3ce1fc51" + } + ] + }] + } + # "# + # , + # r#" + { + "id": 1, + "jsonrpc": "2.0", + "result": { + "Ok": { + "coms": [ + { + "c": "087df32304c5d4ae8b2af0bc31e700019d722910ef87dd4eec3197b80b207e3045", + "f": 1 + }, + { + "c": "08e1da9e6dc4d6e808a718b2f110a991dd775d65ce5ae408a4e1f002a4961aa9e7", + "f": 1 + }, + { + "c": "099b48cfb1f80a2347dc89818449e68e76a3c6817a532a8e9ef2b4a5ccf4363850", + "p": "29701ceae262cac77b79b868c883a292e61e6de8192b868edcd1300b0973d91396b156ace6bd673402a303de10ddd8a5e6b7f17ba6557a574a672bd04cc273ab04ed8e2ca80bac483345c0ec843f521814ce1301ec9adc38956a12b4d948acce71295a4f52bcdeb8a1c9f2d6b2da5d731262a5e9c0276ef904df9ef8d48001420cd59f75a2f1ae5c7a1c7c6b9f140e7613e52ef9e249f29f9340b7efb80699e460164324616f98fd4cde3db52497c919e95222fffeacb7e65deca7e368a80ce713c19de7da5369726228ee336f5bd494538c12ccbffeb1b9bfd5fc8906d1c64245b516f103fa96d9c56975837652c1e0fa5803d7ccf1147d8f927e36da717f7ad79471dbe192f5f50f87a79fc3fe030dba569b634b92d2cf307993cce545633af263897cd7e6ebf4dcafb176d07358bdc38d03e45a49dfa9c8c6517cd68d167ffbf6c3b4de0e2dd21909cbad4c467b84e5700be473a39ac59c669d7c155c4bcab9b8026eea3431c779cd277e4922d2b9742e1f6678cbe869ec3b5b7ef4132ddb6cdd06cf27dbeb28be72b949fa897610e48e3a0d789fd2eea75abc97b3dc7e00e5c8b3d24e40c6f24112adb72352b89a2bef0599345338e9e76202a3c46efa6370952b2aca41aadbae0ea32531acafcdab6dd066d769ebf50cf4f3c0a59d2d5fa79600a207b9417c623f76ad05e8cccfcd4038f9448bc40f127ca7c0d372e46074e334fe49f5a956ec0056f4da601e6af80eb1a6c4951054869e665b296d8c14f344ca2dc5fdd5df4a3652536365a1615ad9b422165c77bf8fe65a835c8e0c41e070014eb66ef8c525204e990b3a3d663c1e42221b496895c37a2f0c1bf05e91235409c3fe3d89a9a79d6c78609ab18a463311911f71fa37bb73b15fcd38143d1404fd2ce81004dc7ff89cf1115dcc0c35ce1c1bf9941586fb959770f2618ccb7118a7" + }, + { + "c": "09ede20409d5ae0d1c0d3f3d2c68038a384cdd6b7cc5ca2aab670f570adc2dffc3", + "p": "6d86fe00220f8c6ac2ad4e338d80063dba5423af525bd273ecfac8ef6b509192732a8cd0c53d3313e663ac5ccece3d589fd2634e29f96e82b99ca6f8b953645a005d1bc73493f8c41f84fb8e327d4cbe6711dba194a60db30700df94a41e1fda7afe0619169389f8d8ee12bddf736c4bc86cd5b1809a5a27f195209147dc38d0de6f6710ce9350f3b8e7e6820bfe5182e6e58f0b41b82b6ec6bb01ffe1d8b3c2368ebf1e31dfdb9e00f0bc68d9119a38d19c038c29c7b37e31246e7bba56019bc88881d7d695d32557fc0e93635b5f24deffefc787787144e5de7e86281e79934e7e20d9408c34317c778e6b218ee26d0a5e56b8b84a883e3ddf8603826010234531281486454f8c2cf3fee074f242f9fc1da3c6636b86fb6f941eb8b633d6e3b3f87dfe5ae261a40190bd4636f433bcdd5e3400255594e282c5396db8999d95be08a35be9a8f70fdb7cf5353b90584523daee6e27e208b2ca0e5758b8a24b974dca00bab162505a2aa4bcefd8320f111240b62f861261f0ce9b35979f9f92da7dd6989fe1f41ec46049fd514d9142ce23755f52ec7e64df2af33579e9b8356171b91bc96b875511bef6062dd59ef3fe2ddcc152147554405b12c7c5231513405eb062aa8fa093e3414a144c544d551c4f1f9bf5d5d2ff5b50a3f296c800907704bed8d8ee948c0855eff65ad44413af641cdc68a06a7c855be7ed7dd64d5f623bbc9645763d48774ba2258240a83f8f89ef84d21c65bcb75895ebca08b0090b40aafb7ddef039fcaf4bad2dbbac72336c4412c600e854d368ed775597c15d2e66775ab47024ce7e62fd31bf90b183149990c10b5b678501dbac1af8b2897b67d085d87cab7af4036cba3bdcfdcc7548d7710511045813c6818d859e192e03adc0d6a6b30c4cbac20a0d6f8719c7a9c3ad46d62eec464c4c44b58fca463fea3ce1fc51" + } + ], + "fee": "23500000", + "id": "0436430c-2b02-624c-2032-570501212b00", + "off": "a5a632f26f27a9b71e98c1c8b8098bb41204ffcfd206d995f9c16d10764ad95a", + "sigs": [ + { + "nonce": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", + "part": "8f07ddd5e9f5179cff19486034181ed76505baaad53e5d994064127b56c5841be7bf31d80494f5e4a3d656649b1610c61a268f9cafcfc604b5d9f25efb2aa3c5", + "xs": "02e3c128e436510500616fef3f9a22b15ca015f407c8c5cf96c9059163c873828f" + }, + { + "nonce": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", + "part": "8f07ddd5e9f5179cff19486034181ed76505baaad53e5d994064127b56c5841b04e1e15ceb1b5dbab8baf7750d7bd4aad6cfe97b83e4dc080dae328eb75881fd", + "xs": "02e89cce4499ac1e9bb498dab9e3fab93cc40cd3d26c04a0292e00f4bf272499ec" + } + ], + "sta": "I3", + "ver": "4:2" + } + } + } + # "# + # ,false, 5, false, true); + ``` + */ + fn finalize_tx(&self, slate: VersionedSlate) -> Result; +} + +impl<'a, L, C, K> ForeignRpc for Foreign<'a, L, C, K> +where + L: WalletLCProvider<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + fn check_version(&self) -> Result { + Foreign::check_version(self) + } + + fn build_coinbase(&self, block_fees: &BlockFees) -> Result { + let cb: CbData = Foreign::build_coinbase(self, block_fees)?; + Ok(VersionedCoinbase::into_version(cb, SlateVersion::V4)) + } + + fn receive_tx( + &self, + in_slate: VersionedSlate, + dest_acct_name: Option, + dest: Option, + ) -> Result { + let version = in_slate.version(); + let slate_from = Slate::from(in_slate); + let out_slate = Foreign::receive_tx( + self, + &slate_from, + dest_acct_name.as_ref().map(String::as_str), + dest, + )?; + Ok(VersionedSlate::into_version(out_slate, version)?) + } + + fn finalize_tx(&self, in_slate: VersionedSlate) -> Result { + let version = in_slate.version(); + let out_slate = Foreign::finalize_tx(self, &Slate::from(in_slate), true)?; + Ok(VersionedSlate::into_version(out_slate, version)?) + } +} + +fn test_check_middleware( + _name: ForeignCheckMiddlewareFn, + _node_version_info: Option, + _slate: Option<&Slate>, +) -> Result<(), libwallet::Error> { + // TODO: Implement checks + // return Err(Error::GenericError("Test Rejection".into()))? + Ok(()) +} + +/// helper to set up a real environment to run integrated doctests +pub fn run_doctest_foreign( + request: serde_json::Value, + test_dir: &str, + use_token: bool, + blocks_to_mine: u64, + init_tx: bool, + init_invoice_tx: bool, +) -> Result, String> { + use easy_jsonrpc_mw::Handler; + use grin_keychain::ExtKeychain; + use grin_wallet_impls::test_framework::{self, LocalWalletClient, WalletProxy}; + use grin_wallet_impls::{DefaultLCProvider, DefaultWalletImpl}; + use grin_wallet_libwallet::{api_impl, WalletInst}; + + use crate::core::global; + use crate::core::global::ChainTypes; + use grin_util as util; + + use std::sync::Arc; + use util::Mutex; + + use std::fs; + use std::thread; + + util::init_test_logger(); + let _ = fs::remove_dir_all(test_dir); + global::set_local_chain_type(ChainTypes::AutomatedTesting); + + let mut wallet_proxy: WalletProxy< + DefaultLCProvider, + LocalWalletClient, + ExtKeychain, + > = WalletProxy::new(test_dir); + let chain = wallet_proxy.chain.clone(); + + let rec_phrase_1 = util::ZeroingString::from( + "fat twenty mean degree forget shell check candy immense awful \ + flame next during february bulb bike sun wink theory day kiwi embrace peace lunch", + ); + let empty_string = util::ZeroingString::from(""); + let client1 = LocalWalletClient::new("wallet1", wallet_proxy.tx.clone()); + let mut wallet1 = + Box::new(DefaultWalletImpl::::new(client1.clone()).unwrap()) + as Box< + dyn WalletInst< + 'static, + DefaultLCProvider, + LocalWalletClient, + ExtKeychain, + >, + >; + let lc = wallet1.lc_provider().unwrap(); + let _ = lc.set_top_level_directory(&format!("{}/wallet1", test_dir)); + lc.create_wallet(None, Some(rec_phrase_1), 32, empty_string.clone(), false) + .unwrap(); + let mask1 = lc + .open_wallet(None, empty_string.clone(), use_token, true) + .unwrap(); + let wallet1 = Arc::new(Mutex::new(wallet1)); + + if mask1.is_some() { + println!("WALLET 1 MASK: {:?}", mask1.clone().unwrap()); + } + + wallet_proxy.add_wallet( + "wallet1", + client1.get_send_instance(), + wallet1.clone(), + mask1.clone(), + ); + + let rec_phrase_2 = util::ZeroingString::from( + "hour kingdom ripple lunch razor inquiry coyote clay stamp mean \ + sell finish magic kid tiny wage stand panther inside settle feed song hole exile", + ); + let client2 = LocalWalletClient::new("wallet2", wallet_proxy.tx.clone()); + let mut wallet2 = + Box::new(DefaultWalletImpl::::new(client2.clone()).unwrap()) + as Box< + dyn WalletInst< + 'static, + DefaultLCProvider, + LocalWalletClient, + ExtKeychain, + >, + >; + let lc = wallet2.lc_provider().unwrap(); + let _ = lc.set_top_level_directory(&format!("{}/wallet2", test_dir)); + lc.create_wallet(None, Some(rec_phrase_2), 32, empty_string.clone(), false) + .unwrap(); + let mask2 = lc.open_wallet(None, empty_string, use_token, true).unwrap(); + let wallet2 = Arc::new(Mutex::new(wallet2)); + + wallet_proxy.add_wallet( + "wallet2", + client2.get_send_instance(), + wallet2.clone(), + mask2.clone(), + ); + + // Set the wallet proxy listener running + thread::spawn(move || { + if let Err(e) = wallet_proxy.run() { + error!("Wallet Proxy error: {}", e); + } + }); + + // Mine a few blocks to wallet 1 so there's something to send + for _ in 0..blocks_to_mine { + let _ = test_framework::award_blocks_to_wallet( + &chain, + wallet1.clone(), + (&mask1).as_ref(), + 1 as usize, + false, + ); + //update local outputs after each block, so transaction IDs stay consistent + let (wallet_refreshed, _) = api_impl::owner::retrieve_summary_info( + wallet1.clone(), + (&mask1).as_ref(), + &None, + true, + 1, + ) + .unwrap(); + assert!(wallet_refreshed); + } + + if init_invoice_tx { + let amount = 60_000_000_000; + let mut slate = { + let mut w_lock = wallet2.lock(); + let w = w_lock.lc_provider().unwrap().wallet_inst().unwrap(); + let args = IssueInvoiceTxArgs { + amount, + ..Default::default() + }; + api_impl::owner::issue_invoice_tx(&mut **w, (&mask2).as_ref(), args, true).unwrap() + }; + slate = { + let mut w_lock = wallet1.lock(); + let w = w_lock.lc_provider().unwrap().wallet_inst().unwrap(); + let args = InitTxArgs { + src_acct_name: None, + amount: slate.amount, + minimum_confirmations: 2, + max_outputs: 500, + num_change_outputs: 1, + selection_strategy_is_use_all: true, + ..Default::default() + }; + api_impl::owner::process_invoice_tx(&mut **w, (&mask1).as_ref(), &slate, args, true) + .unwrap() + }; + println!("INIT INVOICE SLATE"); + // Spit out slate for input to finalize_tx + println!("{}", serde_json::to_string_pretty(&slate).unwrap()); + } + + if init_tx { + let amount = 60_000_000_000; + let mut w_lock = wallet1.lock(); + let w = w_lock.lc_provider().unwrap().wallet_inst().unwrap(); + let args = InitTxArgs { + src_acct_name: None, + amount, + minimum_confirmations: 2, + max_outputs: 500, + num_change_outputs: 1, + selection_strategy_is_use_all: true, + ..Default::default() + }; + let slate = api_impl::owner::init_send_tx(&mut **w, (&mask1).as_ref(), args, true).unwrap(); + println!("INIT SLATE"); + // Spit out slate for input to finalize_tx + println!("{}", serde_json::to_string_pretty(&slate).unwrap()); + } + + let mut api_foreign = match init_invoice_tx { + false => Foreign::new(wallet1, mask1, Some(test_check_middleware), true), + true => Foreign::new(wallet2, mask2, Some(test_check_middleware), true), + }; + api_foreign.doctest_mode = true; + let foreign_api = &api_foreign as &dyn ForeignRpc; + let res = foreign_api.handle_request(request).as_option(); + let _ = fs::remove_dir_all(test_dir); + Ok(res) +} + +#[doc(hidden)] +#[macro_export] +macro_rules! doctest_helper_json_rpc_foreign_assert_response { + ($request:expr, $expected_response:expr, $use_token:expr, $blocks_to_mine:expr, $init_tx:expr, $init_invoice_tx:expr) => { + // create temporary wallet, run jsonrpc request on owner api of wallet, delete wallet, return + // json response. + // In order to prevent leaking tempdirs, This function should not panic. + use grin_wallet_api::run_doctest_foreign; + use serde_json; + use serde_json::Value; + use tempfile::tempdir; + + let dir = tempdir().map_err(|e| format!("{:#?}", e)).unwrap(); + let dir = dir + .path() + .to_str() + .ok_or("Failed to convert tmpdir path to string.".to_owned()) + .unwrap(); + + let request_val: Value = serde_json::from_str($request).unwrap(); + let expected_response: Value = serde_json::from_str($expected_response).unwrap(); + + let response = run_doctest_foreign( + request_val, + dir, + $use_token, + $blocks_to_mine, + $init_tx, + $init_invoice_tx, + ) + .unwrap() + .unwrap(); + + if response != expected_response { + panic!( + "(left != right) \nleft: {}\nright: {}", + serde_json::to_string_pretty(&response).unwrap(), + serde_json::to_string_pretty(&expected_response).unwrap() + ); + } + }; +} diff --git a/api/src/lib.rs b/api/src/lib.rs new file mode 100644 index 0000000..1b79491 --- /dev/null +++ b/api/src/lib.rs @@ -0,0 +1,59 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Higher level wallet functions which can be used by callers to operate +//! on the wallet, as well as helpers to invoke and instantiate wallets +//! and listeners + +#![deny(non_upper_case_globals)] +#![deny(non_camel_case_types)] +#![deny(non_snake_case)] +#![deny(unused_mut)] +#![warn(missing_docs)] + +use grin_core as core; +use grin_keychain as keychain; +use grin_util as util; +use grin_wallet_config as config; +extern crate grin_wallet_impls as impls; +extern crate grin_wallet_libwallet as libwallet; + +#[macro_use] +extern crate serde_derive; +extern crate serde_json; + +#[macro_use] +extern crate log; + +mod foreign; +mod foreign_rpc; + +mod owner; +mod owner_rpc; + +mod types; + +pub use crate::foreign::{Foreign, ForeignCheckMiddleware, ForeignCheckMiddlewareFn}; +pub use crate::foreign_rpc::ForeignRpc; +pub use crate::owner::{try_slatepack_sync_workflow, Owner}; +pub use crate::owner_rpc::OwnerRpc; + +pub use crate::foreign_rpc::foreign_rpc as foreign_rpc_client; +pub use crate::foreign_rpc::run_doctest_foreign; +pub use crate::owner_rpc::run_doctest_owner; + +pub use types::{ + ECDHPubkey, Ed25519SecretKey, EncryptedRequest, EncryptedResponse, EncryptionErrorResponse, + JsonId, Token, +}; diff --git a/api/src/owner.rs b/api/src/owner.rs new file mode 100644 index 0000000..a749d8b --- /dev/null +++ b/api/src/owner.rs @@ -0,0 +1,2643 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Owner API External Definition + +use chrono::prelude::*; +use ed25519_dalek::SecretKey as DalekSecretKey; +use grin_wallet_libwallet::mwixnet::{MixnetReqCreationParams, SwapReq}; +use grin_wallet_libwallet::RetrieveTxQueryArgs; +use uuid::Uuid; + +use crate::config::{TorConfig, WalletConfig}; +use crate::core::core::OutputFeatures; +use crate::core::global; +use crate::impls::HttpSlateSender; +use crate::impls::SlateSender as _; +use crate::keychain::{Identifier, Keychain}; +use crate::libwallet::api_impl::owner_updater::{start_updater_log_thread, StatusMessage}; +use crate::libwallet::api_impl::{owner, owner_updater}; +use crate::libwallet::{ + AcctPathMapping, BuiltOutput, Error, InitTxArgs, IssueInvoiceTxArgs, NodeClient, + NodeHeightResult, OutputCommitMapping, PaymentProof, Slate, Slatepack, SlatepackAddress, + TxLogEntry, ViewWallet, WalletInfo, WalletInst, WalletLCProvider, +}; +use crate::util::logger::LoggingConfig; +use crate::util::secp::{key::SecretKey, pedersen::Commitment}; +use crate::util::{from_hex, static_secp_instance, Mutex, ZeroingString}; +use grin_wallet_util::OnionV3Address; +use std::convert::TryFrom; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::mpsc::{channel, Sender}; +use std::sync::Arc; +use std::thread; +use std::time::Duration; + +/// Main interface into all wallet API functions. +/// Wallet APIs are split into two seperate blocks of functionality +/// called the ['Owner'](struct.Owner.html) and ['Foreign'](struct.Foreign.html) APIs +/// +/// * The 'Owner' API is intended to expose methods that are to be +/// used by the wallet owner only. It is vital that this API is not +/// exposed to anyone other than the owner of the wallet (i.e. the +/// person with access to the seed and password. +/// +/// Methods in both APIs are intended to be 'single use', that is to say each +/// method will 'open' the wallet (load the keychain with its master seed), perform +/// its operation, then 'close' the wallet (unloading references to the keychain and master +/// seed). + +pub struct Owner +where + L: WalletLCProvider<'static, C, K> + 'static, + C: NodeClient + 'static, + K: Keychain + 'static, +{ + /// contain all methods to manage the wallet + pub wallet_inst: Arc>>>, + /// Flag to normalize some output during testing. Can mostly be ignored. + pub doctest_mode: bool, + /// retail TLD during doctest + pub doctest_retain_tld: bool, + /// Share ECDH key + pub shared_key: Arc>>, + /// Update thread + updater: Arc>>, + /// Stop state for update thread + pub updater_running: Arc, + /// Sender for update messages + status_tx: Mutex>>, + /// Holds all update and status messages returned by the + /// updater process + updater_messages: Arc>>, + /// Optional TOR configuration, holding address of sender and + /// data directory + tor_config: Mutex>, +} + +impl Owner +where + L: WalletLCProvider<'static, C, K> + 'static, + C: NodeClient, + K: Keychain, +{ + /// Create a new API instance with the given wallet instance. All subsequent + /// API calls will operate on this instance of the wallet. + /// + /// Each method will call the [`WalletBackend`](../grin_wallet_libwallet/types/trait.WalletBackend.html)'s + /// [`open_with_credentials`](../grin_wallet_libwallet/types/trait.WalletBackend.html#tymethod.open_with_credentials) + /// (initialising a keychain with the master seed,) perform its operation, then close the keychain + /// with a call to [`close`](../grin_wallet_libwallet/types/trait.WalletBackend.html#tymethod.close) + /// + /// # Arguments + /// * `wallet_in` - A reference-counted mutex containing an implementation of the + /// * `custom_channel` - A custom MPSC Tx/Rx pair to capture status + /// updates + /// [`WalletBackend`](../grin_wallet_libwallet/types/trait.WalletBackend.html) trait. + /// + /// # Returns + /// * An instance of the OwnerApi holding a reference to the provided wallet + /// + /// # Example + /// ``` + /// use grin_keychain as keychain; + /// use grin_util as util; + /// use grin_core; + /// use grin_wallet_api as api; + /// use grin_wallet_config as config; + /// use grin_wallet_impls as impls; + /// use grin_wallet_libwallet as libwallet; + /// + /// use grin_core::global; + /// use keychain::ExtKeychain; + /// use tempfile::tempdir; + /// + /// use std::sync::Arc; + /// use util::{Mutex, ZeroingString}; + /// + /// use api::Owner; + /// use config::WalletConfig; + /// use impls::{DefaultWalletImpl, DefaultLCProvider, HTTPNodeClient}; + /// use libwallet::WalletInst; + /// + /// global::set_local_chain_type(global::ChainTypes::AutomatedTesting); + /// + /// let mut wallet_config = WalletConfig::default(); + /// # let dir = tempdir().map_err(|e| format!("{:#?}", e)).unwrap(); + /// # let dir = dir + /// # .path() + /// # .to_str() + /// # .ok_or("Failed to convert tmpdir path to string.".to_owned()) + /// # .unwrap(); + /// # wallet_config.data_file_dir = dir.to_owned(); + /// + /// // A NodeClient must first be created to handle communication between + /// // the wallet and the node. + /// let node_client = HTTPNodeClient::new(&wallet_config.check_node_api_http_addr, None).unwrap(); + /// + /// // impls::DefaultWalletImpl is provided for convenience in instantiating the wallet + /// // It contains the LMDBBackend, DefaultLCProvider (lifecycle) and ExtKeychain used + /// // by the reference wallet implementation. + /// // These traits can be replaced with alternative implementations if desired + /// + /// let mut wallet = Box::new(DefaultWalletImpl::<'static, HTTPNodeClient>::new(node_client.clone()).unwrap()) + /// as Box, HTTPNodeClient, ExtKeychain>>; + /// + /// // Wallet LifeCycle Provider provides all functions init wallet and work with seeds, etc... + /// let lc = wallet.lc_provider().unwrap(); + /// + /// // The top level wallet directory should be set manually (in the reference implementation, + /// // this is provided in the WalletConfig) + /// let _ = lc.set_top_level_directory(&wallet_config.data_file_dir); + /// + /// // Wallet must be opened with the password (TBD) + /// let pw = ZeroingString::from("wallet_password"); + /// lc.open_wallet(None, pw, false, false); + /// + /// // All wallet functions operate on an Arc::Mutex to allow multithreading where needed + /// let mut wallet = Arc::new(Mutex::new(wallet)); + /// + /// let api_owner = Owner::new(wallet.clone(), None); + /// // .. perform wallet operations + /// + /// ``` + + pub fn new( + wallet_inst: Arc>>>, + custom_channel: Option>, + ) -> Self { + let updater_running = Arc::new(AtomicBool::new(false)); + let updater = Arc::new(Mutex::new(owner_updater::Updater::new( + wallet_inst.clone(), + updater_running.clone(), + ))); + let updater_messages = Arc::new(Mutex::new(vec![])); + + let tx = match custom_channel { + Some(c) => c, + None => { + let (tx, rx) = channel(); + let _ = start_updater_log_thread(rx, updater_messages.clone()); + tx + } + }; + + Owner { + wallet_inst, + doctest_mode: false, + doctest_retain_tld: false, + shared_key: Arc::new(Mutex::new(None)), + updater, + updater_running, + status_tx: Mutex::new(Some(tx)), + updater_messages, + tor_config: Mutex::new(None), + } + } + + /// Set the TOR configuration for this instance of the OwnerAPI, used during + /// `init_send_tx` when send args are present and a TOR address is specified + /// + /// # Arguments + /// * `tor_config` - The optional [TorConfig](#) to use + /// # Returns + /// * Nothing + + pub fn set_tor_config(&self, tor_config: Option) { + let mut lock = self.tor_config.lock(); + *lock = tor_config; + } + + /// Returns a list of accounts stored in the wallet (i.e. mappings between + /// user-specified labels and BIP32 derivation paths. + /// # Arguments + /// * `keychain_mask` - Wallet secret mask to XOR against the stored wallet seed before using, if + /// being used. + /// + /// # Returns + /// * Result Containing: + /// * A Vector of [`AcctPathMapping`](../grin_wallet_libwallet/types/struct.AcctPathMapping.html) data + /// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered. + /// + /// # Remarks + /// + /// * A wallet should always have the path with the label 'default' path defined, + /// with path m/0/0 + /// * This method does not need to use the wallet seed or keychain. + /// + /// # Example + /// Set up as in [`new`](struct.Owner.html#method.new) method above. + /// ``` + /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); + /// + /// let api_owner = Owner::new(wallet.clone(), None); + /// + /// let result = api_owner.accounts(None); + /// + /// if let Ok(accts) = result { + /// //... + /// } + /// ``` + + pub fn accounts( + &self, + keychain_mask: Option<&SecretKey>, + ) -> Result, Error> { + let mut w_lock = self.wallet_inst.lock(); + let w = w_lock.lc_provider()?.wallet_inst()?; + // Test keychain mask, to keep API consistent + let _ = w.keychain(keychain_mask)?; + owner::accounts(&mut **w) + } + + /// Creates a new 'account', which is a mapping of a user-specified + /// label to a BIP32 path + /// + /// # Arguments + /// + /// * `keychain_mask` - Wallet secret mask to XOR against the stored wallet seed before using, if + /// being used. + /// * `label` - A human readable label to which to map the new BIP32 Path + /// + /// # Returns + /// * Result Containing: + /// * A [Keychain Identifier](../grin_keychain/struct.Identifier.html) for the new path + /// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered. + /// + /// # Remarks + /// + /// * Wallets should be initialised with the 'default' path mapped to `m/0/0` + /// * Each call to this function will increment the first element of the path + /// so the first call will create an account at `m/1/0` and the second at + /// `m/2/0` etc. . . + /// * The account path is used throughout as the parent key for most key-derivation + /// operations. See [`set_active_account`](struct.Owner.html#method.set_active_account) for + /// further details. + /// + /// * This function does not need to use the root wallet seed or keychain. + /// + /// # Example + /// Set up as in [`new`](struct.Owner.html#method.new) method above. + /// ``` + /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); + /// + /// let api_owner = Owner::new(wallet.clone(), None); + /// + /// let result = api_owner.create_account_path(None, "account1"); + /// + /// if let Ok(identifier) = result { + /// //... + /// } + /// ``` + + pub fn create_account_path( + &self, + keychain_mask: Option<&SecretKey>, + label: &str, + ) -> Result { + let mut w_lock = self.wallet_inst.lock(); + let w = w_lock.lc_provider()?.wallet_inst()?; + owner::create_account_path(&mut **w, keychain_mask, label) + } + + /// Sets the wallet's currently active account. This sets the + /// BIP32 parent path used for most key-derivation operations. + /// + /// # Arguments + /// * `keychain_mask` - Wallet secret mask to XOR against the stored wallet seed before using, if + /// being used. + /// * `label` - The human readable label for the account. Accounts can be retrieved via + /// the [`account`](struct.Owner.html#method.accounts) method + /// + /// # Returns + /// * Result Containing: + /// * `Ok(())` if the path was correctly set + /// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered. + /// + /// # Remarks + /// + /// * Wallet parent paths are 2 path elements long, e.g. `m/0/0` is the path + /// labelled 'default'. Keys derived from this parent path are 3 elements long, + /// e.g. the secret keys derived from the `m/0/0` path will be at paths `m/0/0/0`, + /// `m/0/0/1` etc... + /// + /// * This function does not need to use the root wallet seed or keychain. + /// + /// # Example + /// Set up as in [`new`](struct.Owner.html#method.new) method above. + /// ``` + /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); + /// + /// let api_owner = Owner::new(wallet.clone(), None); + /// + /// let result = api_owner.create_account_path(None, "account1"); + /// + /// if let Ok(identifier) = result { + /// // set the account active + /// let result2 = api_owner.set_active_account(None, "account1"); + /// } + /// ``` + + pub fn set_active_account( + &self, + keychain_mask: Option<&SecretKey>, + label: &str, + ) -> Result<(), Error> { + let mut w_lock = self.wallet_inst.lock(); + let w = w_lock.lc_provider()?.wallet_inst()?; + // Test keychain mask, to keep API consistent + let _ = w.keychain(keychain_mask)?; + owner::set_active_account(&mut **w, label) + } + + /// Returns a list of outputs from the active account in the wallet. + /// + /// # Arguments + /// * `keychain_mask` - Wallet secret mask to XOR against the stored wallet seed before using, if + /// being used. + /// * `include_spent` - If `true`, outputs that have been marked as 'spent' + /// in the wallet will be returned. If `false`, spent outputs will omitted + /// from the results. + /// * `refresh_from_node` - If true, the wallet will attempt to contact + /// a node (via the [`NodeClient`](../grin_wallet_libwallet/types/trait.NodeClient.html) + /// provided during wallet instantiation). If `false`, the results will + /// contain output information that may be out-of-date (from the last time + /// the wallet's output set was refreshed against the node). + /// Note this setting is ignored if the updater process is running via a call to + /// [`start_updater`](struct.Owner.html#method.start_updater) + /// * `tx_id` - If `Some(i)`, only return the outputs associated with + /// the transaction log entry of id `i`. + /// + /// # Returns + /// * `(bool, Vec)` - A tuple: + /// * The first `bool` element indicates whether the data was successfully + /// refreshed from the node (note this may be false even if the `refresh_from_node` + /// argument was set to `true`. + /// * The second element contains a vector of + /// [OutputCommitMapping](../grin_wallet_libwallet/types/struct.OutputCommitMapping.html) + /// of which each element is a mapping between the wallet's internal + /// [OutputData](../grin_wallet_libwallet/types/struct.Output.html) + /// and the Output commitment as identified in the chain's UTXO set + /// + /// # Example + /// Set up as in [`new`](struct.Owner.html#method.new) method above. + /// ``` + /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); + /// + /// let api_owner = Owner::new(wallet.clone(), None); + /// let show_spent = false; + /// let update_from_node = true; + /// let tx_id = None; + /// + /// let result = api_owner.retrieve_outputs(None, show_spent, update_from_node, tx_id); + /// + /// if let Ok((was_updated, output_mappings)) = result { + /// //... + /// } + /// ``` + + pub fn retrieve_outputs( + &self, + keychain_mask: Option<&SecretKey>, + include_spent: bool, + refresh_from_node: bool, + tx_id: Option, + ) -> Result<(bool, Vec), Error> { + let tx = { + let t = self.status_tx.lock(); + t.clone() + }; + let refresh_from_node = match self.updater_running.load(Ordering::Relaxed) { + true => false, + false => refresh_from_node, + }; + owner::retrieve_outputs( + self.wallet_inst.clone(), + keychain_mask, + &tx, + include_spent, + refresh_from_node, + tx_id, + ) + } + + /// Returns a list of [Transaction Log Entries](../grin_wallet_libwallet/types/struct.TxLogEntry.html) + /// from the active account in the wallet. + /// + /// # Arguments + /// * `keychain_mask` - Wallet secret mask to XOR against the stored wallet seed before using, if + /// being used. + /// * `refresh_from_node` - If true, the wallet will attempt to contact + /// a node (via the [`NodeClient`](../grin_wallet_libwallet/types/trait.NodeClient.html) + /// provided during wallet instantiation). If `false`, the results will + /// contain transaction information that may be out-of-date (from the last time + /// the wallet's output set was refreshed against the node). + /// Note this setting is ignored if the updater process is running via a call to + /// [`start_updater`](struct.Owner.html#method.start_updater) + /// * `tx_id` - If `Some(i)`, only return the transactions associated with + /// the transaction log entry of id `i`. + /// * `tx_slate_id` - If `Some(uuid)`, only return transactions associated with + /// the given [`Slate`](../grin_wallet_libwallet/slate/struct.Slate.html) uuid. + /// * `tx_query_args` - If provided, use advanced query arguments as documented in + /// (../grin_wallet_libwallet/types.struct.RetrieveTxQueryArgs.html). If either + /// `tx_id` or `tx_slate_id` is provided in the same call, this argument is ignored + /// + /// # Returns + /// * `(bool, Vec, + refresh_from_node: bool, + tx_id: Option, + tx_slate_id: Option, + tx_query_args: Option, + ) -> Result<(bool, Vec), Error> { + let tx = { + let t = self.status_tx.lock(); + t.clone() + }; + let refresh_from_node = match self.updater_running.load(Ordering::Relaxed) { + true => false, + false => refresh_from_node, + }; + let mut res = owner::retrieve_txs( + self.wallet_inst.clone(), + keychain_mask, + &tx, + refresh_from_node, + tx_id, + tx_slate_id, + tx_query_args, + )?; + if self.doctest_mode { + res.1 = res + .1 + .into_iter() + .map(|mut t| { + t.confirmation_ts = Some(Utc.with_ymd_and_hms(2019, 1, 15, 16, 1, 26).unwrap()); + t.creation_ts = Utc.with_ymd_and_hms(2019, 1, 15, 16, 1, 26).unwrap(); + t + }) + .collect(); + } + Ok(res) + } + + /// Returns summary information from the active account in the wallet. + /// + /// # Arguments + /// * `keychain_mask` - Wallet secret mask to XOR against the stored wallet seed before using, if + /// being used. + /// * `refresh_from_node` - If true, the wallet will attempt to contact + /// a node (via the [`NodeClient`](../grin_wallet_libwallet/types/trait.NodeClient.html) + /// provided during wallet instantiation). If `false`, the results will + /// contain transaction information that may be out-of-date (from the last time + /// the wallet's output set was refreshed against the node). + /// Note this setting is ignored if the updater process is running via a call to + /// [`start_updater`](struct.Owner.html#method.start_updater) + /// * `minimum_confirmations` - The minimum number of confirmations an output + /// should have before it's included in the 'amount_currently_spendable' total + /// + /// # Returns + /// * (`bool`, [`WalletInfo`](../grin_wallet_libwallet/types/struct.WalletInfo.html)) - A tuple: + /// * The first `bool` element indicates whether the data was successfully + /// refreshed from the node (note this may be false even if the `refresh_from_node` + /// argument was set to `true`. + /// * The second element contains the Summary [`WalletInfo`](../grin_wallet_libwallet/types/struct.WalletInfo.html) + /// + /// # Example + /// Set up as in [`new`](struct.Owner.html#method.new) method above. + /// ``` + /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); + /// + /// let mut api_owner = Owner::new(wallet.clone(), None); + /// let update_from_node = true; + /// let minimum_confirmations=10; + /// + /// // Return summary info for active account + /// let result = api_owner.retrieve_summary_info(None, update_from_node, minimum_confirmations); + /// + /// if let Ok((was_updated, summary_info)) = result { + /// //... + /// } + /// ``` + + pub fn retrieve_summary_info( + &self, + keychain_mask: Option<&SecretKey>, + refresh_from_node: bool, + minimum_confirmations: u64, + ) -> Result<(bool, WalletInfo), Error> { + let tx = { + let t = self.status_tx.lock(); + t.clone() + }; + let refresh_from_node = match self.updater_running.load(Ordering::Relaxed) { + true => false, + false => refresh_from_node, + }; + owner::retrieve_summary_info( + self.wallet_inst.clone(), + keychain_mask, + &tx, + refresh_from_node, + minimum_confirmations, + ) + } + + /// Initiates a new transaction as the sender, creating a new + /// [`Slate`](../grin_wallet_libwallet/slate/struct.Slate.html) object containing + /// the sender's inputs, change outputs, and public signature data. This slate can + /// then be sent to the recipient to continue the transaction via the + /// [Foreign API's `receive_tx`](struct.Foreign.html#method.receive_tx) method. + /// + /// When a transaction is created, the wallet must also lock inputs (and create unconfirmed + /// outputs) corresponding to the transaction created in the slate, so that the wallet doesn't + /// attempt to re-spend outputs that are already included in a transaction before the transaction + /// is confirmed. This method also returns a function that will perform that locking, and it is + /// up to the caller to decide the best time to call the lock function + /// (via the [`tx_lock_outputs`](struct.Owner.html#method.tx_lock_outputs) method). + /// If the exchange method is intended to be synchronous (such as via a direct http call,) + /// then the lock call can wait until the response is confirmed. If it is asynchronous, (such + /// as via file transfer,) the lock call should happen immediately (before the file is sent + /// to the recipient). + /// + /// If the `send_args` [`InitTxSendArgs`](../grin_wallet_libwallet/types/struct.InitTxSendArgs.html), + /// of the [`args`](../grin_wallet_libwallet/types/struct.InitTxArgs.html), field is Some, this + /// function will attempt to send the slate back to the sender using the slatepack sync + /// send (TOR). If providing this argument, check the `state` field of the slate to see if the + /// sync_send was successful (it should be S2 if the sync sent successfully). It will also post + /// the transction if the `post_tx` field is set. + /// + /// # Arguments + /// * `keychain_mask` - Wallet secret mask to XOR against the stored wallet seed before using, if + /// being used. + /// * `args` - [`InitTxArgs`](../grin_wallet_libwallet/types/struct.InitTxArgs.html), + /// transaction initialization arguments. See struct documentation for further detail. + /// + /// # Returns + /// * a result containing: + /// * The transaction [Slate](../grin_wallet_libwallet/slate/struct.Slate.html), + /// which can be forwarded to the recieving party by any means. Once the caller is relatively + /// certain that the transaction has been sent to the recipient, the associated wallet + /// transaction outputs should be locked via a call to + /// [`tx_lock_outputs`](struct.Owner.html#method.tx_lock_outputs). This must be called before calling + /// [`finalize_tx`](struct.Owner.html#method.finalize_tx). + /// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered. + /// + /// # Remarks + /// + /// * This method requires an active connection to a node, and will fail with error if a node + /// cannot be contacted to refresh output statuses. + /// * This method will store a partially completed transaction in the wallet's transaction log, + /// which will be updated on the corresponding call to [`finalize_tx`](struct.Owner.html#method.finalize_tx). + /// + /// # Example + /// Set up as in [new](struct.Owner.html#method.new) method above. + /// ``` + /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); + /// + /// let mut api_owner = Owner::new(wallet.clone(), None); + /// // Attempt to create a transaction using the 'default' account + /// let args = InitTxArgs { + /// src_acct_name: None, + /// amount: 2_000_000_000, + /// minimum_confirmations: 2, + /// max_outputs: 500, + /// num_change_outputs: 1, + /// selection_strategy_is_use_all: false, + /// ..Default::default() + /// }; + /// let result = api_owner.init_send_tx( + /// None, + /// args, + /// ); + /// + /// if let Ok(slate) = result { + /// // Send slate somehow + /// // ... + /// // Lock our outputs if we're happy the slate was (or is being) sent + /// api_owner.tx_lock_outputs(None, &slate); + /// } + /// ``` + + pub fn init_send_tx( + &self, + keychain_mask: Option<&SecretKey>, + args: InitTxArgs, + ) -> Result { + let send_args = args.send_args.clone(); + let slate = { + let mut w_lock = self.wallet_inst.lock(); + let w = w_lock.lc_provider()?.wallet_inst()?; + owner::init_send_tx(&mut **w, keychain_mask, args, self.doctest_mode)? + }; + // Helper functionality. If send arguments exist, attempt to send sync and + // finalize + let skip_tor = match send_args.as_ref() { + None => false, + Some(sa) => sa.skip_tor, + }; + match send_args { + Some(sa) => { + let tor_config_lock = self.tor_config.lock(); + let tc = tor_config_lock.clone(); + let tc = match tc { + Some(mut c) => { + c.skip_send_attempt = Some(skip_tor); + Some(c) + } + None => None, + }; + let res = try_slatepack_sync_workflow( + &slate, + &sa.dest, + tc, + None, + false, + self.doctest_mode, + ); + match res { + Ok(Some(s)) => { + if sa.post_tx { + self.tx_lock_outputs(keychain_mask, &s)?; + let ret_slate = self.finalize_tx(keychain_mask, &s)?; + let result = self.post_tx(keychain_mask, &ret_slate, sa.fluff); + match result { + Ok(_) => { + info!("Tx sent ok",); + return Ok(ret_slate); + } + Err(e) => { + error!("Tx sent fail: {}", e); + return Err(e); + } + } + } else { + self.tx_lock_outputs(keychain_mask, &s)?; + let ret_slate = self.finalize_tx(keychain_mask, &s)?; + return Ok(ret_slate); + } + } + Ok(None) => Ok(slate), + Err(_) => Ok(slate), + } + } + None => Ok(slate), + } + } + + /// Issues a new invoice transaction slate, essentially a `request for payment`. + /// The slate created by this function will contain the amount, an output for the amount, + /// as well as round 1 of singature creation complete. The slate should then be send + /// to the payer, who should add their inputs and signature data and return the slate + /// via the [Foreign API's `finalize_tx`](struct.Foreign.html#method.finalize_tx) method. + /// + /// # Arguments + /// * `keychain_mask` - Wallet secret mask to XOR against the stored wallet seed before using, if + /// being used. + /// * `args` - [`IssueInvoiceTxArgs`](../grin_wallet_libwallet/types/struct.IssueInvoiceTxArgs.html), + /// invoice transaction initialization arguments. See struct documentation for further detail. + /// + /// # Returns + /// * ``Ok([`slate`](../grin_wallet_libwallet/slate/struct.Slate.html))` if successful, + /// containing the updated slate. + /// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered. + /// + /// # Example + /// Set up as in [`new`](struct.Owner.html#method.new) method above. + /// ``` + /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); + /// + /// let mut api_owner = Owner::new(wallet.clone(), None); + /// + /// let args = IssueInvoiceTxArgs { + /// amount: 60_000_000_000, + /// ..Default::default() + /// }; + /// let result = api_owner.issue_invoice_tx(None, args); + /// + /// if let Ok(slate) = result { + /// // if okay, send to the payer to add their inputs + /// // . . . + /// } + /// ``` + pub fn issue_invoice_tx( + &self, + keychain_mask: Option<&SecretKey>, + args: IssueInvoiceTxArgs, + ) -> Result { + let mut w_lock = self.wallet_inst.lock(); + let w = w_lock.lc_provider()?.wallet_inst()?; + owner::issue_invoice_tx(&mut **w, keychain_mask, args, self.doctest_mode) + } + + /// Processes an invoice tranaction created by another party, essentially + /// a `request for payment`. The incoming slate should contain a requested + /// amount, an output created by the invoicer convering the amount, and + /// part 1 of signature creation completed. This function will add inputs + /// equalling the amount + fees, as well as perform round 1 and 2 of signature + /// creation. + /// + /// Callers should note that no prompting of the user will be done by this function + /// it is up to the caller to present the request for payment to the user + /// and verify that payment should go ahead. + /// + /// If the `send_args` [`InitTxSendArgs`](../grin_wallet_libwallet/types/struct.InitTxSendArgs.html), + /// of the [`args`](../grin_wallet_libwallet/types/struct.InitTxArgs.html), field is Some, this + /// function will attempt to send the slate back to the initiator using the slatepack sync + /// send (TOR). If providing this argument, check the `state` field of the slate to see if the + /// sync_send was successful (it should be I3 if the sync sent successfully). + /// + /// This function also stores the final transaction in the user's wallet files for retrieval + /// via the [`get_stored_tx`](struct.Owner.html#method.get_stored_tx) function. + /// + /// # Arguments + /// * `keychain_mask` - Wallet secret mask to XOR against the stored wallet seed before using, if + /// being used. + /// * `slate` - The transaction [`Slate`](../grin_wallet_libwallet/slate/struct.Slate.html). The + /// payer should have filled in round 1 and 2. + /// * `args` - [`InitTxArgs`](../grin_wallet_libwallet/types/struct.InitTxArgs.html), + /// transaction initialization arguments. See struct documentation for further detail. + /// + /// # Returns + /// * ``Ok([`slate`](../grin_wallet_libwallet/slate/struct.Slate.html))` if successful, + /// containing the updated slate. + /// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered. + /// + /// # Example + /// Set up as in [`new`](struct.Owner.html#method.new) method above. + /// ``` + /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); + /// + /// let mut api_owner = Owner::new(wallet.clone(), None); + /// + /// // . . . + /// // The slate has been recieved from the invoicer, somehow + /// # let slate = Slate::blank(2, true); + /// let args = InitTxArgs { + /// src_acct_name: None, + /// amount: slate.amount, + /// minimum_confirmations: 2, + /// max_outputs: 500, + /// num_change_outputs: 1, + /// selection_strategy_is_use_all: false, + /// ..Default::default() + /// }; + /// + /// let result = api_owner.process_invoice_tx(None, &slate, args); + /// + /// if let Ok(slate) = result { + /// // If result okay, send back to the invoicer + /// // . . . + /// } + /// ``` + + pub fn process_invoice_tx( + &self, + keychain_mask: Option<&SecretKey>, + slate: &Slate, + args: InitTxArgs, + ) -> Result { + let mut w_lock = self.wallet_inst.lock(); + let w = w_lock.lc_provider()?.wallet_inst()?; + let send_args = args.send_args.clone(); + let slate = + owner::process_invoice_tx(&mut **w, keychain_mask, slate, args, self.doctest_mode)?; + // Helper functionality. If send arguments exist, attempt to send + match send_args { + Some(sa) => { + let tor_config_lock = self.tor_config.lock(); + let tc = tor_config_lock.clone(); + let tc = match tc { + Some(mut c) => { + c.skip_send_attempt = Some(sa.skip_tor); + Some(c) + } + None => None, + }; + let res = try_slatepack_sync_workflow( + &slate, + &sa.dest, + tc, + None, + true, + self.doctest_mode, + ); + match res { + Ok(s) => Ok(s.unwrap()), + Err(_) => Ok(slate), + } + } + None => Ok(slate), + } + } + + /// Locks the outputs associated with the inputs to the transaction in the given + /// [`Slate`](../grin_wallet_libwallet/slate/struct.Slate.html), + /// making them unavailable for use in further transactions. This function is called + /// by the sender, (or more generally, all parties who have put inputs into the transaction,) + /// and must be called before the corresponding call to [`finalize_tx`](struct.Owner.html#method.finalize_tx) + /// that completes the transaction. + /// + /// Outputs will generally remain locked until they are removed from the chain, + /// at which point they will become `Spent`. It is commonplace for transactions not to complete + /// for various reasons over which a particular wallet has no control. For this reason, + /// [`cancel_tx`](struct.Owner.html#method.cancel_tx) can be used to manually unlock outputs + /// and return them to the `Unspent` state. + /// + /// # Arguments + /// * `keychain_mask` - Wallet secret mask to XOR against the stored wallet seed before using, if + /// being used. + /// * `slate` - The transaction [`Slate`](../grin_wallet_libwallet/slate/struct.Slate.html). All + /// * `participant_id` - The participant id, generally 0 for the party putting in funds, 1 for the + /// party receiving. + /// elements in the `input` vector of the `tx` field that are found in the wallet's currently + /// active account will be set to status `Locked` + /// + /// # Returns + /// * Ok(()) if successful + /// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered. + /// + /// # Example + /// Set up as in [`new`](struct.Owner.html#method.new) method above. + /// ``` + /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); + /// + /// let mut api_owner = Owner::new(wallet.clone(), None); + /// let args = InitTxArgs { + /// src_acct_name: None, + /// amount: 2_000_000_000, + /// minimum_confirmations: 10, + /// max_outputs: 500, + /// num_change_outputs: 1, + /// selection_strategy_is_use_all: false, + /// ..Default::default() + /// }; + /// let result = api_owner.init_send_tx( + /// None, + /// args, + /// ); + /// + /// if let Ok(slate) = result { + /// // Send slate somehow + /// // ... + /// // Lock our outputs if we're happy the slate was (or is being) sent + /// api_owner.tx_lock_outputs(None, &slate); + /// } + /// ``` + + pub fn tx_lock_outputs( + &self, + keychain_mask: Option<&SecretKey>, + slate: &Slate, + ) -> Result<(), Error> { + let mut w_lock = self.wallet_inst.lock(); + let w = w_lock.lc_provider()?.wallet_inst()?; + owner::tx_lock_outputs(&mut **w, keychain_mask, slate) + } + + /// Finalizes a transaction, after all parties + /// have filled in both rounds of Slate generation. This step adds + /// all participants partial signatures to create the final signature, + /// resulting in a final transaction that is ready to post to a node. + /// + /// Note that this function DOES NOT POST the transaction to a node + /// for validation. This is done in separately via the + /// [`post_tx`](struct.Owner.html#method.post_tx) function. + /// + /// This function also stores the final transaction in the user's wallet files for retrieval + /// via the [`get_stored_tx`](struct.Owner.html#method.get_stored_tx) function. + /// + /// # Arguments + /// * `keychain_mask` - Wallet secret mask to XOR against the stored wallet seed before using, if + /// being used. + /// * `slate` - The transaction [`Slate`](../grin_wallet_libwallet/slate/struct.Slate.html). All + /// participants must have filled in both rounds, and the sender should have locked their + /// outputs (via the [`tx_lock_outputs`](struct.Owner.html#method.tx_lock_outputs) function). + /// + /// # Returns + /// * ``Ok([`slate`](../grin_wallet_libwallet/slate/struct.Slate.html))` if successful, + /// containing the new finalized slate. + /// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered. + /// + /// # Example + /// Set up as in [`new`](struct.Owner.html#method.new) method above. + /// ``` + /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); + /// + /// let mut api_owner = Owner::new(wallet.clone(), None); + /// let args = InitTxArgs { + /// src_acct_name: None, + /// amount: 2_000_000_000, + /// minimum_confirmations: 10, + /// max_outputs: 500, + /// num_change_outputs: 1, + /// selection_strategy_is_use_all: false, + /// ..Default::default() + /// }; + /// let result = api_owner.init_send_tx( + /// None, + /// args, + /// ); + /// + /// if let Ok(slate) = result { + /// // Send slate somehow + /// // ... + /// // Lock our outputs if we're happy the slate was (or is being) sent + /// let res = api_owner.tx_lock_outputs(None, &slate); + /// // + /// // Retrieve slate back from recipient + /// // + /// let res = api_owner.finalize_tx(None, &slate); + /// } + /// ``` + + pub fn finalize_tx( + &self, + keychain_mask: Option<&SecretKey>, + slate: &Slate, + ) -> Result { + let mut w_lock = self.wallet_inst.lock(); + let w = w_lock.lc_provider()?.wallet_inst()?; + owner::finalize_tx(&mut **w, keychain_mask, slate) + } + + /// Posts a completed transaction to the listening node for validation and inclusion in a block + /// for mining. + /// + /// # Arguments + /// * `keychain_mask` - Wallet secret mask to XOR against the stored wallet seed before using, if + /// being used. + /// * `tx` - A completed [`Transaction`](../grin_core/core/transaction/struct.Transaction.html), + /// typically the `tx` field in the transaction [`Slate`](../grin_wallet_libwallet/slate/struct.Slate.html). + /// * `fluff` - Instruct the node whether to use the Dandelion protocol when posting the + /// transaction. If `true`, the node should skip the Dandelion phase and broadcast the + /// transaction to all peers immediately. If `false`, the node will follow dandelion logic and + /// initiate the stem phase. + /// + /// # Returns + /// * `Ok(())` if successful + /// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered. + /// + /// # Example + /// Set up as in [`new`](struct.Owner.html#method.new) method above. + /// ``` + /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); + /// + /// let mut api_owner = Owner::new(wallet.clone(), None); + /// let args = InitTxArgs { + /// src_acct_name: None, + /// amount: 2_000_000_000, + /// minimum_confirmations: 10, + /// max_outputs: 500, + /// num_change_outputs: 1, + /// selection_strategy_is_use_all: false, + /// ..Default::default() + /// }; + /// let result = api_owner.init_send_tx( + /// None, + /// args, + /// ); + /// + /// if let Ok(slate) = result { + /// // Send slate somehow + /// // ... + /// // Lock our outputs if we're happy the slate was (or is being) sent + /// let res = api_owner.tx_lock_outputs(None, &slate); + /// // + /// // Retrieve slate back from recipient + /// // + /// let res = api_owner.finalize_tx(None, &slate); + /// let res = api_owner.post_tx(None, &slate, true); + /// } + /// ``` + + pub fn post_tx( + &self, + keychain_mask: Option<&SecretKey>, + slate: &Slate, + fluff: bool, + ) -> Result<(), Error> { + let client = { + let mut w_lock = self.wallet_inst.lock(); + let w = w_lock.lc_provider()?.wallet_inst()?; + // Test keychain mask, to keep API consistent + let _ = w.keychain(keychain_mask)?; + w.w2n_client().clone() + }; + owner::post_tx(&client, slate.tx_or_err()?, fluff) + } + + /// Cancels a transaction. This entails: + /// * Setting the transaction status to either `TxSentCancelled` or `TxReceivedCancelled` + /// * Deleting all change outputs or recipient outputs associated with the transaction + /// * Setting the status of all assocatied inputs from `Locked` to `Spent` so they can be + /// used in new transactions. + /// + /// Transactions can be cancelled by transaction log id or slate id (call with either set to + /// Some, not both) + /// + /// # Arguments + /// + /// * `keychain_mask` - Wallet secret mask to XOR against the stored wallet seed before using, if + /// being used. + /// * `tx_id` - If present, cancel by the [`TxLogEntry`](../grin_wallet_libwallet/types/struct.TxLogEntry.html) id + /// for the transaction. + /// + /// * `tx_slate_id` - If present, cancel by the Slate id. + /// + /// # Returns + /// * `Ok(())` if successful + /// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered. + /// + /// # Example + /// Set up as in [`new`](struct.Owner.html#method.new) method above. + /// ``` + /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); + /// + /// let mut api_owner = Owner::new(wallet.clone(), None); + /// let args = InitTxArgs { + /// src_acct_name: None, + /// amount: 2_000_000_000, + /// minimum_confirmations: 10, + /// max_outputs: 500, + /// num_change_outputs: 1, + /// selection_strategy_is_use_all: false, + /// ..Default::default() + /// }; + /// let result = api_owner.init_send_tx( + /// None, + /// args, + /// ); + /// + /// if let Ok(slate) = result { + /// // Send slate somehow + /// // ... + /// // Lock our outputs if we're happy the slate was (or is being) sent + /// let res = api_owner.tx_lock_outputs(None, &slate); + /// // + /// // We didn't get the slate back, or something else went wrong + /// // + /// let res = api_owner.cancel_tx(None, None, Some(slate.id.clone())); + /// } + /// ``` + + pub fn cancel_tx( + &self, + keychain_mask: Option<&SecretKey>, + tx_id: Option, + tx_slate_id: Option, + ) -> Result<(), Error> { + let tx = { + let t = self.status_tx.lock(); + t.clone() + }; + owner::cancel_tx( + self.wallet_inst.clone(), + keychain_mask, + &tx, + tx_id, + tx_slate_id, + ) + } + + /// Retrieves the stored transaction associated with a TxLogEntry. Can be used even after the + /// transaction has completed. Either the Transaction Log ID or the Slate UUID must be supplied. + /// If both are supplied, the Transaction Log ID is preferred. + /// + /// # Arguments + /// + /// * `keychain_mask` - Wallet secret mask to XOR against the stored wallet seed before using, if + /// being used. + /// * `tx_id` - The id of the transaction in the wallet's Transaction Log. Either this or + /// `slate_id` must be provided. + /// * `slate_id` - The UUID of the Transaction Slate to find. Either this or `tx_id` must be + /// provided + /// + /// # Returns + /// * Ok(Some([Slate](../grin_wallet_libwallet/slate/struct.Slate.html)) containing the stored + /// transaction, if successful. Note that this Slate will not contain all of the fields used by + /// the original Slate that resulted in the transaction. + /// * Ok(None) if the stored Transaction isn't found. + /// * [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered. + /// + /// # Example + /// Set up as in [`new`](struct.Owner.html#method.new) method above. + /// ``` + /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); + /// + /// let api_owner = Owner::new(wallet.clone(), None); + /// let update_from_node = true; + /// let tx_id = None; + /// let tx_slate_id = None; + /// + /// // Return all TxLogEntries + /// let result = api_owner.retrieve_txs(None, update_from_node, tx_id, tx_slate_id, None); + /// + /// if let Ok((was_updated, tx_log_entries)) = result { + /// let stored_tx = api_owner.get_stored_tx(None, Some(tx_log_entries[0].id), None).unwrap(); + /// //... + /// } + /// ``` + + pub fn get_stored_tx( + &self, + keychain_mask: Option<&SecretKey>, + tx_id: Option, + slate_id: Option<&Uuid>, + ) -> Result, Error> { + let mut w_lock = self.wallet_inst.lock(); + let w = w_lock.lc_provider()?.wallet_inst()?; + // Test keychain mask, to keep API consistent + let _ = w.keychain(keychain_mask)?; + owner::get_stored_tx(&**w, tx_id, slate_id) + } + + /// Return the rewind hash of the wallet. + /// The rewind hash when shared, help third-party to retrieve informations (outputs, balance, ...) that belongs to this wallet. + /// + /// # Arguments + /// + /// * `keychain_mask` - Wallet secret mask to XOR against the stored wallet seed before using, if + /// being used. + /// + /// # Returns + /// * `Ok(String)` if successful + /// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered. + + /// # Example + /// Set up as in [`new`](struct.Owner.html#method.new) method above. + /// ``` + /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); + /// + /// let mut api_owner = Owner::new(wallet.clone(), None); + /// let result = api_owner.scan( + /// None, + /// Some(20000), + /// false, + /// ); + /// + /// if let Ok(_) = result { + /// // Wallet outputs should be consistent with what's on chain + /// // ... + /// } + /// ``` + + pub fn get_rewind_hash(&self, keychain_mask: Option<&SecretKey>) -> Result { + owner::get_rewind_hash(self.wallet_inst.clone(), keychain_mask) + } + + /// Scans the entire UTXO set from the node, identify which outputs belong to the given rewind hash view wallet. + /// + /// This function can be used to retrieve outputs informations (outputs, balance, ...) from a rewind hash view wallet. + /// + /// This operation scans the entire chain, and is expected to be time intensive. It is imperative + /// that no other processes should be trying to use the wallet at the same time this function is + /// running. + /// + /// # Arguments + /// + /// * `rewind_hash` - Rewind hash of a wallet, used to retrieve the output of a third-party wallet. + /// * `start_height` - If provided, the height of the first block from which to start scanning. + /// The scan will start from block 1 if this is not provided. + /// + /// # Returns + /// * `Ok(ViewWallet)` if successful + /// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered. + + /// # Example + /// Set up as in [`new`](struct.Owner.html#method.new) method above. + /// ``` + /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); + /// + /// let mut api_owner = Owner::new(wallet.clone(), None); + /// let result = api_owner.scan( + /// None, + /// Some(20000), + /// false, + /// ); + /// + /// if let Ok(_) = result { + /// // Wallet outputs should be consistent with what's on chain + /// // ... + /// } + /// ``` + + pub fn scan_rewind_hash( + &self, + rewind_hash: String, + start_height: Option, + ) -> Result { + let tx = { + let t = self.status_tx.lock(); + t.clone() + }; + owner::scan_rewind_hash(self.wallet_inst.clone(), rewind_hash, start_height, &tx) + } + + /// Scans the entire UTXO set from the node, identify which outputs belong to the given wallet + /// update the wallet state to be consistent with what's currently in the UTXO set. + /// + /// This function can be used to repair wallet state, particularly by restoring outputs that may + /// be missing if the wallet owner has cancelled transactions locally that were then successfully + /// posted to the chain. + /// + /// This operation scans the entire chain, and is expected to be time intensive. It is imperative + /// that no other processes should be trying to use the wallet at the same time this function is + /// running. + /// + /// When an output is found that doesn't exist in the wallet, a corresponding + /// [TxLogEntry](../grin_wallet_libwallet/types/struct.TxLogEntry.html) is created. + /// + /// # Arguments + /// + /// * `keychain_mask` - Wallet secret mask to XOR against the stored wallet seed before using, if + /// being used. + /// * `start_height` - If provided, the height of the first block from which to start scanning. + /// The scan will start from block 1 if this is not provided. + /// * `delete_unconfirmed` - if `false`, the scan process will be non-destructive, and + /// mostly limited to restoring missing outputs. It will leave unconfirmed transaction logs entries + /// and unconfirmed outputs intact. If `true`, the process will unlock all locked outputs, + /// restore all missing outputs, and mark any outputs that have been marked 'Spent' but are still + /// in the UTXO set as 'Unspent' (as can happen during a fork). It will also attempt to cancel any + /// transaction log entries associated with any locked outputs or outputs incorrectly marked 'Spent'. + /// Note this completely removes all outstanding transactions, so users should be very aware what + /// will happen if this flag is set. Note that if transactions/outputs are removed that later + /// confirm on the chain, another call to this function will restore them. + /// + /// # Returns + /// * `Ok(())` if successful + /// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered. + + /// # Example + /// Set up as in [`new`](struct.Owner.html#method.new) method above. + /// ``` + /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); + /// + /// let mut api_owner = Owner::new(wallet.clone(), None); + /// let result = api_owner.scan( + /// None, + /// Some(20000), + /// false, + /// ); + /// + /// if let Ok(_) = result { + /// // Wallet outputs should be consistent with what's on chain + /// // ... + /// } + /// ``` + + pub fn scan( + &self, + keychain_mask: Option<&SecretKey>, + start_height: Option, + delete_unconfirmed: bool, + ) -> Result<(), Error> { + let tx = { + let t = self.status_tx.lock(); + t.clone() + }; + owner::scan( + self.wallet_inst.clone(), + keychain_mask, + start_height, + delete_unconfirmed, + &tx, + ) + } + + /// Retrieves the last known height known by the wallet. This is determined as follows: + /// * If the wallet can successfully contact its configured node, the reported node + /// height is returned, and the `updated_from_node` field in the response is `true` + /// * If the wallet cannot contact the node, this function returns the maximum height + /// of all outputs contained within the wallet, and the `updated_from_node` fields + /// in the response is set to false. + /// + /// Clients should generally ensure the `updated_from_node` field is returned as + /// `true` before assuming the height for any operation. + /// + /// # Arguments + /// + /// * `keychain_mask` - Wallet secret mask to XOR against the stored wallet seed before using, if + /// being used. + /// + /// # Returns + /// * Ok with a [`NodeHeightResult`](../grin_wallet_libwallet/types/struct.NodeHeightResult.html) + /// if successful. If the height result was obtained from the configured node, + /// `updated_from_node` will be set to `true` + /// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered. + /// + /// # Example + /// Set up as in [`new`](struct.Owner.html#method.new) method above. + /// ``` + /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); + /// + /// let api_owner = Owner::new(wallet.clone(), None); + /// let result = api_owner.node_height(None); + /// + /// if let Ok(node_height_result) = result { + /// if node_height_result.updated_from_node { + /// //we can assume node_height_result.height is relatively safe to use + /// + /// } + /// //... + /// } + /// ``` + + pub fn node_height( + &self, + keychain_mask: Option<&SecretKey>, + ) -> Result { + { + let mut w_lock = self.wallet_inst.lock(); + let w = w_lock.lc_provider()?.wallet_inst()?; + // Test keychain mask, to keep API consistent + let _ = w.keychain(keychain_mask)?; + } + let mut res = owner::node_height(self.wallet_inst.clone(), keychain_mask)?; + if self.doctest_mode { + // return a consistent hash for doctest + res.header_hash = + "d4b3d3c40695afd8c7760f8fc423565f7d41310b7a4e1c4a4a7950a66f16240d".to_owned(); + } + Ok(res) + } + + // LIFECYCLE FUNCTIONS + + /// Retrieve the top-level directory for the wallet. This directory should contain the + /// `grin-wallet.toml` file and the `wallet_data` directory that contains the wallet + /// seed + data files. Future versions of the wallet API will support multiple wallets + /// within the top level directory. + /// + /// The top level directory defaults to (in order of precedence): + /// + /// 1) The current directory, from which `grin-wallet` or the main process was run, if it + /// contains a `grin-wallet.toml` file. + /// 2) ~/.grin// otherwise + /// + /// # Arguments + /// + /// * None + /// + /// # Returns + /// * Ok with a String value representing the full path to the top level wallet dierctory + /// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered. + /// + /// # Example + /// Set up as in [`new`](struct.Owner.html#method.new) method above. + /// ``` + /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); + /// + /// let api_owner = Owner::new(wallet.clone(), None); + /// let result = api_owner.get_top_level_directory(); + /// + /// if let Ok(dir) = result { + /// println!("Top level directory is: {}", dir); + /// //... + /// } + /// ``` + + pub fn get_top_level_directory(&self) -> Result { + let mut w_lock = self.wallet_inst.lock(); + let lc = w_lock.lc_provider()?; + if self.doctest_mode && !self.doctest_retain_tld { + Ok("/doctest/dir".to_owned()) + } else { + lc.get_top_level_directory() + } + } + + /// Set the top-level directory for the wallet. This directory can be empty, and will be created + /// during a subsequent calls to [`create_config`](struct.Owner.html#method.create_config) + /// + /// Set [`get_top_level_directory`](struct.Owner.html#method.get_top_level_directory) for a + /// description of the top level directory and default paths. + /// + /// # Arguments + /// + /// * `dir`: The new top-level directory path (either relative to current directory or + /// absolute. + /// + /// # Returns + /// * Ok if successful + /// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered. + /// + /// # Example + /// Set up as in [`new`](struct.Owner.html#method.new) method above. + /// ``` + /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); + /// + /// let dir = "path/to/wallet/dir"; + /// + /// # let dir = tempdir().map_err(|e| format!("{:#?}", e)).unwrap(); + /// # let dir = dir + /// # .path() + /// # .to_str() + /// # .ok_or("Failed to convert tmpdir path to string.".to_owned()) + /// # .unwrap(); + /// + /// let api_owner = Owner::new(wallet.clone(), None); + /// let result = api_owner.set_top_level_directory(dir); + /// + /// if let Ok(dir) = result { + /// //... + /// } + /// ``` + + pub fn set_top_level_directory(&self, dir: &str) -> Result<(), Error> { + let mut w_lock = self.wallet_inst.lock(); + let lc = w_lock.lc_provider()?; + lc.set_top_level_directory(dir) + } + + /// Create a `grin-wallet.toml` configuration file in the top-level directory for the + /// specified chain type. + /// A custom [`WalletConfig`](../grin_wallet_config/types/struct.WalletConfig.html) + /// and/or grin `LoggingConfig` may optionally be provided, otherwise defaults will be used. + /// + /// Paths in the configuration file will be updated to reflect the top level directory, so + /// path-related values in the optional configuration structs will be ignored. + /// + /// # Arguments + /// + /// * `chain_type`: The chain type to use in creation of the configuration file. This can be + /// * `AutomatedTesting` + /// * `UserTesting` + /// * `Testnet` + /// * `Mainnet` + /// + /// # Returns + /// * Ok if successful + /// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered. + /// + /// # Example + /// Set up as in [`new`](struct.Owner.html#method.new) method above. + /// ``` + /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); + /// + /// use grin_core::global::ChainTypes; + /// + /// let dir = "path/to/wallet/dir"; + /// + /// # let dir = tempdir().map_err(|e| format!("{:#?}", e)).unwrap(); + /// # let dir = dir + /// # .path() + /// # .to_str() + /// # .ok_or("Failed to convert tmpdir path to string.".to_owned()) + /// # .unwrap(); + /// + /// let api_owner = Owner::new(wallet.clone(), None); + /// let _ = api_owner.set_top_level_directory(dir); + /// + /// let result = api_owner.create_config(&ChainTypes::Mainnet, None, None, None); + /// + /// if let Ok(_) = result { + /// //... + /// } + /// ``` + + pub fn create_config( + &self, + chain_type: &global::ChainTypes, + wallet_config: Option, + logging_config: Option, + tor_config: Option, + ) -> Result<(), Error> { + let mut w_lock = self.wallet_inst.lock(); + let lc = w_lock.lc_provider()?; + lc.create_config( + chain_type, + "grin-wallet.toml", + wallet_config, + logging_config, + tor_config, + ) + } + + /// Creates a new wallet seed and empty wallet database in the `wallet_data` directory of + /// the top level directory. + /// + /// Paths in the configuration file will be updated to reflect the top level directory, so + /// path-related values in the optional configuration structs will be ignored. + /// + /// The wallet files must not already exist, and ~The `grin-wallet.toml` file must exist + /// in the top level directory (can be created via a call to + /// [`create_config`](struct.Owner.html#method.create_config)) + /// + /// # Arguments + /// + /// * `name`: Reserved for future use, use `None` for the time being. + /// * `mnemonic`: If present, restore the wallet seed from the given mnemonic instead of creating + /// a new random seed. + /// * `mnemonic_length`: Desired length of mnemonic in bytes (16 or 32, either 12 or 24 words). + /// Use 0 if mnemonic isn't being used. + /// * `password`: The password used to encrypt/decrypt the `wallet.seed` file + /// + /// # Returns + /// * Ok if successful + /// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered. + /// + /// # Example + /// Set up as in [`new`](struct.Owner.html#method.new) method above. + /// ``` + /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); + /// + /// use grin_core::global::ChainTypes; + /// + /// // note that the WalletInst struct does not necessarily need to contain an + /// // instantiated wallet + /// + /// let dir = "path/to/wallet/dir"; + /// + /// # let dir = tempdir().map_err(|e| format!("{:#?}", e)).unwrap(); + /// # let dir = dir + /// # .path() + /// # .to_str() + /// # .ok_or("Failed to convert tmpdir path to string.".to_owned()) + /// # .unwrap(); + /// let api_owner = Owner::new(wallet.clone(), None); + /// let _ = api_owner.set_top_level_directory(dir); + /// + /// // Create configuration + /// let result = api_owner.create_config(&ChainTypes::Mainnet, None, None, None); + /// + /// // create new wallet wirh random seed + /// let pw = ZeroingString::from("my_password"); + /// let result = api_owner.create_wallet(None, None, 0, pw); + /// + /// if let Ok(r) = result { + /// //... + /// } + /// ``` + + pub fn create_wallet( + &self, + name: Option<&str>, + mnemonic: Option, + mnemonic_length: u32, + password: ZeroingString, + ) -> Result<(), Error> { + let mut w_lock = self.wallet_inst.lock(); + let lc = w_lock.lc_provider()?; + lc.create_wallet( + name, + mnemonic, + mnemonic_length as usize, + password, + self.doctest_mode, + ) + } + + /// `Opens` a wallet, populating the internal keychain with the encrypted seed, and optionally + /// returning a `keychain_mask` token to the caller to provide in all future calls. + /// If using a mask, the seed will be stored in-memory XORed against the `keychain_mask`, and + /// will not be useable if the mask is not provided. + /// + /// # Arguments + /// + /// * `name`: Reserved for future use, use `None` for the time being. + /// * `password`: The password to use to open the wallet + /// a new random seed. + /// * `use_mask`: Whether to create and return a mask which much be provided in all future + /// API calls. + /// + /// # Returns + /// * Ok if successful + /// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered. + /// + /// # Example + /// Set up as in [`new`](struct.Owner.html#method.new) method above. + /// ``` + /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); + /// + /// use grin_core::global::ChainTypes; + /// + /// // note that the WalletInst struct does not necessarily need to contain an + /// // instantiated wallet + /// let dir = "path/to/wallet/dir"; + /// + /// # let dir = tempdir().map_err(|e| format!("{:#?}", e)).unwrap(); + /// # let dir = dir + /// # .path() + /// # .to_str() + /// # .ok_or("Failed to convert tmpdir path to string.".to_owned()) + /// # .unwrap(); + /// let api_owner = Owner::new(wallet.clone(), None); + /// let _ = api_owner.set_top_level_directory(dir); + /// + /// // Create configuration + /// let result = api_owner.create_config(&ChainTypes::Mainnet, None, None, None); + /// + /// // create new wallet wirh random seed + /// let pw = ZeroingString::from("my_password"); + /// let _ = api_owner.create_wallet(None, None, 0, pw.clone()); + /// + /// let result = api_owner.open_wallet(None, pw, true); + /// + /// if let Ok(m) = result { + /// // use this mask in all subsequent calls + /// let mask = m; + /// } + /// ``` + + pub fn open_wallet( + &self, + name: Option<&str>, + password: ZeroingString, + use_mask: bool, + ) -> Result, Error> { + // just return a representative string for doctest mode + if self.doctest_mode { + let secp_inst = static_secp_instance(); + let secp = secp_inst.lock(); + return Ok(Some(SecretKey::from_slice( + &secp, + &from_hex("d096b3cb75986b3b13f80b8f5243a9edf0af4c74ac37578c5a12cfb5b59b1868") + .unwrap(), + )?)); + } + let mut w_lock = self.wallet_inst.lock(); + let lc = w_lock.lc_provider()?; + lc.open_wallet(name, password, use_mask, self.doctest_mode) + } + + /// `Close` a wallet, removing the master seed from memory. + /// + /// # Arguments + /// + /// * `name`: Reserved for future use, use `None` for the time being. + /// + /// # Returns + /// * Ok if successful + /// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered. + /// + /// # Example + /// Set up as in [`new`](struct.Owner.html#method.new) method above. + /// ``` + /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); + /// + /// use grin_core::global::ChainTypes; + /// + /// // Set up as above + /// # let api_owner = Owner::new(wallet.clone(), None); + /// + /// let res = api_owner.close_wallet(None); + /// + /// if let Ok(_) = res { + /// // ... + /// } + /// ``` + + pub fn close_wallet(&self, name: Option<&str>) -> Result<(), Error> { + let mut w_lock = self.wallet_inst.lock(); + let lc = w_lock.lc_provider()?; + lc.close_wallet(name) + } + + /// Return the BIP39 mnemonic for the given wallet. This function will decrypt + /// the wallet's seed file with the given password, and thus does not need the + /// wallet to be open. + /// + /// # Arguments + /// + /// * `name`: Reserved for future use, use `None` for the time being. + /// * `password`: The password used to encrypt the seed file. + /// + /// # Returns + /// * Ok(BIP-39 mneminc) if successful + /// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered. + /// + /// # Example + /// Set up as in [`new`](struct.Owner.html#method.new) method above. + /// ``` + /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); + /// + /// use grin_core::global::ChainTypes; + /// + /// // Set up as above + /// # let api_owner = Owner::new(wallet.clone(), None); + /// + /// let pw = ZeroingString::from("my_password"); + /// let res = api_owner.get_mnemonic(None, pw); + /// + /// if let Ok(mne) = res { + /// // ... + /// } + /// ``` + pub fn get_mnemonic( + &self, + name: Option<&str>, + password: ZeroingString, + ) -> Result { + let mut w_lock = self.wallet_inst.lock(); + let lc = w_lock.lc_provider()?; + lc.get_mnemonic(name, password) + } + + /// Changes a wallet's password, meaning the old seed file is decrypted with the old password, + /// and a new seed file is created with the same mnemonic and encrypted with the new password. + /// + /// This function temporarily backs up the old seed file until a test-decryption of the new + /// file is confirmed to contain the same seed as the original seed file, at which point the + /// backup is deleted. If this operation fails for an unknown reason, the backup file will still + /// exist in the wallet's data directory encrypted with the old password. + /// + /// # Arguments + /// + /// * `name`: Reserved for future use, use `None` for the time being. + /// * `old`: The password used to encrypt the existing seed file (i.e. old password) + /// * `new`: The password to be used to encrypt the new seed file + /// + /// # Returns + /// * Ok(()) if successful + /// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered. + /// + /// # Example + /// Set up as in [`new`](struct.Owner.html#method.new) method above. + /// ``` + /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); + /// + /// use grin_core::global::ChainTypes; + /// + /// // Set up as above + /// # let api_owner = Owner::new(wallet.clone(), None); + /// + /// let old = ZeroingString::from("my_password"); + /// let new = ZeroingString::from("new_password"); + /// let res = api_owner.change_password(None, old, new); + /// + /// if let Ok(mne) = res { + /// // ... + /// } + /// ``` + pub fn change_password( + &self, + name: Option<&str>, + old: ZeroingString, + new: ZeroingString, + ) -> Result<(), Error> { + let mut w_lock = self.wallet_inst.lock(); + let lc = w_lock.lc_provider()?; + lc.change_password(name, old, new) + } + + /// Deletes a wallet, removing the config file, seed file and all data files. + /// Obviously, use with extreme caution and plenty of user warning + /// + /// Highly recommended that the wallet be explicitly closed first via the `close_wallet` + /// function. + /// + /// # Arguments + /// + /// * `name`: Reserved for future use, use `None` for the time being. + /// + /// # Returns + /// * Ok if successful + /// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered. + /// + /// # Example + /// Set up as in [`new`](struct.Owner.html#method.new) method above. + /// ``` + /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); + /// + /// use grin_core::global::ChainTypes; + /// + /// // Set up as above + /// # let api_owner = Owner::new(wallet.clone(), None); + /// + /// let res = api_owner.delete_wallet(None); + /// + /// if let Ok(_) = res { + /// // ... + /// } + /// ``` + + pub fn delete_wallet(&self, name: Option<&str>) -> Result<(), Error> { + let mut w_lock = self.wallet_inst.lock(); + let lc = w_lock.lc_provider()?; + lc.delete_wallet(name) + } + + /// Starts a background wallet update thread, which performs the wallet update process + /// automatically at the frequency specified. + /// + /// The updater process is as follows: + /// + /// * Reconcile the wallet outputs against the node's current UTXO set, confirming + /// transactions if needs be. + /// * Look up transactions by kernel in cases where it's necessary (for instance, when + /// there are no change outputs for a transaction and transaction status can't be + /// inferred from the output state. + /// * Incrementally perform a scan of the UTXO set, correcting outputs and transactions + /// where their local state differs from what's on-chain. The wallet stores the last + /// position scanned, and will scan back 100 blocks worth of UTXOs on each update, to + /// correct any differences due to forks or otherwise. + /// + /// Note that an update process can take a long time, particularly when the entire + /// UTXO set is being scanned for correctness. The wallet status can be determined by + /// calling the [`get_updater_messages`](struct.Owner.html#method.get_updater_messages). + /// + /// # Arguments + /// + /// * `keychain_mask` - Wallet secret mask to XOR against the stored wallet seed before using, if + /// being used. + /// * `frequency`: The frequency at which to call the update process. Note this is + /// time elapsed since the last successful update process. If calling via the JSON-RPC + /// api, this represents milliseconds. + /// + /// # Returns + /// * Ok if successful + /// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered. + /// + /// # Example + /// Set up as in [`new`](struct.Owner.html#method.new) method above. + /// ``` + /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); + /// + /// use grin_core::global::ChainTypes; + /// + /// use std::time::Duration; + /// + /// // Set up as above + /// # let api_owner = Owner::new(wallet.clone(), None); + /// + /// let res = api_owner.start_updater(None, Duration::from_secs(60)); + /// + /// if let Ok(_) = res { + /// // ... + /// } + /// ``` + + pub fn start_updater( + &self, + keychain_mask: Option<&SecretKey>, + frequency: Duration, + ) -> Result<(), Error> { + let updater_inner = self.updater.clone(); + let tx_inner = { + let t = self.status_tx.lock(); + t.clone() + }; + let keychain_mask = match keychain_mask { + Some(m) => Some(m.clone()), + None => None, + }; + let _ = thread::Builder::new() + .name("wallet-updater".to_string()) + .spawn(move || { + let u = updater_inner.lock(); + if let Err(e) = u.run(frequency, keychain_mask, &tx_inner) { + error!("Wallet state updater failed with error: {:?}", e); + } + })?; + Ok(()) + } + + /// Stops the background update thread. If the updater is currently updating, the + /// thread will stop after the next update + /// + /// # Arguments + /// + /// * None + /// + /// # Returns + /// * Ok if successful + /// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered. + /// + /// # Example + /// Set up as in [`new`](struct.Owner.html#method.new) method above. + /// ``` + /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); + /// + /// use grin_core::global::ChainTypes; + /// + /// use std::time::Duration; + /// + /// // Set up as above + /// # let api_owner = Owner::new(wallet.clone(), None); + /// + /// let res = api_owner.start_updater(None, Duration::from_secs(60)); + /// + /// if let Ok(_) = res { + /// // ... + /// } + /// + /// let res = api_owner.stop_updater(); + /// ``` + + pub fn stop_updater(&self) -> Result<(), Error> { + self.updater_running.store(false, Ordering::Relaxed); + Ok(()) + } + + /// Retrieve messages from the updater thread, up to `count` number of messages. + /// The resulting array will be ordered newest messages first. The updater will + /// store a maximum of 10,000 messages, after which it will start removing the oldest + /// messages as newer ones are created. + /// + /// Messages retrieved via this method are removed from the internal queue, so calling + /// this function at a specified interval should result in a complete message history. + /// + /// # Arguments + /// + /// * `count` - The number of messages to retrieve. + /// + /// # Returns + /// * Ok with a Vec of [`StatusMessage`](../grin_wallet_libwallet/api_impl/owner_updater/enum.StatusMessage.html) + /// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered. + /// + /// # Example + /// Set up as in [`new`](struct.Owner.html#method.new) method above. + /// ``` + /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); + /// + /// use grin_core::global::ChainTypes; + /// + /// use std::time::Duration; + /// + /// // Set up as above + /// # let api_owner = Owner::new(wallet.clone(), None); + /// + /// let res = api_owner.start_updater(None, Duration::from_secs(60)); + /// + /// let messages = api_owner.get_updater_messages(10000); + /// + /// if let Ok(_) = res { + /// // ... + /// } + /// + /// ``` + + pub fn get_updater_messages(&self, count: usize) -> Result, Error> { + let mut q = self.updater_messages.lock(); + let index = q.len().saturating_sub(count); + Ok(q.split_off(index)) + } + + // SLATEPACK + + /// Retrieve the public slatepack address associated with the active account at the + /// given derivation path. + /// + /// In this case, an "address" means a Slatepack Address corresponding to + /// a private key derived as follows: + /// + /// e.g. The default parent account is at + /// + /// `m/0/0` + /// + /// With output blinding factors created as + /// + /// `m/0/0/0` + /// `m/0/0/1` etc... + /// + /// The corresponding public address derivation path would be at: + /// + /// `m/0/1` + /// + /// With addresses created as: + /// + /// `m/0/1/0` + /// `m/0/1/1` etc... + /// + /// Note that these addresses correspond to the public keys used in the addresses + /// of TOR hidden services configured by the wallet listener. + /// + /// # Arguments + /// + /// * `keychain_mask` - Wallet secret mask to XOR against the stored wallet seed before using, if + /// * `derivation_index` - The index along the derivation path to retrieve an address for + /// + /// # Returns + /// * Ok with a SlatepackAddress representing the address + /// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered. + /// + /// # Example + /// Set up as in [`new`](struct.Owner.html#method.new) method above. + /// ``` + /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); + /// + /// use grin_core::global::ChainTypes; + /// + /// use std::time::Duration; + /// + /// // Set up as above + /// # let api_owner = Owner::new(wallet.clone(), None); + /// + /// let res = api_owner.get_slatepack_address(None, 0); + /// + /// if let Ok(_) = res { + /// // ... + /// } + /// + /// ``` + + pub fn get_slatepack_address( + &self, + keychain_mask: Option<&SecretKey>, + derivation_index: u32, + ) -> Result { + owner::get_slatepack_address(self.wallet_inst.clone(), keychain_mask, derivation_index) + } + + /// Retrieve the private ed25519 slatepack key at the given derivation index. Currently + /// used to decrypt encrypted slatepack messages. + /// + /// # Arguments + /// + /// * `keychain_mask` - Wallet secret mask to XOR against the stored wallet seed before using, if + /// * `derivation_index` - The index along the derivation path to for which to retrieve the secret key + /// + /// # Returns + /// * Ok with an ed25519_dalek::SecretKey if successful + /// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered. + /// + /// # Example + /// Set up as in [`new`](struct.Owner.html#method.new) method above. + /// ``` + /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); + /// + /// use grin_core::global::ChainTypes; + /// + /// use std::time::Duration; + /// + /// // Set up as above + /// # let api_owner = Owner::new(wallet.clone(), None); + /// + /// let res = api_owner.get_slatepack_secret_key(None, 0); + /// + /// if let Ok(_) = res { + /// // ... + /// } + /// + /// ``` + pub fn get_slatepack_secret_key( + &self, + keychain_mask: Option<&SecretKey>, + derivation_index: u32, + ) -> Result { + owner::get_slatepack_secret_key(self.wallet_inst.clone(), keychain_mask, derivation_index) + } + + /// Create a slatepack from a given slate, optionally encoding the slate with the provided + /// recipient public keys + /// + /// # Arguments + /// + /// * `keychain_mask` - Wallet secret mask to XOR against the stored wallet seed before using, if + /// * `sender_index` - If Some(n), the index along the derivation path to include as the sender + /// * `recipients` - Optional recipients for which to encrypt the slatepack's payload (i.e. the + /// slate). If an empty vec, the payload will remain unencrypted + /// + /// # Returns + /// * Ok with a String representing an armored slatepack if successful + /// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered. + /// + /// # Example + /// Set up as in [`new`](struct.Owner.html#method.new) method above. + /// ``` + /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); + /// + /// use grin_core::global::ChainTypes; + /// + /// use std::time::Duration; + /// + /// // Set up as above + /// # let api_owner = Owner::new(wallet.clone(), None); + /// + /// let mut api_owner = Owner::new(wallet.clone(), None); + /// let args = InitTxArgs { + /// src_acct_name: None, + /// amount: 2_000_000_000, + /// minimum_confirmations: 10, + /// max_outputs: 500, + /// num_change_outputs: 1, + /// selection_strategy_is_use_all: false, + /// ..Default::default() + /// }; + /// let result = api_owner.init_send_tx( + /// None, + /// args, + /// ); + /// + /// if let Ok(slate) = result { + /// // Create a slatepack from our slate + /// let slatepack = api_owner.create_slatepack_message( + /// None, + /// &slate, + /// Some(0), + /// vec![], + /// ); + /// } + /// + /// ``` + + pub fn create_slatepack_message( + &self, + keychain_mask: Option<&SecretKey>, + slate: &Slate, + sender_index: Option, + recipients: Vec, + ) -> Result { + owner::create_slatepack_message( + self.wallet_inst.clone(), + keychain_mask, + slate, + sender_index, + recipients, + ) + } + + /// Extract the slate from the given slatepack. If the slatepack payload is encrypted, attempting to + /// decrypt with keys at the given address derivation path indices. + /// + /// # Arguments + /// + /// * `keychain_mask` - Wallet secret mask to XOR against the stored wallet seed before using, if + /// * `slatepack` - A string representing an armored slatepack + /// * `secret_indices` - Indices along this wallet's deriviation path with which to attempt + /// decryption. This function will attempt to use secret keys at each index along this path + /// to attempt to decrypt the payload, returning an error if none of the keys match. + /// + /// # Returns + /// * Ok with a [Slate](../grin_wallet_libwallet/slate/struct.Slate.html) if successful + /// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered. + /// + /// # Example + /// Set up as in [`new`](struct.Owner.html#method.new) method above. + /// ``` + /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); + /// + /// use grin_core::global::ChainTypes; + /// + /// use std::time::Duration; + /// + /// // Set up as above + /// # let api_owner = Owner::new(wallet.clone(), None); + /// // ... receive a slatepack from somewhere + /// # let slatepack_string = String::from(""); + /// let res = api_owner.slate_from_slatepack_message( + /// None, + /// slatepack_string, + /// vec![0, 1, 2], + /// ); + /// ``` + + pub fn slate_from_slatepack_message( + &self, + keychain_mask: Option<&SecretKey>, + slatepack: String, + secret_indices: Vec, + ) -> Result { + owner::slate_from_slatepack_message( + self.wallet_inst.clone(), + keychain_mask, + slatepack, + secret_indices, + ) + } + + /// Decode an armored slatepack, returning a Slatepack object that can be + /// viewed, manipulated, output as json, etc. The resulting slatepack will be + /// decrypted by this wallet if possible + /// + /// # Arguments + /// + /// * `keychain_mask` - Wallet secret mask to XOR against the stored wallet seed before using + /// * `slatepack` - A string representing an armored slatepack + /// * `secret_indices` - Indices along this wallet's deriviation path with which to attempt + /// decryption. If this wallet can't decrypt this slatepack, the payload of the returned + /// Slatepack will remain encrypted. + /// + /// # Returns + /// * Ok with a [Slatepack](../grin_wallet_libwallet/slatepack/types/struct.Slatepack.html) if successful + /// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered. + /// + /// # Example + /// Set up as in [`new`](struct.Owner.html#method.new) method above. + /// ``` + /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); + /// + /// use grin_core::global::ChainTypes; + /// + /// use std::time::Duration; + /// + /// // Set up as above + /// # let api_owner = Owner::new(wallet.clone(), None); + /// # let slatepack_string = String::from(""); + /// // .. receive a slatepack from somewhere + /// let res = api_owner.decode_slatepack_message( + /// None, + /// slatepack_string, + /// vec![0, 1, 2], + /// ); + /// + /// ``` + + pub fn decode_slatepack_message( + &self, + keychain_mask: Option<&SecretKey>, + slatepack: String, + secret_indices: Vec, + ) -> Result { + owner::decode_slatepack_message( + self.wallet_inst.clone(), + keychain_mask, + slatepack, + secret_indices, + ) + } + + // PAYMENT PROOFS + + /// Returns a single, exportable [PaymentProof](../grin_wallet_libwallet/api_impl/types/struct.PaymentProof.html) + /// from a completed transaction within the wallet. + /// + /// The transaction must have been created with a payment proof, and the transaction must be + /// complete in order for a payment proof to be returned. Either the `tx_id` or `tx_slate_id` + /// argument must be provided, or the function will return an error. + /// + /// # Arguments + /// * `keychain_mask` - Wallet secret mask to XOR against the stored wallet seed before using, if + /// being used. + /// * `refresh_from_node` - If true, the wallet will attempt to contact + /// a node (via the [`NodeClient`](../grin_wallet_libwallet/types/trait.NodeClient.html) + /// provided during wallet instantiation). If `false`, the results will + /// contain transaction information that may be out-of-date (from the last time + /// the wallet's output set was refreshed against the node). + /// Note this setting is ignored if the updater process is running via a call to + /// [`start_updater`](struct.Owner.html#method.start_updater) + /// * `tx_id` - If `Some(i)` return the proof associated with the transaction with id `i` + /// * `tx_slate_id` - If `Some(uuid)`, return the proof associated with the transaction with the + /// given `uuid` + /// + /// # Returns + /// * Ok([PaymentProof](../grin_wallet_libwallet/api_impl/types/struct.PaymentProof.html)) if successful + /// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered + /// or the proof is not present or complete + /// + /// # Example + /// Set up as in [`new`](struct.Owner.html#method.new) method above. + /// ``` + /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); + /// + /// let api_owner = Owner::new(wallet.clone(), None); + /// let update_from_node = true; + /// let tx_id = None; + /// let tx_slate_id = Some(Uuid::parse_str("0436430c-2b02-624c-2032-570501212b00").unwrap()); + /// + /// // Return all TxLogEntries + /// let result = api_owner.retrieve_payment_proof(None, update_from_node, tx_id, tx_slate_id); + /// + /// if let Ok(p) = result { + /// //... + /// } + /// ``` + + pub fn retrieve_payment_proof( + &self, + keychain_mask: Option<&SecretKey>, + refresh_from_node: bool, + tx_id: Option, + tx_slate_id: Option, + ) -> Result { + let tx = { + let t = self.status_tx.lock(); + t.clone() + }; + let refresh_from_node = match self.updater_running.load(Ordering::Relaxed) { + true => false, + false => refresh_from_node, + }; + owner::retrieve_payment_proof( + self.wallet_inst.clone(), + keychain_mask, + &tx, + refresh_from_node, + tx_id, + tx_slate_id, + ) + } + + /// Verifies a [PaymentProof](../grin_wallet_libwallet/api_impl/types/struct.PaymentProof.html) + /// This process entails: + /// + /// * Ensuring the kernel identified by the proof's stored excess commitment exists in the kernel set + /// * Reproducing the signed message `amount|kernel_commitment|sender_address` + /// * Validating the proof's `recipient_sig` against the message using the recipient's + /// address as the public key and + /// * Validating the proof's `sender_sig` against the message using the senders's + /// address as the public key + /// + /// This function also checks whether the sender or recipient address belongs to the currently + /// open wallet, and returns 2 booleans indicating whether the address belongs to the sender and + /// whether the address belongs to the recipient respectively + /// + /// # Arguments + /// * `keychain_mask` - Wallet secret mask to XOR against the stored wallet seed before using, if + /// being used. + /// * `proof` A [PaymentProof](../grin_wallet_libwallet/api_impl/types/struct.PaymentProof.html)) + /// + /// # Returns + /// * Ok((bool, bool)) if the proof is valid. The first boolean indicates whether the sender + /// address belongs to this wallet, the second whether the recipient address belongs to this + /// wallet + /// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered + /// or the proof is not present or complete + /// + /// # Example + /// Set up as in [`new`](struct.Owner.html#method.new) method above. + /// ``` + /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); + /// + /// let api_owner = Owner::new(wallet.clone(), None); + /// let update_from_node = true; + /// let tx_id = None; + /// let tx_slate_id = Some(Uuid::parse_str("0436430c-2b02-624c-2032-570501212b00").unwrap()); + /// + /// // Return all TxLogEntries + /// let result = api_owner.retrieve_payment_proof(None, update_from_node, tx_id, tx_slate_id); + /// + /// // The proof will likely be exported as JSON to be provided to another party + /// + /// if let Ok(p) = result { + /// let valid = api_owner.verify_payment_proof(None, &p); + /// if let Ok(_) = valid { + /// //... + /// } + /// } + /// ``` + + pub fn verify_payment_proof( + &self, + keychain_mask: Option<&SecretKey>, + proof: &PaymentProof, + ) -> Result<(bool, bool), Error> { + owner::verify_payment_proof(self.wallet_inst.clone(), keychain_mask, proof) + } + + /// Builds an output + pub fn build_output( + &self, + keychain_mask: Option<&SecretKey>, + features: OutputFeatures, + amount: u64, + ) -> Result { + let mut w_lock = self.wallet_inst.lock(); + let w = w_lock.lc_provider()?.wallet_inst()?; + owner::build_output(&mut **w, keychain_mask, features, amount) + } + + // MWIXNET + + /// Creates an mwixnet request [SwapReq](../grin_wallet_libwallet/api_impl/types/struct.SwapReq.html) + /// from a given output commitment under this wallet's control. + /// + /// # Arguments + /// * `keychain_mask` - Wallet secret mask to XOR against the stored wallet seed before using, if + /// being used. + /// * `params` - A [MixnetReqCreationParams](../grin_wallet_libwallet/api_impl/types/struct.MixnetReqCreationParams.html) + /// struct containing the parameters for the request, which include: + /// `server_keys` - The public keys of the servers participating in the mixnet (each encoded internally as a `SecretKey`) + /// `fee_per_hop` - The fee to be paid to each server for each hop in the mixnet + /// * `commitment` - The commitment of the output to be mixed + /// * `lock_output` - Whether to lock the referenced output after creating the request + /// + /// # Returns + /// * Ok([SwapReq](../grin_wallet_libwallet/api_impl/types/struct.SwapReq.html)) if successful + /// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered + /// + /// # Example + /// Set up as in [`new`](struct.Owner.html#method.new) method above. + /// ``` + /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); + /// + /// let api_owner = Owner::new(wallet.clone(), None); + /// let keychain_mask = None; + /// let params = MixnetReqCreationParams { + /// server_keys: vec![], // Public keys here in secret key representation + /// fee_per_hop: 100, + /// }; + /// + /// let commitment = Commitment::from_vec(vec![0; 32]); + /// let lock_output = true; + /// + /// let result = api_owner.create_mwixnet_req( + /// keychain_mask, + /// ¶ms, + /// &commitment, + /// lock_output, + /// ); + /// + /// if let Ok(req) = result { + /// //... + /// } + /// ``` + + pub fn create_mwixnet_req( + &self, + keychain_mask: Option<&SecretKey>, + params: &MixnetReqCreationParams, + commitment: &Commitment, + lock_output: bool, // use_test_rng: bool, + ) -> Result { + let mut w_lock = self.wallet_inst.lock(); + let w = w_lock.lc_provider()?.wallet_inst()?; + owner::create_mwixnet_req( + &mut **w, + keychain_mask, + params, + commitment, + lock_output, + self.doctest_mode, + ) + } +} + +/// attempt to send slate synchronously with TOR +pub fn try_slatepack_sync_workflow( + slate: &Slate, + dest: &str, + tor_config: Option, + tor_sender: Option, + send_to_finalize: bool, + test_mode: bool, +) -> Result, libwallet::Error> { + if let Some(tc) = &tor_config { + if tc.skip_send_attempt == Some(true) { + return Ok(None); + } + } + let mut ret_slate = Slate::blank(2, false); + let mut send_sync = |mut sender: HttpSlateSender, method_str: &str| match sender + .send_tx(&slate, send_to_finalize) + { + Ok(s) => { + ret_slate = s; + return Ok(()); + } + Err(e) => { + debug!( + "Send ({}): Could not send Slate via {}: {}", + method_str, method_str, e + ); + return Err(e); + } + }; + + // Try parsing Slatepack address + match SlatepackAddress::try_from(dest) { + Ok(address) => { + let tor_addr = OnionV3Address::try_from(&address).unwrap(); + // Try sending to the destination via TOR + let sender = match tor_sender { + None => { + if test_mode { + None + } else { + match HttpSlateSender::with_socks_proxy( + &tor_addr.to_http_str(), + &tor_config.as_ref().unwrap().socks_proxy_addr, + &tor_config.as_ref().unwrap().send_config_dir, + tor_config.as_ref().unwrap().bridge.clone(), + tor_config.as_ref().unwrap().proxy.clone(), + ) { + Ok(s) => Some(s), + Err(e) => { + debug!("Send (TOR): Cannot create TOR Slate sender {:?}", e); + None + } + } + } + } + Some(s) => { + if test_mode { + None + } else { + Some(s) + } + } + }; + if let Some(s) = sender { + warn!("Attempting to send transaction via TOR"); + match send_sync(s, "TOR") { + Ok(_) => return Ok(Some(ret_slate)), + Err(e) => { + debug!("Unable to send via TOR: {}", e); + warn!("Unable to send transaction via TOR"); + } + } + } + } + Err(e) => { + debug!("Send (TOR): Destination is not SlatepackAddress {:?}", e); + warn!("Destination is not a valid Slatepack address. Will output Slatepack.") + } + } + + Ok(None) +} + +#[doc(hidden)] +#[macro_export] +macro_rules! doctest_helper_setup_doc_env { + ($wallet:ident, $wallet_config:ident) => { + use grin_core::{self, global}; + use grin_keychain as keychain; + use grin_util as util; + use grin_wallet_api as api; + use grin_wallet_config as config; + use grin_wallet_impls as impls; + use grin_wallet_libwallet as libwallet; + + use keychain::ExtKeychain; + use tempfile::tempdir; + + use grin_util::secp::pedersen::Commitment; + use std::sync::Arc; + use util::{Mutex, ZeroingString}; + + use api::{Foreign, Owner}; + use config::WalletConfig; + use impls::{DefaultLCProvider, DefaultWalletImpl, HTTPNodeClient}; + use libwallet::{ + mwixnet::MixnetReqCreationParams, BlockFees, InitTxArgs, IssueInvoiceTxArgs, Slate, + WalletInst, + }; + + use uuid::Uuid; + + // don't run on windows CI, which gives very inconsistent results + if cfg!(windows) { + return; + } + + // Set our local chain_type for testing. + global::set_local_chain_type(global::ChainTypes::AutomatedTesting); + + let dir = tempdir().map_err(|e| format!("{:#?}", e)).unwrap(); + let dir = dir + .path() + .to_str() + .ok_or("Failed to convert tmpdir path to string.".to_owned()) + .unwrap(); + let mut wallet_config = WalletConfig::default(); + wallet_config.data_file_dir = dir.to_owned(); + let pw = ZeroingString::from(""); + + let node_client = + HTTPNodeClient::new(&wallet_config.check_node_api_http_addr, None).unwrap(); + let mut wallet = Box::new( + DefaultWalletImpl::<'static, HTTPNodeClient>::new(node_client.clone()).unwrap(), + ) + as Box< + WalletInst< + 'static, + DefaultLCProvider, + HTTPNodeClient, + ExtKeychain, + >, + >; + let lc = wallet.lc_provider().unwrap(); + let _ = lc.set_top_level_directory(&wallet_config.data_file_dir); + lc.open_wallet(None, pw, false, false); + let mut $wallet = Arc::new(Mutex::new(wallet)); + }; +} diff --git a/api/src/owner_rpc.rs b/api/src/owner_rpc.rs new file mode 100644 index 0000000..c983c20 --- /dev/null +++ b/api/src/owner_rpc.rs @@ -0,0 +1,2726 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! JSON-RPC Stub generation for the Owner API +use grin_wallet_libwallet::RetrieveTxQueryArgs; +use libwallet::mwixnet::SwapReq; +use uuid::Uuid; + +use crate::config::{TorConfig, WalletConfig}; +use crate::core::core::OutputFeatures; +use crate::core::global; +use crate::keychain::{Identifier, Keychain}; +use crate::libwallet::{ + mwixnet::MixnetReqCreationParams, AcctPathMapping, Amount, BuiltOutput, Error, InitTxArgs, + IssueInvoiceTxArgs, NodeClient, NodeHeightResult, OutputCommitMapping, PaymentProof, Slate, + SlateVersion, Slatepack, SlatepackAddress, StatusMessage, TxLogEntry, VersionedSlate, + ViewWallet, WalletInfo, WalletLCProvider, +}; +use crate::util::logger::LoggingConfig; +use crate::util::secp::key::{PublicKey, SecretKey}; +use crate::util::secp::pedersen::Commitment; +use crate::util::{from_hex, static_secp_instance, Mutex, ZeroingString}; +use crate::{ECDHPubkey, Ed25519SecretKey, Owner, Token}; +use easy_jsonrpc_mw; +use grin_wallet_util::OnionV3Address; +use rand::thread_rng; +use std::convert::TryFrom; +use std::sync::Arc; +use std::time::Duration; + +/// Public definition used to generate Owner jsonrpc api. +/// Secure version containing wallet lifecycle functions. All calls to this API must be encrypted. +/// See [`init_secure_api`](#tymethod.init_secure_api) for details of secret derivation +/// and encryption. + +#[easy_jsonrpc_mw::rpc] +pub trait OwnerRpc { + /** + Networked version of [Owner::accounts](struct.Owner.html#method.accounts). + + # Json rpc example + + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "accounts", + "params": { + "token": "d202964900000000d302964900000000d402964900000000d502964900000000" + }, + "id": 1 + } + # "# + # , + # r#" + { + "jsonrpc": "2.0", + "result": { + "Ok": [ + { + "label": "default", + "path": "0200000000000000000000000000000000" + } + ] + }, + "id": 1 + } + # "# + # , 4, false, false, false, false); + ``` + */ + fn accounts(&self, token: Token) -> Result, Error>; + + /** + Networked version of [Owner::create_account_path](struct.Owner.html#method.create_account_path). + + # Json rpc example + + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "create_account_path", + "params": { + "token": "d202964900000000d302964900000000d402964900000000d502964900000000", + "label": "account1" + }, + "id": 1 + } + # "# + # , + # r#" + { + "jsonrpc": "2.0", + "result": { + "Ok": "0200000001000000000000000000000000" + }, + "id": 1 + } + # "# + # , 4, false, false, false, false); + ``` + */ + fn create_account_path(&self, token: Token, label: &String) -> Result; + + /** + Networked version of [Owner::set_active_account](struct.Owner.html#method.set_active_account). + + # Json rpc example + + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "set_active_account", + "params": { + "token": "d202964900000000d302964900000000d402964900000000d502964900000000", + "label": "default" + }, + "id": 1 + } + # "# + # , + # r#" + { + "jsonrpc": "2.0", + "result": { + "Ok": null + }, + "id": 1 + } + # "# + # , 4, false, false, false, false); + ``` + */ + fn set_active_account(&self, token: Token, label: &String) -> Result<(), Error>; + + /** + Networked version of [Owner::retrieve_outputs](struct.Owner.html#method.retrieve_outputs). + + # Json rpc example + + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "retrieve_outputs", + "params": { + "token": "d202964900000000d302964900000000d402964900000000d502964900000000", + "include_spent": false, + "refresh_from_node": true, + "tx_id": null + }, + "id": 1 + } + # "# + # , + # r#" + { + "id": 1, + "jsonrpc": "2.0", + "result": { + "Ok": [ + true, + [ + { + "commit": "08e1da9e6dc4d6e808a718b2f110a991dd775d65ce5ae408a4e1f002a4961aa9e7", + "output": { + "commit": "08e1da9e6dc4d6e808a718b2f110a991dd775d65ce5ae408a4e1f002a4961aa9e7", + "height": "1", + "is_coinbase": true, + "key_id": "0300000000000000000000000000000000", + "lock_height": "4", + "mmr_index": null, + "n_child": 0, + "root_key_id": "0200000000000000000000000000000000", + "status": "Unspent", + "tx_log_entry": 0, + "value": "60000000000" + } + }, + { + "commit": "087df32304c5d4ae8b2af0bc31e700019d722910ef87dd4eec3197b80b207e3045", + "output": { + "commit": "087df32304c5d4ae8b2af0bc31e700019d722910ef87dd4eec3197b80b207e3045", + "height": "2", + "is_coinbase": true, + "key_id": "0300000000000000000000000100000000", + "lock_height": "5", + "mmr_index": null, + "n_child": 1, + "root_key_id": "0200000000000000000000000000000000", + "status": "Unspent", + "tx_log_entry": 1, + "value": "60000000000" + } + } + ] + ] + } + } + # "# + # , 2, false, false, false, false); + ``` + */ + + fn retrieve_outputs( + &self, + token: Token, + include_spent: bool, + refresh_from_node: bool, + tx_id: Option, + ) -> Result<(bool, Vec), Error>; + + /** + Networked version of [Owner::retrieve_txs](struct.Owner.html#method.retrieve_txs). + + # Json rpc example + + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "retrieve_txs", + "params": { + "token": "d202964900000000d302964900000000d402964900000000d502964900000000", + "refresh_from_node": true, + "tx_id": null, + "tx_slate_id": null + }, + "id": 1 + } + # "# + # , + # r#" + { + "id": 1, + "jsonrpc": "2.0", + "result": { + "Ok": [ + true, + [ + { + "amount_credited": "60000000000", + "amount_debited": "0", + "confirmation_ts": "2019-01-15T16:01:26Z", + "confirmed": true, + "creation_ts": "2019-01-15T16:01:26Z", + "fee": null, + "id": 0, + "kernel_excess": "0838e19c490038b10f051c9c190a9b1f96d59bbd242f5d3143f50630deb74342ed", + "kernel_lookup_min_height": 1, + "num_inputs": 0, + "num_outputs": 1, + "parent_key_id": "0200000000000000000000000000000000", + "stored_tx": null, + "ttl_cutoff_height": null, + "tx_slate_id": null, + "payment_proof": null, + "reverted_after": null, + "tx_type": "ConfirmedCoinbase" + }, + { + "amount_credited": "60000000000", + "amount_debited": "0", + "confirmation_ts": "2019-01-15T16:01:26Z", + "confirmed": true, + "creation_ts": "2019-01-15T16:01:26Z", + "fee": null, + "id": 1, + "kernel_excess": "08cd9d890c0b6a004f700aa5939a1ce0488fe2a11fa33cf096b50732ceab0be1df", + "kernel_lookup_min_height": 2, + "num_inputs": 0, + "num_outputs": 1, + "parent_key_id": "0200000000000000000000000000000000", + "stored_tx": null, + "ttl_cutoff_height": null, + "payment_proof": null, + "reverted_after": null, + "tx_slate_id": null, + "tx_type": "ConfirmedCoinbase" + } + ] + ] + } + } + # "# + # , 2, false, false, false, false); + ``` + */ + + fn retrieve_txs( + &self, + token: Token, + refresh_from_node: bool, + tx_id: Option, + tx_slate_id: Option, + ) -> Result<(bool, Vec), Error>; + + /** + Networked version of [Owner::retrieve_txs](struct.Owner.html#method.retrieve_txs), which passes only the `tx_query_args` + parameter. See (../grin_wallet_libwallet/types.struct.RetrieveTxQueryArgs.html) + + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "query_txs", + "params": { + "token": "d202964900000000d302964900000000d402964900000000d502964900000000", + "refresh_from_node": true, + "query": { + "min_id": 0, + "max_id": 100, + "min_amount": "0", + "max_amount": "60000000000", + "sort_field": "Id", + "sort_order": "Asc" + } + }, + "id": 1 + } + # "# + # , + # r#" + { + "id": 1, + "jsonrpc": "2.0", + "result": { + "Ok": [ + true, + [ + { + "amount_credited": "60000000000", + "amount_debited": "0", + "confirmation_ts": "2019-01-15T16:01:26Z", + "confirmed": true, + "creation_ts": "2019-01-15T16:01:26Z", + "fee": null, + "id": 0, + "kernel_excess": "0838e19c490038b10f051c9c190a9b1f96d59bbd242f5d3143f50630deb74342ed", + "kernel_lookup_min_height": 1, + "num_inputs": 0, + "num_outputs": 1, + "parent_key_id": "0200000000000000000000000000000000", + "stored_tx": null, + "ttl_cutoff_height": null, + "tx_slate_id": null, + "payment_proof": null, + "reverted_after": null, + "tx_type": "ConfirmedCoinbase" + }, + { + "amount_credited": "60000000000", + "amount_debited": "0", + "confirmation_ts": "2019-01-15T16:01:26Z", + "confirmed": true, + "creation_ts": "2019-01-15T16:01:26Z", + "fee": null, + "id": 1, + "kernel_excess": "08cd9d890c0b6a004f700aa5939a1ce0488fe2a11fa33cf096b50732ceab0be1df", + "kernel_lookup_min_height": 2, + "num_inputs": 0, + "num_outputs": 1, + "parent_key_id": "0200000000000000000000000000000000", + "stored_tx": null, + "ttl_cutoff_height": null, + "payment_proof": null, + "reverted_after": null, + "tx_slate_id": null, + "tx_type": "ConfirmedCoinbase" + } + ] + ] + } + } + # "# + # , 2, false, false, false, false); + ``` + + */ + + fn query_txs( + &self, + token: Token, + refresh_from_node: bool, + query: RetrieveTxQueryArgs, + ) -> Result<(bool, Vec), Error>; + + /** + Networked version of [Owner::retrieve_summary_info](struct.Owner.html#method.retrieve_summary_info). + + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "retrieve_summary_info", + "params": { + "token": "d202964900000000d302964900000000d402964900000000d502964900000000", + "refresh_from_node": true, + "minimum_confirmations": 1 + }, + "id": 1 + } + # "# + # , + # r#" + { + "id": 1, + "jsonrpc": "2.0", + "result": { + "Ok": [ + true, + { + "amount_awaiting_confirmation": "0", + "amount_awaiting_finalization": "0", + "amount_currently_spendable": "60000000000", + "amount_immature": "180000000000", + "amount_locked": "0", + "amount_reverted": "0", + "last_confirmed_height": "4", + "minimum_confirmations": "1", + "total": "240000000000" + } + ] + } + } + # "# + # , 4, false, false, false, false); + ``` + */ + + fn retrieve_summary_info( + &self, + token: Token, + refresh_from_node: bool, + minimum_confirmations: u64, + ) -> Result<(bool, WalletInfo), Error>; + + /** + ;Networked version of [Owner::init_send_tx](struct.Owner.html#method.init_send_tx). + + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "init_send_tx", + "params": { + "token": "d202964900000000d302964900000000d402964900000000d502964900000000", + "args": { + "src_acct_name": null, + "amount": "6000000000", + "minimum_confirmations": 2, + "max_outputs": 500, + "num_change_outputs": 1, + "selection_strategy_is_use_all": true, + "target_slate_version": null, + "payment_proof_recipient_address": "tgrin1xtxavwfgs48ckf3gk8wwgcndmn0nt4tvkl8a7ltyejjcy2mc6nfs9gm2lp", + "ttl_blocks": null, + "send_args": null + } + }, + "id": 1 + } + # "# + # , + # r#" + { + "id": 1, + "jsonrpc": "2.0", + "result": { + "Ok": { + "amt": "6000000000", + "fee": "23000000", + "id": "0436430c-2b02-624c-2032-570501212b00", + "proof": { + "raddr": "32cdd63928854f8b2628b1dce4626ddcdf35d56cb7cfdf7d64cca5822b78d4d3", + "saddr": "32cdd63928854f8b2628b1dce4626ddcdf35d56cb7cfdf7d64cca5822b78d4d3" + }, + "sigs": [ + { + "nonce": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", + "xs": "02e89cce4499ac1e9bb498dab9e3fab93cc40cd3d26c04a0292e00f4bf272499ec" + } + ], + "sta": "S1", + "ver": "4:2" + } + } + } + # "# + # , 4, false, false, false, false); + ``` + */ + + fn init_send_tx(&self, token: Token, args: InitTxArgs) -> Result; + + /** + ;Networked version of [Owner::issue_invoice_tx](struct.Owner.html#method.issue_invoice_tx). + + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "issue_invoice_tx", + "params": { + "token": "d202964900000000d302964900000000d402964900000000d502964900000000", + "args": { + "amount": "6000000000", + "dest_acct_name": null, + "target_slate_version": null + } + }, + "id": 1 + } + # "# + # , + # r#" + { + "id": 1, + "jsonrpc": "2.0", + "result": { + "Ok": { + "amt": "6000000000", + "id": "0436430c-2b02-624c-2032-570501212b00", + "sigs": [ + { + "nonce": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", + "xs": "02e89cce4499ac1e9bb498dab9e3fab93cc40cd3d26c04a0292e00f4bf272499ec" + } + ], + "sta": "I1", + "ver": "4:2" + } + } + } + # "# + # , 4, false, false, false, false); + ``` + */ + + fn issue_invoice_tx( + &self, + token: Token, + args: IssueInvoiceTxArgs, + ) -> Result; + + /** + ;Networked version of [Owner::process_invoice_tx](struct.Owner.html#method.process_invoice_tx). + + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "process_invoice_tx", + "params": { + "token": "d202964900000000d302964900000000d402964900000000d502964900000000", + "slate": { + "amt": "6000000000", + "id": "0436430c-2b02-624c-2032-570501212b00", + "off": "d202964900000000d302964900000000d402964900000000d502964900000000", + "sigs": [ + { + "nonce": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", + "xs": "028e95921cc0d5be5922362265d352c9bdabe51a9e1502a3f0d4a10387f1893f40" + } + ], + "sta": "I1", + "ver": "4:2" + }, + "args": { + "src_acct_name": null, + "amount": "0", + "minimum_confirmations": 2, + "max_outputs": 500, + "num_change_outputs": 1, + "selection_strategy_is_use_all": true, + "target_slate_version": null, + "payment_proof_recipient_address": null, + "ttl_blocks": null, + "send_args": null + } + }, + "id": 1 + } + # "# + # , + # r#" + { + "id": 1, + "jsonrpc": "2.0", + "result": { + "Ok": { + "coms": [ + { + "c": "08e1da9e6dc4d6e808a718b2f110a991dd775d65ce5ae408a4e1f002a4961aa9e7", + "f": 1 + }, + { + "c": "087e4e373ef2ab9921ba53e05f384b717789ddb4ad18a8f2057c9338bd639e02a5", + "p": "28875d797af7cb6c63eba070e0a79af57ea0a434d7d34801a02bc85624ae14a4a13519164737c7154b6222a9d6da33b8c52ef7dc4dc58aea3c776b7907e474450a52f3ccc017f66e2ce9f97a45733d6ed90a223e7d1a67802d393834cc9e4103c27bb7d63abc2753a5b54bcc48751c63b6accde16a37678338452bc985d24fb6af405a9166c0ca750f1cdedc5c0996c56f199722df3844b822de96480fac6e706dab6241d0338d7914a10a0e83406d0689224a3286e8c579c50882ce96123aecc6aa667c27abf1ce894e0c6282fc81e5fba51d498af16c5b0c39b45faf3f0cd7140dccae7d8d45330ec7895ce0c90e2490877311b9dfe157c05c6206f929ffef0da1a8d807077712a80670dfb9ac38ca565d47acf7e93bd09f418f20f10c9e87f6f4421fa889e522c33475f98ddff87a36eb0a0b445a8679628e163ae56bf3cfc39a5a5867d3e31e1e9d373a6b3924d7d895d5140e4bf00c0cbf7f343c12dc2b2c6b01769a588cc1ef1178fbf3bd645e25bf5c458c4af79884329b7ed80e08868121baeb39b11814f2dd8dddbb7114382e65378e2c6f1e837ace9a980acb965629f9f1525f60efb54301a7540a9105bf33eac1be37e1add96801f1c62857be0ac38ac370e0722764c59517960056bafe6fdd388eb78c98954f3f966d44e8f060366617844eff416625f8609b44263efc10e4f2f4fb22ceae5c16d4105e477a49511b4ac37aefac17e5532ee1ccb1654eb0bf17b32415561f02c2b07462f2c5aa7846ef21cfb30548c6bfe4d762333a199be183d7d9fa1ae6c9b4730965f741183d75ac0610efcf48d0039514011816f421a7a1a4c7c1bbc2ba8b522178cff367b4c704d343fac3a2662b50211556b630b5620244587d2f90941ef1edf8e44fa97d35daaa58d16fff3f57c6e6fa618f511dc770704d831a1f49630ec9da6f33f551923c" + } + ], + "fee": "23000000", + "id": "0436430c-2b02-624c-2032-570501212b00", + "off": "16672e6b4e2a6851b27641d8b5c32fcee83abbd516ceb9af5f0e8b6aad8d26a5", + "sigs": [ + { + "nonce": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", + "part": "8f07ddd5e9f5179cff19486034181ed76505baaad53e5d994064127b56c5841bdac2d36fe4c972de75f4e462004de9ca3e8c77d4dae5344d210beea9ad138c45", + "xs": "02e3c128e436510500616fef3f9a22b15ca015f407c8c5cf96c9059163c873828f" + } + ], + "sta": "I2", + "ver": "4:2" + } + } + } + # "# + # , 4, false, false, false, false); + ``` + */ + + fn process_invoice_tx( + &self, + token: Token, + slate: VersionedSlate, + args: InitTxArgs, + ) -> Result; + + /** + Networked version of [Owner::tx_lock_outputs](struct.Owner.html#method.tx_lock_outputs). + + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "tx_lock_outputs", + "id": 1, + "params": { + "token": "d202964900000000d302964900000000d402964900000000d502964900000000", + "slate": { + "ver": "4:2", + "id": "0436430c-2b02-624c-2032-570501212b00", + "sta": "S1", + "off": "d202964900000000d302964900000000d402964900000000d502964900000000", + "amt": "60000000000", + "fee": "7000000", + "sigs": [ + { + "xs": "030152d2d72e2dba7c6086ad49a219d9ff0dfe0fd993dcaea22e058c210033ce93", + "nonce": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f" + } + ] + } + } + } + # "# + # , + # r#" + { + "jsonrpc": "2.0", + "id": 1, + "result": { + "Ok": null + } + } + # "# + # , 5 ,true, false, false, false); + + ``` + */ + fn tx_lock_outputs(&self, token: Token, slate: VersionedSlate) -> Result<(), Error>; + + /** + Networked version of [Owner::finalize_tx](struct.Owner.html#method.finalize_tx). + + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "finalize_tx", + "id": 1, + "params": { + "token": "d202964900000000d302964900000000d402964900000000d502964900000000", + "slate": + { + "ver": "4:2", + "id": "0436430c-2b02-624c-2032-570501212b00", + "sta": "S2", + "off": "6c6a69136154775488782121887bb3c32787a8320551fdb9732ec2d333fe54ee", + "sigs": [ + { + "xs": "02e3c128e436510500616fef3f9a22b15ca015f407c8c5cf96c9059163c873828f", + "nonce": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", + "part": "8f07ddd5e9f5179cff19486034181ed76505baaad53e5d994064127b56c5841be7bf31d80494f5e4a3d656649b1610c61a268f9cafcfc604b5d9f25efb2aa3c5" + } + ], + "coms": [ + { + "c": "099b48cfb1f80a2347dc89818449e68e76a3c6817a532a8e9ef2b4a5ccf4363850", + "p": "29701ceae262cac77b79b868c883a292e61e6de8192b868edcd1300b0973d91396b156ace6bd673402a303de10ddd8a5e6b7f17ba6557a574a672bd04cc273ab04ed8e2ca80bac483345c0ec843f521814ce1301ec9adc38956a12b4d948acce71295a4f52bcdeb8a1c9f2d6b2da5d731262a5e9c0276ef904df9ef8d48001420cd59f75a2f1ae5c7a1c7c6b9f140e7613e52ef9e249f29f9340b7efb80699e460164324616f98fd4cde3db52497c919e95222fffeacb7e65deca7e368a80ce713c19de7da5369726228ee336f5bd494538c12ccbffeb1b9bfd5fc8906d1c64245b516f103fa96d9c56975837652c1e0fa5803d7ccf1147d8f927e36da717f7ad79471dbe192f5f50f87a79fc3fe030dba569b634b92d2cf307993cce545633af263897cd7e6ebf4dcafb176d07358bdc38d03e45a49dfa9c8c6517cd68d167ffbf6c3b4de0e2dd21909cbad4c467b84e5700be473a39ac59c669d7c155c4bcab9b8026eea3431c779cd277e4922d2b9742e1f6678cbe869ec3b5b7ef4132ddb6cdd06cf27dbeb28be72b949fa897610e48e3a0d789fd2eea75abc97b3dc7e00e5c8b3d24e40c6f24112adb72352b89a2bef0599345338e9e76202a3c46efa6370952b2aca41aadbae0ea32531acafcdab6dd066d769ebf50cf4f3c0a59d2d5fa79600a207b9417c623f76ad05e8cccfcd4038f9448bc40f127ca7c0d372e46074e334fe49f5a956ec0056f4da601e6af80eb1a6c4951054869e665b296d8c14f344ca2dc5fdd5df4a3652536365a1615ad9b422165c77bf8fe65a835c8e0c41e070014eb66ef8c525204e990b3a3d663c1e42221b496895c37a2f0c1bf05e91235409c3fe3d89a9a79d6c78609ab18a463311911f71fa37bb73b15fcd38143d1404fd2ce81004dc7ff89cf1115dcc0c35ce1c1bf9941586fb959770f2618ccb7118a7" + } + ] + } + } + } + # "# + # , + # r#" + { + "jsonrpc": "2.0", + "id": 1, + "result": { + "Ok": { + "coms": [ + { + "c": "087df32304c5d4ae8b2af0bc31e700019d722910ef87dd4eec3197b80b207e3045", + "f": 1 + }, + { + "c": "08e1da9e6dc4d6e808a718b2f110a991dd775d65ce5ae408a4e1f002a4961aa9e7", + "f": 1 + }, + { + "c": "099b48cfb1f80a2347dc89818449e68e76a3c6817a532a8e9ef2b4a5ccf4363850", + "p": "29701ceae262cac77b79b868c883a292e61e6de8192b868edcd1300b0973d91396b156ace6bd673402a303de10ddd8a5e6b7f17ba6557a574a672bd04cc273ab04ed8e2ca80bac483345c0ec843f521814ce1301ec9adc38956a12b4d948acce71295a4f52bcdeb8a1c9f2d6b2da5d731262a5e9c0276ef904df9ef8d48001420cd59f75a2f1ae5c7a1c7c6b9f140e7613e52ef9e249f29f9340b7efb80699e460164324616f98fd4cde3db52497c919e95222fffeacb7e65deca7e368a80ce713c19de7da5369726228ee336f5bd494538c12ccbffeb1b9bfd5fc8906d1c64245b516f103fa96d9c56975837652c1e0fa5803d7ccf1147d8f927e36da717f7ad79471dbe192f5f50f87a79fc3fe030dba569b634b92d2cf307993cce545633af263897cd7e6ebf4dcafb176d07358bdc38d03e45a49dfa9c8c6517cd68d167ffbf6c3b4de0e2dd21909cbad4c467b84e5700be473a39ac59c669d7c155c4bcab9b8026eea3431c779cd277e4922d2b9742e1f6678cbe869ec3b5b7ef4132ddb6cdd06cf27dbeb28be72b949fa897610e48e3a0d789fd2eea75abc97b3dc7e00e5c8b3d24e40c6f24112adb72352b89a2bef0599345338e9e76202a3c46efa6370952b2aca41aadbae0ea32531acafcdab6dd066d769ebf50cf4f3c0a59d2d5fa79600a207b9417c623f76ad05e8cccfcd4038f9448bc40f127ca7c0d372e46074e334fe49f5a956ec0056f4da601e6af80eb1a6c4951054869e665b296d8c14f344ca2dc5fdd5df4a3652536365a1615ad9b422165c77bf8fe65a835c8e0c41e070014eb66ef8c525204e990b3a3d663c1e42221b496895c37a2f0c1bf05e91235409c3fe3d89a9a79d6c78609ab18a463311911f71fa37bb73b15fcd38143d1404fd2ce81004dc7ff89cf1115dcc0c35ce1c1bf9941586fb959770f2618ccb7118a7" + }, + { + "c": "09ede20409d5ae0d1c0d3f3d2c68038a384cdd6b7cc5ca2aab670f570adc2dffc3", + "p": "6d86fe00220f8c6ac2ad4e338d80063dba5423af525bd273ecfac8ef6b509192732a8cd0c53d3313e663ac5ccece3d589fd2634e29f96e82b99ca6f8b953645a005d1bc73493f8c41f84fb8e327d4cbe6711dba194a60db30700df94a41e1fda7afe0619169389f8d8ee12bddf736c4bc86cd5b1809a5a27f195209147dc38d0de6f6710ce9350f3b8e7e6820bfe5182e6e58f0b41b82b6ec6bb01ffe1d8b3c2368ebf1e31dfdb9e00f0bc68d9119a38d19c038c29c7b37e31246e7bba56019bc88881d7d695d32557fc0e93635b5f24deffefc787787144e5de7e86281e79934e7e20d9408c34317c778e6b218ee26d0a5e56b8b84a883e3ddf8603826010234531281486454f8c2cf3fee074f242f9fc1da3c6636b86fb6f941eb8b633d6e3b3f87dfe5ae261a40190bd4636f433bcdd5e3400255594e282c5396db8999d95be08a35be9a8f70fdb7cf5353b90584523daee6e27e208b2ca0e5758b8a24b974dca00bab162505a2aa4bcefd8320f111240b62f861261f0ce9b35979f9f92da7dd6989fe1f41ec46049fd514d9142ce23755f52ec7e64df2af33579e9b8356171b91bc96b875511bef6062dd59ef3fe2ddcc152147554405b12c7c5231513405eb062aa8fa093e3414a144c544d551c4f1f9bf5d5d2ff5b50a3f296c800907704bed8d8ee948c0855eff65ad44413af641cdc68a06a7c855be7ed7dd64d5f623bbc9645763d48774ba2258240a83f8f89ef84d21c65bcb75895ebca08b0090b40aafb7ddef039fcaf4bad2dbbac72336c4412c600e854d368ed775597c15d2e66775ab47024ce7e62fd31bf90b183149990c10b5b678501dbac1af8b2897b67d085d87cab7af4036cba3bdcfdcc7548d7710511045813c6818d859e192e03adc0d6a6b30c4cbac20a0d6f8719c7a9c3ad46d62eec464c4c44b58fca463fea3ce1fc51" + } + ], + "fee": "23500000", + "id": "0436430c-2b02-624c-2032-570501212b00", + "off": "a5a632f26f27a9b71e98c1c8b8098bb41204ffcfd206d995f9c16d10764ad95a", + "sigs": [ + { + "nonce": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", + "part": "8f07ddd5e9f5179cff19486034181ed76505baaad53e5d994064127b56c5841be7bf31d80494f5e4a3d656649b1610c61a268f9cafcfc604b5d9f25efb2aa3c5", + "xs": "02e3c128e436510500616fef3f9a22b15ca015f407c8c5cf96c9059163c873828f" + }, + { + "nonce": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", + "part": "8f07ddd5e9f5179cff19486034181ed76505baaad53e5d994064127b56c5841b04e1e15ceb1b5dbab8baf7750d7bd4aad6cfe97b83e4dc080dae328eb75881fd", + "xs": "02e89cce4499ac1e9bb498dab9e3fab93cc40cd3d26c04a0292e00f4bf272499ec" + } + ], + "sta": "S3", + "ver": "4:2" + } + } + } + # "# + # , 5, true, true, false, false); + ``` + */ + fn finalize_tx(&self, token: Token, slate: VersionedSlate) -> Result; + + /** + Networked version of [Owner::post_tx](struct.Owner.html#method.post_tx). + + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "id": 1, + "method": "post_tx", + "params": { + "token": "d202964900000000d302964900000000d402964900000000d502964900000000", + "slate": { + "ver": "4:2", + "id": "0436430c-2b02-624c-2032-570501212b00", + "sta": "S3", + "off": "750dbf4fd43b7f4cfd68d2698a522f3ff6e6a00ad9895b33f1ec46493b837b49", + "fee": "23500000", + "sigs": [ + { + "xs": "033bbe2a419ea2e9d6810a8d66552e709d1783ca50759a44dbaf63fc79c0164c4c", + "nonce": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", + "part": "8f07ddd5e9f5179cff19486034181ed76505baaad53e5d994064127b56c5841b92c7c53280dd79f8b028cd9863bac89820267cac794b121e217541efb061ad53" + }, + { + "xs": "02b57c1f4fea69a3ee070309cf8f06082022fe06f25a9be1851b56ef0fa18f25d6", + "nonce": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", + "part": "8f07ddd5e9f5179cff19486034181ed76505baaad53e5d994064127b56c5841b4cd4afef1cd2d708100cd1680d6566e4e987ac5c939ace9c0e036a679121c7a8" + } + ], + "coms": [ + { + "f": 1, + "c": "087df32304c5d4ae8b2af0bc31e700019d722910ef87dd4eec3197b80b207e3045" + }, + { + "f": 1, + "c": "08e1da9e6dc4d6e808a718b2f110a991dd775d65ce5ae408a4e1f002a4961aa9e7" + }, + { + "c": "099b48cfb1f80a2347dc89818449e68e76a3c6817a532a8e9ef2b4a5ccf4363850", + "p": "29701ceae262cac77b79b868c883a292e61e6de8192b868edcd1300b0973d91396b156ace6bd673402a303de10ddd8a5e6b7f17ba6557a574a672bd04cc273ab04ed8e2ca80bac483345c0ec843f521814ce1301ec9adc38956a12b4d948acce71295a4f52bcdeb8a1c9f2d6b2da5d731262a5e9c0276ef904df9ef8d48001420cd59f75a2f1ae5c7a1c7c6b9f140e7613e52ef9e249f29f9340b7efb80699e460164324616f98fd4cde3db52497c919e95222fffeacb7e65deca7e368a80ce713c19de7da5369726228ee336f5bd494538c12ccbffeb1b9bfd5fc8906d1c64245b516f103fa96d9c56975837652c1e0fa5803d7ccf1147d8f927e36da717f7ad79471dbe192f5f50f87a79fc3fe030dba569b634b92d2cf307993cce545633af263897cd7e6ebf4dcafb176d07358bdc38d03e45a49dfa9c8c6517cd68d167ffbf6c3b4de0e2dd21909cbad4c467b84e5700be473a39ac59c669d7c155c4bcab9b8026eea3431c779cd277e4922d2b9742e1f6678cbe869ec3b5b7ef4132ddb6cdd06cf27dbeb28be72b949fa897610e48e3a0d789fd2eea75abc97b3dc7e00e5c8b3d24e40c6f24112adb72352b89a2bef0599345338e9e76202a3c46efa6370952b2aca41aadbae0ea32531acafcdab6dd066d769ebf50cf4f3c0a59d2d5fa79600a207b9417c623f76ad05e8cccfcd4038f9448bc40f127ca7c0d372e46074e334fe49f5a956ec0056f4da601e6af80eb1a6c4951054869e665b296d8c14f344ca2dc5fdd5df4a3652536365a1615ad9b422165c77bf8fe65a835c8e0c41e070014eb66ef8c525204e990b3a3d663c1e42221b496895c37a2f0c1bf05e91235409c3fe3d89a9a79d6c78609ab18a463311911f71fa37bb73b15fcd38143d1404fd2ce81004dc7ff89cf1115dcc0c35ce1c1bf9941586fb959770f2618ccb7118a7" + }, + { + "c": "09ede20409d5ae0d1c0d3f3d2c68038a384cdd6b7cc5ca2aab670f570adc2dffc3", + "p": "6d86fe00220f8c6ac2ad4e338d80063dba5423af525bd273ecfac8ef6b509192732a8cd0c53d3313e663ac5ccece3d589fd2634e29f96e82b99ca6f8b953645a005d1bc73493f8c41f84fb8e327d4cbe6711dba194a60db30700df94a41e1fda7afe0619169389f8d8ee12bddf736c4bc86cd5b1809a5a27f195209147dc38d0de6f6710ce9350f3b8e7e6820bfe5182e6e58f0b41b82b6ec6bb01ffe1d8b3c2368ebf1e31dfdb9e00f0bc68d9119a38d19c038c29c7b37e31246e7bba56019bc88881d7d695d32557fc0e93635b5f24deffefc787787144e5de7e86281e79934e7e20d9408c34317c778e6b218ee26d0a5e56b8b84a883e3ddf8603826010234531281486454f8c2cf3fee074f242f9fc1da3c6636b86fb6f941eb8b633d6e3b3f87dfe5ae261a40190bd4636f433bcdd5e3400255594e282c5396db8999d95be08a35be9a8f70fdb7cf5353b90584523daee6e27e208b2ca0e5758b8a24b974dca00bab162505a2aa4bcefd8320f111240b62f861261f0ce9b35979f9f92da7dd6989fe1f41ec46049fd514d9142ce23755f52ec7e64df2af33579e9b8356171b91bc96b875511bef6062dd59ef3fe2ddcc152147554405b12c7c5231513405eb062aa8fa093e3414a144c544d551c4f1f9bf5d5d2ff5b50a3f296c800907704bed8d8ee948c0855eff65ad44413af641cdc68a06a7c855be7ed7dd64d5f623bbc9645763d48774ba2258240a83f8f89ef84d21c65bcb75895ebca08b0090b40aafb7ddef039fcaf4bad2dbbac72336c4412c600e854d368ed775597c15d2e66775ab47024ce7e62fd31bf90b183149990c10b5b678501dbac1af8b2897b67d085d87cab7af4036cba3bdcfdcc7548d7710511045813c6818d859e192e03adc0d6a6b30c4cbac20a0d6f8719c7a9c3ad46d62eec464c4c44b58fca463fea3ce1fc51" + } + ] + }, + "fluff": false + } + } + # "# + # , + # r#" + { + "id": 1, + "jsonrpc": "2.0", + "result": { + "Ok": null + } + } + # "# + # , 5, true, true, true, false); + ``` + */ + + fn post_tx(&self, token: Token, slate: VersionedSlate, fluff: bool) -> Result<(), Error>; + + /** + Networked version of [Owner::cancel_tx](struct.Owner.html#method.cancel_tx). + + + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "cancel_tx", + "params": { + "token": "d202964900000000d302964900000000d402964900000000d502964900000000", + "tx_id": null, + "tx_slate_id": "0436430c-2b02-624c-2032-570501212b00" + }, + "id": 1 + } + # "# + # , + # r#" + { + "id": 1, + "jsonrpc": "2.0", + "result": { + "Ok": null + } + } + # "# + # , 5, true, true, false, false); + ``` + */ + fn cancel_tx( + &self, + token: Token, + tx_id: Option, + tx_slate_id: Option, + ) -> Result<(), Error>; + + /** + Networked version of [Owner::get_stored_tx](struct.Owner.html#method.get_stored_tx). + + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "get_stored_tx", + "id": 1, + "params": { + "token": "d202964900000000d302964900000000d402964900000000d502964900000000", + "id": null, + "slate_id": "0436430c-2b02-624c-2032-570501212b00" + } + } + # "# + # , + # r#" + { + "jsonrpc": "2.0", + "id": 1, + "result": { + "Ok": { + "coms": [ + { + "c": "099b48cfb1f80a2347dc89818449e68e76a3c6817a532a8e9ef2b4a5ccf4363850", + "p": "29701ceae262cac77b79b868c883a292e61e6de8192b868edcd1300b0973d91396b156ace6bd673402a303de10ddd8a5e6b7f17ba6557a574a672bd04cc273ab04ed8e2ca80bac483345c0ec843f521814ce1301ec9adc38956a12b4d948acce71295a4f52bcdeb8a1c9f2d6b2da5d731262a5e9c0276ef904df9ef8d48001420cd59f75a2f1ae5c7a1c7c6b9f140e7613e52ef9e249f29f9340b7efb80699e460164324616f98fd4cde3db52497c919e95222fffeacb7e65deca7e368a80ce713c19de7da5369726228ee336f5bd494538c12ccbffeb1b9bfd5fc8906d1c64245b516f103fa96d9c56975837652c1e0fa5803d7ccf1147d8f927e36da717f7ad79471dbe192f5f50f87a79fc3fe030dba569b634b92d2cf307993cce545633af263897cd7e6ebf4dcafb176d07358bdc38d03e45a49dfa9c8c6517cd68d167ffbf6c3b4de0e2dd21909cbad4c467b84e5700be473a39ac59c669d7c155c4bcab9b8026eea3431c779cd277e4922d2b9742e1f6678cbe869ec3b5b7ef4132ddb6cdd06cf27dbeb28be72b949fa897610e48e3a0d789fd2eea75abc97b3dc7e00e5c8b3d24e40c6f24112adb72352b89a2bef0599345338e9e76202a3c46efa6370952b2aca41aadbae0ea32531acafcdab6dd066d769ebf50cf4f3c0a59d2d5fa79600a207b9417c623f76ad05e8cccfcd4038f9448bc40f127ca7c0d372e46074e334fe49f5a956ec0056f4da601e6af80eb1a6c4951054869e665b296d8c14f344ca2dc5fdd5df4a3652536365a1615ad9b422165c77bf8fe65a835c8e0c41e070014eb66ef8c525204e990b3a3d663c1e42221b496895c37a2f0c1bf05e91235409c3fe3d89a9a79d6c78609ab18a463311911f71fa37bb73b15fcd38143d1404fd2ce81004dc7ff89cf1115dcc0c35ce1c1bf9941586fb959770f2618ccb7118a7" + } + ], + "fee": "23500000", + "id": "0436430c-2b02-624c-2032-570501212b00", + "sigs": [], + "sta": "S3", + "ver": "4:3" + } + } + } + # "# + # , 5, true, true, false, false); + ``` + */ + fn get_stored_tx( + &self, + token: Token, + id: Option, + slate_id: Option, + ) -> Result, Error>; + + /** + Networked version of [Owner::get_rewind_hash](struct.Owner.html#method.get_rewind_hash). + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "get_rewind_hash", + "params": { + "token": "d202964900000000d302964900000000d402964900000000d502964900000000" + }, + "id": 1 + } + # "# + # , + # r#" + { + "id":1, + "jsonrpc":"2.0", + "result":{ + "Ok":"c820c52a492b7db511c752035483d0e50e8fd3ec62544f1b99638e220a4682de" + } + } + # "# + # , 0, false, false, false, false); + ``` + */ + fn get_rewind_hash(&self, token: Token) -> Result; + + /** + Networked version of [Owner::scan_rewind_hash](struct.Owner.html#method.scan_rewind_hash). + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "scan_rewind_hash", + "params": { + "rewind_hash": "c820c52a492b7db511c752035483d0e50e8fd3ec62544f1b99638e220a4682de", + "start_height": 1 + }, + "id": 1 + } + # "# + # , + # r#" + { + "id":1, + "jsonrpc":"2.0", + "result":{ + "Ok":{ + "last_pmmr_index":8, + "output_result":[ + { + "commit":"08e1da9e6dc4d6e808a718b2f110a991dd775d65ce5ae408a4e1f002a4961aa9e7", + "height":1, + "is_coinbase":true, + "lock_height":4, + "mmr_index":1, + "value":60000000000 + }, + { + "commit":"087df32304c5d4ae8b2af0bc31e700019d722910ef87dd4eec3197b80b207e3045", + "height":2, + "is_coinbase":true, + "lock_height":5, + "mmr_index":2, + "value":60000000000 + }, + { + "commit":"084219d64014223a205431acfa8f8cc3e8cb8c6d04df80b26713314becf83861c7", + "height":3, + "is_coinbase":true, + "lock_height":6, + "mmr_index":4, + "value":60000000000 + }, + { + "commit":"09c5efc4dab05d7d16fc90168c484c13f15a142ea4e1bf93c3fad12f5e8a402598", + "height":4, + "is_coinbase":true, + "lock_height":7, + "mmr_index":5, + "value":60000000000 + }, + { + "commit":"08fe198e525a5937d0c5d01fa354394d2679be6df5d42064a0f7550c332fce3d9d", + "height":5, + "is_coinbase":true, + "lock_height":8, + "mmr_index":8, + "value":60000000000 + } + ], + "rewind_hash":"c820c52a492b7db511c752035483d0e50e8fd3ec62544f1b99638e220a4682de", + "total_balance":300000000000 + } + } + } + # "# + # , 5, false, false, false, false); + ``` + */ + fn scan_rewind_hash( + &self, + rewind_hash: String, + start_height: Option, + ) -> Result; + + /** + Networked version of [Owner::scan](struct.Owner.html#method.scan). + + + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "scan", + "params": { + "token": "d202964900000000d302964900000000d402964900000000d502964900000000", + "start_height": 1, + "delete_unconfirmed": false + }, + "id": 1 + } + # "# + # , + # r#" + { + "id": 1, + "jsonrpc": "2.0", + "result": { + "Ok": null + } + } + # "# + # , 1, false, false, false, false); + ``` + */ + fn scan( + &self, + token: Token, + start_height: Option, + delete_unconfirmed: bool, + ) -> Result<(), Error>; + + /** + Networked version of [Owner::node_height](struct.Owner.html#method.node_height). + + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "node_height", + "params": { + "token": "d202964900000000d302964900000000d402964900000000d502964900000000" + }, + "id": 1 + } + # "# + # , + # r#" + { + "id": 1, + "jsonrpc": "2.0", + "result": { + "Ok": { + "header_hash": "d4b3d3c40695afd8c7760f8fc423565f7d41310b7a4e1c4a4a7950a66f16240d", + "height": "5", + "updated_from_node": true + } + } + } + # "# + # , 5, false, false, false, false); + ``` + */ + fn node_height(&self, token: Token) -> Result; + + /** + Initializes the secure JSON-RPC API. This function must be called and a shared key + established before any other OwnerAPI JSON-RPC function can be called. + + The shared key will be derived using ECDH with the provided public key on the secp256k1 curve. This + function will return its public key used in the derivation, which the caller should multiply by its + private key to derive the shared key. + + Once the key is established, all further requests and responses are encrypted and decrypted with the + following parameters: + * AES-256 in GCM mode with 128-bit tags and 96 bit nonces + * 12 byte nonce which must be included in each request/response to use on the decrypting side + * Empty vector for additional data + * Suffix length = AES-256 GCM mode tag length = 16 bytes + * + + Fully-formed JSON-RPC requests (as documented) should be encrypted using these parameters, encoded + into base64 and included with the one-time nonce in a request for the `encrypted_request_v3` method + as follows: + + ``` + # let s = r#" + { + "jsonrpc": "2.0", + "method": "encrypted_request_v3", + "id": "1", + "params": { + "nonce": "ef32...", + "body_enc": "e0bcd..." + } + } + # "#; + ``` + + With a typical response being: + + ``` + # let s = r#"{ + { + "jsonrpc": "2.0", + "method": "encrypted_response_v3", + "id": "1", + "Ok": { + "nonce": "340b...", + "body_enc": "3f09c..." + } + } + # }"#; + ``` + + */ + + fn init_secure_api(&self, ecdh_pubkey: ECDHPubkey) -> Result; + + /** + Networked version of [Owner::get_top_level_directory](struct.Owner.html#method.get_top_level_directory). + + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "get_top_level_directory", + "params": { + }, + "id": 1 + } + # "# + # , + # r#" + { + "id": 1, + "jsonrpc": "2.0", + "result": { + "Ok": "/doctest/dir" + } + } + # "# + # , 5, false, false, false, false); + ``` + */ + + fn get_top_level_directory(&self) -> Result; + + /** + Networked version of [Owner::set_top_level_directory](struct.Owner.html#method.set_top_level_directory). + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "set_top_level_directory", + "params": { + "dir": "/home/wallet_user/my_wallet_dir" + }, + "id": 1 + } + # "# + # , + # r#" + { + "id": 1, + "jsonrpc": "2.0", + "result": { + "Ok": null + } + } + # "# + # , 5, false, false, false, false); + ``` + */ + + fn set_top_level_directory(&self, dir: String) -> Result<(), Error>; + + /** + Networked version of [Owner::create_config](struct.Owner.html#method.create_config). + + Both the `wallet_config` and `logging_config` parameters can be `null`, the examples + below are for illustration. Note that the values provided for `log_file_path` and `data_file_dir` + will be ignored and replaced with the actual values based on the value of `get_top_level_directory` + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "create_config", + "params": { + "chain_type": "Mainnet", + "wallet_config": { + "chain_type": null, + "api_listen_interface": "127.0.0.1", + "api_listen_port": 3415, + "owner_api_listen_port": 3420, + "api_secret_path": null, + "node_api_secret_path": null, + "check_node_api_http_addr": "http://127.0.0.1:3413", + "owner_api_include_foreign": false, + "data_file_dir": "/path/to/data/file/dir", + "no_commit_cache": null, + "tls_certificate_file": null, + "tls_certificate_key": null, + "dark_background_color_scheme": null + }, + "logging_config": { + "log_to_stdout": false, + "stdout_log_level": "Info", + "log_to_file": true, + "file_log_level": "Debug", + "log_file_path": "/path/to/log/file", + "log_file_append": true, + "log_max_size": null, + "log_max_files": null, + "tui_running": null + }, + "tor_config" : { + "use_tor_listener": true, + "socks_proxy_addr": "127.0.0.1:9050", + "send_config_dir": "." + } + }, + "id": 1 + } + # "# + # , + # r#" + { + "id": 1, + "jsonrpc": "2.0", + "result": { + "Ok": null + } + } + # "# + # , 5, false, false, false, false); + ``` + */ + fn create_config( + &self, + chain_type: global::ChainTypes, + wallet_config: Option, + logging_config: Option, + tor_config: Option, + ) -> Result<(), Error>; + + /** + Networked version of [Owner::create_wallet](struct.Owner.html#method.create_wallet). + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "create_wallet", + "params": { + "name": null, + "mnemonic": null, + "mnemonic_length": 32, + "password": "my_secret_password" + }, + "id": 1 + } + # "# + # , + # r#" + { + "id": 1, + "jsonrpc": "2.0", + "result": { + "Ok": null + } + } + # "# + # , 0, false, false, false, false); + ``` + */ + + fn create_wallet( + &self, + name: Option, + mnemonic: Option, + mnemonic_length: u32, + password: String, + ) -> Result<(), Error>; + + /** + Networked version of [Owner::open_wallet](struct.Owner.html#method.open_wallet). + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "open_wallet", + "params": { + "name": null, + "password": "my_secret_password" + }, + "id": 1 + } + # "# + # , + # r#" + { + "id": 1, + "jsonrpc": "2.0", + "result": { + "Ok": "d096b3cb75986b3b13f80b8f5243a9edf0af4c74ac37578c5a12cfb5b59b1868" + } + } + # "# + # , 0, false, false, false, false); + ``` + */ + + fn open_wallet(&self, name: Option, password: String) -> Result; + + /** + Networked version of [Owner::close_wallet](struct.Owner.html#method.close_wallet). + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "close_wallet", + "params": { + "name": null + }, + "id": 1 + } + # "# + # , + # r#" + { + "id": 1, + "jsonrpc": "2.0", + "result": { + "Ok": null + } + } + # "# + # , 0, false, false, false, false); + ``` + */ + + fn close_wallet(&self, name: Option) -> Result<(), Error>; + + /** + Networked version of [Owner::get_mnemonic](struct.Owner.html#method.get_mnemonic). + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "get_mnemonic", + "params": { + "name": null, + "password": "" + }, + "id": 1 + } + # "# + # , + # r#" + { + "id": 1, + "jsonrpc": "2.0", + "result": { + "Ok": "fat twenty mean degree forget shell check candy immense awful flame next during february bulb bike sun wink theory day kiwi embrace peace lunch" + } + } + # "# + # , 0, false, false, false, false); + ``` + */ + + fn get_mnemonic(&self, name: Option, password: String) -> Result; + + /** + Networked version of [Owner::change_password](struct.Owner.html#method.change_password). + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "change_password", + "params": { + "name": null, + "old": "", + "new": "new_password" + }, + "id": 1 + } + # "# + # , + # r#" + { + "id": 1, + "jsonrpc": "2.0", + "result": { + "Ok": null + } + } + # "# + # , 0, false, false, false, false); + ``` + */ + fn change_password(&self, name: Option, old: String, new: String) -> Result<(), Error>; + + /** + Networked version of [Owner::delete_wallet](struct.Owner.html#method.delete_wallet). + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "delete_wallet", + "params": { + "name": null + }, + "id": 1 + } + # "# + # , + # r#" + { + "id": 1, + "jsonrpc": "2.0", + "result": { + "Ok": null + } + } + # "# + # , 0, false, false, false, false); + ``` + */ + fn delete_wallet(&self, name: Option) -> Result<(), Error>; + + /** + Networked version of [Owner::start_updated](struct.Owner.html#method.start_updater). + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "start_updater", + "params": { + "token": "d202964900000000d302964900000000d402964900000000d502964900000000", + "frequency": 30000 + }, + "id": 1 + } + # "# + # , + # r#" + { + "id": 1, + "jsonrpc": "2.0", + "result": { + "Ok": null + } + } + # "# + # , 0, false, false, false, false); + ``` + */ + + fn start_updater(&self, token: Token, frequency: u32) -> Result<(), Error>; + + /** + Networked version of [Owner::stop_updater](struct.Owner.html#method.stop_updater). + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "stop_updater", + "params": null, + "id": 1 + } + # "# + # , + # r#" + { + "id": 1, + "jsonrpc": "2.0", + "result": { + "Ok": null + } + } + # "# + # , 0, false, false, false, false); + ``` + */ + fn stop_updater(&self) -> Result<(), Error>; + + /** + Networked version of [Owner::get_updater_messages](struct.Owner.html#method.get_updater_messages). + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "get_updater_messages", + "params": { + "count": 1 + }, + "id": 1 + } + # "# + # , + # r#" + { + "id": 1, + "jsonrpc": "2.0", + "result": { + "Ok": [] + } + } + # "# + # , 0, false, false, false, false); + ``` + */ + + fn get_updater_messages(&self, count: u32) -> Result, Error>; + + /** + Networked version of [Owner::get_slatepack_address](struct.Owner.html#method.get_slatepack_address). + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "get_slatepack_address", + "params": { + "token": "d202964900000000d302964900000000d402964900000000d502964900000000", + "derivation_index": 0 + }, + "id": 1 + } + # "# + # , + # r#" + { + "id": 1, + "jsonrpc": "2.0", + "result": { + "Ok": "tgrin1xtxavwfgs48ckf3gk8wwgcndmn0nt4tvkl8a7ltyejjcy2mc6nfs9gm2lp" + } + } + # "# + # , 0, false, false, false, false); + ``` + */ + + fn get_slatepack_address( + &self, + token: Token, + derivation_index: u32, + ) -> Result; + + /** + Networked version of [Owner::get_slatepack_secret_key](struct.Owner.html#method.get_slatepack_secret_key). + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "get_slatepack_secret_key", + "params": { + "token": "d202964900000000d302964900000000d402964900000000d502964900000000", + "derivation_index": 0 + }, + "id": 1 + } + # "# + # , + # r#" + { + "id": 1, + "jsonrpc": "2.0", + "result": { + "Ok": "86cca2aedea7989dfcca62e54477301d098bac260656d11373e314c099f0b26f" + } + } + # "# + # , 0, false, false, false, false); + ``` + */ + + fn get_slatepack_secret_key( + &self, + token: Token, + derivation_index: u32, + ) -> Result; + + /** + Networked version of [Owner::create_slatepack_message](struct.Owner.html#method.create_slatepack_message). + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "create_slatepack_message", + "params": { + "token": "d202964900000000d302964900000000d402964900000000d502964900000000", + "sender_index": 0, + "recipients": [], + "slate": { + "ver": "4:2", + "id": "0436430c-2b02-624c-2032-570501212b00", + "sta": "S1", + "off": "d202964900000000d302964900000000d402964900000000d502964900000000", + "amt": "60000000000", + "fee": "7000000", + "sigs": [ + { + "xs": "030152d2d72e2dba7c6086ad49a219d9ff0dfe0fd993dcaea22e058c210033ce93", + "nonce": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f" + } + ] + } + }, + "id": 1 + } + # "# + # , + # r#" + { + "id": 1, + "jsonrpc": "2.0", + "result": { + "Ok": "BEGINSLATEPACK. xyfzdULuUuM5r3R kS68aywyCuYssPs Jf1JbvnBcK6NDDo ajiGAgh2SPx4t49 xtKuJE3BZCcSEue ksecMmbSoV2DQbX gGcmJniP9UadcmR N1KSc5FBhwAaUjy LXeYDP7EV7Cmsj4 pLaJdZTJTQbccUH 2zG8QTgoEiEWP5V T6rKst1TibmDAFm RRVHYDtskdYJb5G krqfpgN7RjvPfpm Z5ZFyz6ipAt5q9T 2HCjrTxkHdVi9js 22tr2Lx6iXT5vm8 JL6HhjwyFrSaEmN AjsBE8jgiaAABA6 GGZKwcXeXToMfRt nL9DeX1. ENDSLATEPACK." + } + } + # "# + # , 0, false, false, false, false); + ``` + */ + + fn create_slatepack_message( + &self, + token: Token, + slate: VersionedSlate, + sender_index: Option, + recipients: Vec, + ) -> Result; + + /** + Networked version of [Owner::slate_from_slatepack_message](struct.Owner.html#method.slate_from_slatepack_message). + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "slate_from_slatepack_message", + "params": { + "token": "d202964900000000d302964900000000d402964900000000d502964900000000", + "secret_indices": [0], + "message": "BEGINSLATEPACK. 8GQrdcwdLKJD28F 3a9siP7ZhZgAh7w BR2EiZHza5WMWmZ Cc8zBUemrrYRjhq j3VBwA8vYnvXXKU BDmQBN2yKgmR8mX UzvXHezfznA61d7 qFZYChhz94vd8Ew NEPLz7jmcVN2C3w wrfHbeiLubYozP2 uhLouFiYRrbe3fQ 4uhWGfT3sQYXScT dAeo29EaZJpfauh j8VL5jsxST2SPHq nzXFC2w9yYVjt7D ju7GSgHEp5aHz9R xstGbHjbsb4JQod kYLuELta1ohUwDD pvjhyJmsbLcsPei k5AQhZsJ8RJGBtY bou6cU7tZeFJvor 4LB9CBfFB3pmVWD vSLd5RPS75dcnHP nbXD8mSDZ8hJS2Q A9wgvppWzuWztJ2 dLUU8f9tLJgsRBw YZAs71HiVeg7. ENDSLATEPACK." + }, + "id": 1 + } + # "# + # , + # r#" + { + "id": 1, + "jsonrpc": "2.0", + "result": { + "Ok": { + "amt": "6000000000", + "fee": "8000000", + "id": "0436430c-2b02-624c-2032-570501212b00", + "off": "d202964900000000d302964900000000d402964900000000d502964900000000", + "proof": { + "raddr": "783f6528669742a990e0faf0a5fca5d5b3330e37bbb9cd5c628696d03ce4e810", + "saddr": "32cdd63928854f8b2628b1dce4626ddcdf35d56cb7cfdf7d64cca5822b78d4d3" + }, + "sigs": [ + { + "nonce": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", + "xs": "023878ce845727f3a4ec76ca3f3db4b38a2d05d636b8c3632108b857fed63c96de" + } + ], + "sta": "S1", + "ver": "4:2" + } + } + } + # "# + # , 0, false, false, false, false); + ``` + */ + + fn slate_from_slatepack_message( + &self, + token: Token, + message: String, + secret_indices: Vec, + ) -> Result; + + /** + Networked version of [Owner::decode_slatepack_message](struct.Owner.html#method.decode_slatepack_message). + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "decode_slatepack_message", + "params": { + "token": "d202964900000000d302964900000000d402964900000000d502964900000000", + "secret_indices": [0], + "message": "BEGINSLATEPACK. t9EcGgrKr1GFCQB SK2jPCxME6Hgpqx bntpQm3zKFycoPY nW4UeoL4KQ7ExNK At6EQsvpz6MjUs8 6WG8KHEbMfqufJQ ZJTw2gkcdJmJjiJ f29oGgYqqXDZox4 ujPSjrtoxCN4h3e i1sZ8dYsm3dPeXL 7VQLsYNjAefciqj ZJXPm4Pqd7VDdd4 okGBGBu3YJvYzT6 arAxeCEx66us31h AJLcDweFwyWBkW5 J1DLiYAjt5ftFTo CjpfW9KjiLq2LM5 jepXWEHJPSDAYVK 4macDZUhRbJiG6E hrQcPrJBVC716mb Hw5E1PFrE6on5wq oEmrS4j9vaB5nw8 Z9ZyXvPc2LN7tER yt6pSHZeY9EpYdY zv4bthzfRfF8ePT TMeMpV2gpgyRXQa CPD2TR. ENDSLATEPACK." + }, + "id": 1 + } + # "# + # , + # r#" + { + "id": 1, + "jsonrpc": "2.0", + "result": { + "Ok": { + "mode": 0, + "payload": "AAQAAgQ2QwwrAmJMIDJXBQEhKwAB0gKWSQAAAADTApZJAAAAANQClkkAAAAA1QKWSQAAAAAGAAAAAWWgvAAAAAAAAHoSAAEAAjh4zoRXJ/Ok7HbKPz20s4otBdY2uMNjIQi4V/7WPJbeAxuExVZ7EmRAmV0+1aq6BWXXHhg0YEgZ/5wX9enV3QePAjLN1jkohU+LJiix3ORibdzfNdVst8/ffWTMpYIreNTTeD9lKGaXQqmQ4Prwpfyl1bMzDje7uc1cYoaW0Dzk6BAA", + "sender": "tgrin1xtxavwfgs48ckf3gk8wwgcndmn0nt4tvkl8a7ltyejjcy2mc6nfs9gm2lp", + "slatepack": "1.0" + } + } + } + # "# + # , 0, false, false, false, false); + ``` + */ + + fn decode_slatepack_message( + &self, + token: Token, + message: String, + secret_indices: Vec, + ) -> Result; + + /** + Networked version of [Owner::retrieve_payment_proof](struct.Owner.html#method.retrieve_payment_proof). + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "retrieve_payment_proof", + "params": { + "token": "d202964900000000d302964900000000d402964900000000d502964900000000", + "refresh_from_node": true, + "tx_id": null, + "tx_slate_id": "0436430c-2b02-624c-2032-570501212b00" + }, + "id": 1 + } + # "# + # , + # r#" + { + "id": 1, + "jsonrpc": "2.0", + "result": { + "Ok": { + "amount": "60000000000", + "excess": "09eac5f5872fa5e08e0c29fd900f1b8f77ff3ad1d0d1c46aeb202cbf92363fe0af", + "recipient_address": "tgrin10qlk22rxjap2ny8qltc2tl996kenxr3hhwuu6hrzs6tdq08yaqgqq6t83r", + "recipient_sig": "02868f2d2b983981f8f98043701687a8531ed2de564ea3df48e9e7e0229ccbe8359efe506896df2efbe3528e977252c50e4a41ca3cc9896e7c5a30bbb1d33604", + "sender_address": "tgrin1xtxavwfgs48ckf3gk8wwgcndmn0nt4tvkl8a7ltyejjcy2mc6nfs9gm2lp", + "sender_sig": "c511764f3f61ed3d1cbca9514df8bc6811fad5662b1cb0e0587b9c9e49db9f33183cce71af6cb24b507fabf525a2bc405c6e84e63a60334edff0b451ae5e6102" + } + } + } + # "# + # , 5, true, true, true, true); + ``` + */ + + fn retrieve_payment_proof( + &self, + token: Token, + refresh_from_node: bool, + tx_id: Option, + tx_slate_id: Option, + ) -> Result; + + /** + Networked version of [Owner::verify_payment_proof](struct.Owner.html#method.verify_payment_proof). + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "verify_payment_proof", + "params": { + "token": "d202964900000000d302964900000000d402964900000000d502964900000000", + "proof": { + "amount": "60000000000", + "excess": "09eac5f5872fa5e08e0c29fd900f1b8f77ff3ad1d0d1c46aeb202cbf92363fe0af", + "recipient_address": "slatepack10qlk22rxjap2ny8qltc2tl996kenxr3hhwuu6hrzs6tdq08yaqgqnlumr7", + "recipient_sig": "02868f2d2b983981f8f98043701687a8531ed2de564ea3df48e9e7e0229ccbe8359efe506896df2efbe3528e977252c50e4a41ca3cc9896e7c5a30bbb1d33604", + "sender_address": "slatepack1xtxavwfgs48ckf3gk8wwgcndmn0nt4tvkl8a7ltyejjcy2mc6nfskdvkdu", + "sender_sig": "c511764f3f61ed3d1cbca9514df8bc6811fad5662b1cb0e0587b9c9e49db9f33183cce71af6cb24b507fabf525a2bc405c6e84e63a60334edff0b451ae5e6102" + } + }, + "id": 1 + } + # "# + # , + # r#" + { + "id": 1, + "jsonrpc": "2.0", + "result": { + "Ok": [ + true, + false + ] + } + } + # "# + # , 5, true, true, true, true); + ``` + */ + + fn verify_payment_proof( + &self, + token: Token, + proof: PaymentProof, + ) -> Result<(bool, bool), Error>; + + /** + Networked version of [Owner::set_tor_config](struct.Owner.html#method.set_tor_config). + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "set_tor_config", + "params": { + "tor_config": { + "use_tor_listener": true, + "socks_proxy_addr": "127.0.0.1:59050", + "send_config_dir": "." + } + }, + "id": 1 + } + # "# + # , + # r#" + { + "id": 1, + "jsonrpc": "2.0", + "result": { + "Ok": null + } + } + # "# + # , 0, false, false, false, false); + ``` + */ + fn set_tor_config(&self, tor_config: Option) -> Result<(), Error>; + + /** + Networked version of [Owner::build_output](struct.Owner.html#method.build_output). + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "build_output", + "params": { + "token": "d202964900000000d302964900000000d402964900000000d502964900000000", + "features": "Plain", + "amount": "60000000000" + }, + "id": 1 + } + # "# + # , + # r#" + { + "id": 1, + "jsonrpc": "2.0", + "result": { + "Ok": { + "blind": "089705aa74b638ee391e295d227c534a50dd58e603bca97a4404747cf8a5a189", + "key_id": "0300000000000000000000000000000000", + "output": { + "commit": "08e1da9e6dc4d6e808a718b2f110a991dd775d65ce5ae408a4e1f002a4961aa9e7", + "features": "Plain", + "proof": "4b5d6fb1b4d143fc50c83aef61c5410be760a395ed71f3424f7746bf5ee0539ae299569d99b73ea6583b1057834551faa0ac8cfe34c75431b86d6f37dec1ff070fc01f44babf0d3446781564ff7a143242ea67cb4ff7b11fe399735695c3fe70b40b71f31b04cf73b1d1f3430fb53a8c9f990fae48c09b42f8212d60a2d3ce0b8ea4dc0d37a82c3f328162ab8d50f48c28cb9a721a87a40aa3915bf9fffc0cd820e15b758e8565ad7fbf22d03711dc83f98e7c9f955d9398a1c75bc96df2ee64751592953cced38527b3f68282d2ca2fdf2994fbd93a1642fb9d265d57c3cf7df01501da569f2b4e606a1c3084c807a39947a3e1fd41b0647891e1f64842a2b98e694b93857e30691e0b0bca7bc49dec9d6af1003a40b3431ae0bcae8454a438523d066dcac4f194d8370c5ba6567830f302e1ec2607b8d1720bb6c6c57c549f1a3ef7ad2b54dfdd0178329e0723b8a55b438a1e43a984c072d6505aa5e193042d9703484c8383e78d9553684fad5e399f11f8ae6577e4ac4e3c2478e3fd8df0164600b4816b2167c2bf5b9fd7dd29cc1041fccbf1392240fd7c1dc39dd1ebc86b882a383dfe683e9f029d40b2829e3bf56b9760e1d81b7ad4a9066b1c01ccbea6b196154443cacedaccd5ff4fd25cbd9a8f0d271d5688bbe4b956fd34d3413d0478ac9400f6f1ff3890dea10be072d2d48bfa69a6e1e1b6fffaa9db4663eb1ecc26da331072877eb6d4a05a41584d44ed5d2a96a98727563bf180768940c99a15e9183ae927f47f2c0e13d9c00d7ebf0dacb1b6c139d3e18701d10c9d1ef300eeeab756eaa4584c3f5fb42793f7c2517601ae31d887c177eec8bce35c0aa16ba6991fd885deb9ff7b44ffd489f8e9e9d0717141501143c027d33e8a4baf6d85c859ff8a04d1aafbb3d1a97dc6c8ee3642ec41b8e43a137b43c8e60d69a6f19eb9749e" + } + } + } + } + # "# + # , 0, false, false, false, false); + ``` + */ + fn build_output( + &self, + token: Token, + features: OutputFeatures, + amount: Amount, + ) -> Result; + + /** + Networked version of [Owner::build_output](struct.Owner.html#method.create_mwixnet_req). + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "create_mwixnet_req", + "params": { + "token": "d202964900000000d302964900000000d402964900000000d502964900000000", + "commitment": "08e1da9e6dc4d6e808a718b2f110a991dd775d65ce5ae408a4e1f002a4961aa9e7", + "fee_per_hop": "5000000", + "lock_output": true, + "server_keys": [ + "97444ae673bb92c713c1a2f7b8882ffbfc1c67401a280a775dce1a8651584332", + "0c9414341f2140ed34a5a12a6479bf5a6404820d001ab81d9d3e8cc38f049b4e", + "b58ece97d60e71bb7e53218400b0d67bfe6a3cb7d3b4a67a44f8fb7c525cbca5" + ] + }, + "id": 1 + } + # "# + # , + # r#" + { + "id": 1, + "jsonrpc": "2.0", + "result": { + "Ok": { + "comsig": "099561ed0be59f6502ee358ee4f6760cd16d6be04d58d7a2c1bf2fd09dd7fd2d291beaae5483c6f18d1ceaae6321f06f9ba129a1ee9e7d15f152c67397a621538b5c10bbeb95140dee815c02657c91152939afe389458dc59af095e8e8e5c81a08", + "onion": { + "commit": "08e1da9e6dc4d6e808a718b2f110a991dd775d65ce5ae408a4e1f002a4961aa9e7", + "data": [ + "37f68116475e1aa6b58fc911addbd0e04e7aa19ab3e82e7b5cfcaf57d82cf35e7388ce51711cc5ef8cf7630f7dc7229878f91c7ec85991a7fc0051a7bbc66569db3a3aa89ef490055f3c", + "b9ff8c0c1699808efce46d581647c65764a28e813023ae677d688282422a07505ae1a051037d7ba58f3279846d0300800fc1c5bfcc548dab815e9fd2f29df9515170c41fa6e4e44b8bcb", + "62ea6b8369686a0415e1e752b9b4d6e66cf5b6066a2d3c60d8818890a55f3adff4601466f4c6e6b646568b99ae93549a3595b7a7b4be815ced87d9297cabbd69518d7b2ed6edd14007528fd346aaea765a1165fe886666627ebcab9588b8ee1c9e98395ae67913c48eb6e924581b40182fce807f97312fb07fd5e216d99941f2b488babce4078a50cd66b28b30a66c4f54fcc127437408a99b30ffd6c3d0d8c7d39e864fc04e321b8c10138c8852d4cad0a4f2780412b9dadcc6e0f2657b7803a81bccb809ca392464be2e01755be7377d0e815698ad6ea51d4617cc92c3ccf852f038e33cc9c90992438ba5c49cca7cc188b682da684e2f4c9733a84a7b64ac5c2216ebf5926f0ee67b664fb5bab799109cbee755ce1aebc8cd352fea51cd84c333cb958093c53544c3f3ab05dba64d8f041c3b179796b476ec04b11044e39db6994ab767315e52cc0ef023432ec88ade2911612db7e74e0923889f765b58b00e3869c5072a4e882c1b721913f63bda986b8c97b7ae575f0d4be596a1ac3cd0db96ce6074ee000b32018b3bda16d7dba34a13ba9c3ce983946414c16e278351a3411cb8ef2cb8ef5b6e1667c4c58bc797c0324ae4fec8960d684e561c0e833ee4c3331c6c439b59042a62993535e23cc8a8a4cf705c0f9b1d62db4e3d76c22c01138800414b143ddff471e4df4413e842a1b41f43cc9647e47145fd6c86d4d1a34fb2f62f5a55b31c9353ee34743c548eff955f2d2143c1a86cbcb452104f96d0142db31153021bbeed995c71a92de8fb1f97269533a508085c543fcb3ee57000bb265e74187b858403aa97b6c7b085e5d5b6025cbfe5f6926d33c835f90e60fc62013e80bbe0a855da5938b4b8f83ac29c5e8251827795356222079a6d1612e2fdf93bd7836d1613c7a353ada48ce256f880bbbb3108e037e3b5647101bd4d549101b0ee73d2248a932a802a3b1beb0b69d777c4285d57e91d83e96fe2f8a1a2f182fe2c6ca37b18460cf8d7f56c201147b9be19f1d01f8ad305c1e9c4dd79b5d8719d6550432352cf737082b1e9de7a083ffbe1" + ], + "pubkey": "e7ee7d51b11d09f268ade98bc9d7ae9be3c4ac124ce1c3a40e50d34460fa5f08" + } + } + } + } + # "# + # , 5, true, true, false, false); + ``` + * + */ + + fn create_mwixnet_req( + &self, + token: Token, + commitment: String, + fee_per_hop: String, + lock_output: bool, + server_keys: Vec, + ) -> Result; +} + +impl OwnerRpc for Owner +where + L: WalletLCProvider<'static, C, K>, + C: NodeClient + 'static, + K: Keychain + 'static, +{ + fn accounts(&self, token: Token) -> Result, Error> { + Owner::accounts(self, (&token.keychain_mask).as_ref()) + } + + fn create_account_path(&self, token: Token, label: &String) -> Result { + Owner::create_account_path(self, (&token.keychain_mask).as_ref(), label) + } + + fn set_active_account(&self, token: Token, label: &String) -> Result<(), Error> { + Owner::set_active_account(self, (&token.keychain_mask).as_ref(), label) + } + + fn retrieve_outputs( + &self, + token: Token, + include_spent: bool, + refresh_from_node: bool, + tx_id: Option, + ) -> Result<(bool, Vec), Error> { + Owner::retrieve_outputs( + self, + (&token.keychain_mask).as_ref(), + include_spent, + refresh_from_node, + tx_id, + ) + } + + fn retrieve_txs( + &self, + token: Token, + refresh_from_node: bool, + tx_id: Option, + tx_slate_id: Option, + ) -> Result<(bool, Vec), Error> { + Owner::retrieve_txs( + self, + (&token.keychain_mask).as_ref(), + refresh_from_node, + tx_id, + tx_slate_id, + None, + ) + } + + fn query_txs( + &self, + token: Token, + refresh_from_node: bool, + query: RetrieveTxQueryArgs, + ) -> Result<(bool, Vec), Error> { + Owner::retrieve_txs( + self, + (&token.keychain_mask).as_ref(), + refresh_from_node, + None, + None, + Some(query), + ) + } + + fn retrieve_summary_info( + &self, + token: Token, + refresh_from_node: bool, + minimum_confirmations: u64, + ) -> Result<(bool, WalletInfo), Error> { + Owner::retrieve_summary_info( + self, + (&token.keychain_mask).as_ref(), + refresh_from_node, + minimum_confirmations, + ) + } + + fn init_send_tx(&self, token: Token, args: InitTxArgs) -> Result { + let slate = Owner::init_send_tx(self, (&token.keychain_mask).as_ref(), args)?; + let version = SlateVersion::V4; + VersionedSlate::into_version(slate, version) + } + + fn issue_invoice_tx( + &self, + token: Token, + args: IssueInvoiceTxArgs, + ) -> Result { + let slate = Owner::issue_invoice_tx(self, (&token.keychain_mask).as_ref(), args)?; + let version = SlateVersion::V4; + VersionedSlate::into_version(slate, version) + } + + fn process_invoice_tx( + &self, + token: Token, + in_slate: VersionedSlate, + args: InitTxArgs, + ) -> Result { + let out_slate = Owner::process_invoice_tx( + self, + (&token.keychain_mask).as_ref(), + &Slate::from(in_slate), + args, + )?; + let version = SlateVersion::V4; + VersionedSlate::into_version(out_slate, version) + } + + fn finalize_tx(&self, token: Token, in_slate: VersionedSlate) -> Result { + let out_slate = Owner::finalize_tx( + self, + (&token.keychain_mask).as_ref(), + &Slate::from(in_slate), + )?; + let version = SlateVersion::V4; + VersionedSlate::into_version(out_slate, version) + } + + fn tx_lock_outputs(&self, token: Token, in_slate: VersionedSlate) -> Result<(), Error> { + Owner::tx_lock_outputs( + self, + (&token.keychain_mask).as_ref(), + &Slate::from(in_slate), + ) + } + + fn cancel_tx( + &self, + token: Token, + tx_id: Option, + tx_slate_id: Option, + ) -> Result<(), Error> { + Owner::cancel_tx(self, (&token.keychain_mask).as_ref(), tx_id, tx_slate_id) + } + + fn get_stored_tx( + &self, + token: Token, + id: Option, + slate_id: Option, + ) -> Result, Error> { + let out_slate = Owner::get_stored_tx( + self, + (&token.keychain_mask).as_ref(), + id, + (&slate_id).as_ref(), + )?; + match out_slate { + Some(s) => { + let version = SlateVersion::V4; + Ok(Some(VersionedSlate::into_version(s, version)?)) + } + None => Ok(None), + } + } + + fn post_tx(&self, token: Token, slate: VersionedSlate, fluff: bool) -> Result<(), Error> { + Owner::post_tx( + self, + (&token.keychain_mask).as_ref(), + &Slate::from(slate), + fluff, + ) + } + + fn get_rewind_hash(&self, token: Token) -> Result { + Owner::get_rewind_hash(self, (&token.keychain_mask).as_ref()) + } + + fn scan_rewind_hash( + &self, + rewind_hash: String, + start_height: Option, + ) -> Result { + Owner::scan_rewind_hash(self, rewind_hash, start_height) + } + + fn scan( + &self, + token: Token, + start_height: Option, + delete_unconfirmed: bool, + ) -> Result<(), Error> { + Owner::scan( + self, + (&token.keychain_mask).as_ref(), + start_height, + delete_unconfirmed, + ) + } + + fn node_height(&self, token: Token) -> Result { + Owner::node_height(self, (&token.keychain_mask).as_ref()) + } + + fn init_secure_api(&self, ecdh_pubkey: ECDHPubkey) -> Result { + let secp_inst = static_secp_instance(); + let secp = secp_inst.lock(); + let sec_key = SecretKey::new(&secp, &mut thread_rng()); + + let mut shared_pubkey = ecdh_pubkey.ecdh_pubkey; + shared_pubkey + .mul_assign(&secp, &sec_key) + .map_err(Error::Secp)?; + + let x_coord = shared_pubkey.serialize_vec(&secp, true); + let shared_key = SecretKey::from_slice(&secp, &x_coord[1..]).map_err(Error::Secp)?; + { + let mut s = self.shared_key.lock(); + *s = Some(shared_key); + } + + let pub_key = PublicKey::from_secret_key(&secp, &sec_key).map_err(Error::Secp)?; + + Ok(ECDHPubkey { + ecdh_pubkey: pub_key, + }) + } + + fn get_top_level_directory(&self) -> Result { + Owner::get_top_level_directory(self) + } + + fn set_top_level_directory(&self, dir: String) -> Result<(), Error> { + Owner::set_top_level_directory(self, &dir) + } + + fn create_config( + &self, + chain_type: global::ChainTypes, + wallet_config: Option, + logging_config: Option, + tor_config: Option, + ) -> Result<(), Error> { + Owner::create_config(self, &chain_type, wallet_config, logging_config, tor_config) + } + + fn create_wallet( + &self, + name: Option, + mnemonic: Option, + mnemonic_length: u32, + password: String, + ) -> Result<(), Error> { + let n = name.as_ref().map(|s| s.as_str()); + let m = match mnemonic { + Some(s) => Some(ZeroingString::from(s)), + None => None, + }; + Owner::create_wallet(self, n, m, mnemonic_length, ZeroingString::from(password)) + } + + fn open_wallet(&self, name: Option, password: String) -> Result { + let n = name.as_ref().map(|s| s.as_str()); + let sec_key = Owner::open_wallet(self, n, ZeroingString::from(password), true)?; + Ok(Token { + keychain_mask: sec_key, + }) + } + + fn close_wallet(&self, name: Option) -> Result<(), Error> { + let n = name.as_ref().map(|s| s.as_str()); + Owner::close_wallet(self, n) + } + + fn get_mnemonic(&self, name: Option, password: String) -> Result { + let n = name.as_ref().map(|s| s.as_str()); + let res = Owner::get_mnemonic(self, n, ZeroingString::from(password))?; + Ok((&*res).to_string()) + } + + fn change_password(&self, name: Option, old: String, new: String) -> Result<(), Error> { + let n = name.as_ref().map(|s| s.as_str()); + Owner::change_password(self, n, ZeroingString::from(old), ZeroingString::from(new)) + } + + fn delete_wallet(&self, name: Option) -> Result<(), Error> { + let n = name.as_ref().map(|s| s.as_str()); + Owner::delete_wallet(self, n) + } + + fn start_updater(&self, token: Token, frequency: u32) -> Result<(), Error> { + Owner::start_updater( + self, + (&token.keychain_mask).as_ref(), + Duration::from_millis(frequency as u64), + ) + } + + fn stop_updater(&self) -> Result<(), Error> { + Owner::stop_updater(self) + } + + fn get_updater_messages(&self, count: u32) -> Result, Error> { + Owner::get_updater_messages(self, count as usize) + } + + fn get_slatepack_address( + &self, + token: Token, + derivation_index: u32, + ) -> Result { + Owner::get_slatepack_address(self, (&token.keychain_mask).as_ref(), derivation_index) + } + + fn get_slatepack_secret_key( + &self, + token: Token, + derivation_index: u32, + ) -> Result { + let key = Owner::get_slatepack_secret_key( + self, + (&token.keychain_mask).as_ref(), + derivation_index, + )?; + Ok(Ed25519SecretKey { key }) + } + + fn create_slatepack_message( + &self, + token: Token, + slate: VersionedSlate, + sender_index: Option, + recipients: Vec, + ) -> Result { + let res = Owner::create_slatepack_message( + self, + (&token.keychain_mask).as_ref(), + &Slate::from(slate), + sender_index, + recipients, + )?; + Ok(res.trim().into()) + } + + fn slate_from_slatepack_message( + &self, + token: Token, + message: String, + secret_indices: Vec, + ) -> Result { + let slate = Owner::slate_from_slatepack_message( + self, + (&token.keychain_mask).as_ref(), + message, + secret_indices, + )?; + let version = SlateVersion::V4; + VersionedSlate::into_version(slate, version) + } + + fn decode_slatepack_message( + &self, + token: Token, + message: String, + secret_indices: Vec, + ) -> Result { + Owner::decode_slatepack_message( + self, + (&token.keychain_mask).as_ref(), + message, + secret_indices, + ) + } + + fn retrieve_payment_proof( + &self, + token: Token, + refresh_from_node: bool, + tx_id: Option, + tx_slate_id: Option, + ) -> Result { + Owner::retrieve_payment_proof( + self, + (&token.keychain_mask).as_ref(), + refresh_from_node, + tx_id, + tx_slate_id, + ) + } + + fn verify_payment_proof( + &self, + token: Token, + proof: PaymentProof, + ) -> Result<(bool, bool), Error> { + Owner::verify_payment_proof(self, (&token.keychain_mask).as_ref(), &proof) + } + + fn set_tor_config(&self, tor_config: Option) -> Result<(), Error> { + Owner::set_tor_config(self, tor_config); + Ok(()) + } + + fn build_output( + &self, + token: Token, + features: OutputFeatures, + amount: Amount, + ) -> Result { + Owner::build_output(self, (&token.keychain_mask).as_ref(), features, amount.0) + } + + fn create_mwixnet_req( + &self, + token: Token, + commitment: String, + fee_per_hop: String, + lock_output: bool, + server_keys: Vec, + ) -> Result { + let commit = + Commitment::from_vec(from_hex(&commitment).map_err(|e| Error::CommitDeser(e))?); + + let secp_inst = static_secp_instance(); + let secp = secp_inst.lock(); + + let mut keys = vec![]; + for key in server_keys { + keys.push(SecretKey::from_slice( + &secp, + &grin_util::from_hex(&key).map_err(|e| Error::ServerKeyDeser(e))?, + )?) + } + + let req_params = MixnetReqCreationParams { + server_keys: keys, + fee_per_hop: fee_per_hop + .parse::() + .map_err(|_| Error::U64Deser(fee_per_hop))?, + }; + + Owner::create_mwixnet_req( + self, + (&token.keychain_mask).as_ref(), + &req_params, + &commit, + lock_output, + ) + } +} + +/// helper to set up a real environment to run integrated doctests +pub fn run_doctest_owner( + request: serde_json::Value, + test_dir: &str, + blocks_to_mine: u64, + perform_tx: bool, + lock_tx: bool, + finalize_tx: bool, + payment_proof: bool, +) -> Result, String> { + use easy_jsonrpc_mw::Handler; + use grin_keychain::ExtKeychain; + use grin_wallet_impls::test_framework::{self, LocalWalletClient, WalletProxy}; + use grin_wallet_impls::{DefaultLCProvider, DefaultWalletImpl}; + use grin_wallet_libwallet::{api_impl, WalletInst}; + + use crate::core::global::ChainTypes; + use grin_util as util; + + use std::{fs, thread}; + + util::init_test_logger(); + let _ = fs::remove_dir_all(test_dir); + global::set_local_chain_type(ChainTypes::AutomatedTesting); + + let mut wallet_proxy: WalletProxy< + DefaultLCProvider, + LocalWalletClient, + ExtKeychain, + > = WalletProxy::new(test_dir); + let chain = wallet_proxy.chain.clone(); + + let rec_phrase_1 = util::ZeroingString::from( + "fat twenty mean degree forget shell check candy immense awful \ + flame next during february bulb bike sun wink theory day kiwi embrace peace lunch", + ); + let empty_string = util::ZeroingString::from(""); + + let client1 = LocalWalletClient::new("wallet1", wallet_proxy.tx.clone()); + let mut wallet1 = + Box::new(DefaultWalletImpl::::new(client1.clone()).unwrap()) + as Box< + dyn WalletInst< + 'static, + DefaultLCProvider, + LocalWalletClient, + ExtKeychain, + >, + >; + let lc = wallet1.lc_provider().unwrap(); + let _ = lc.set_top_level_directory(&format!("{}/wallet1", test_dir)); + lc.create_wallet(None, Some(rec_phrase_1), 32, empty_string.clone(), false) + .unwrap(); + let mask1 = lc + .open_wallet(None, empty_string.clone(), true, true) + .unwrap(); + let wallet1 = Arc::new(Mutex::new(wallet1)); + + if mask1.is_some() { + println!("WALLET 1 MASK: {:?}", mask1.clone().unwrap()); + } + + wallet_proxy.add_wallet( + "wallet1", + client1.get_send_instance(), + wallet1.clone(), + mask1.clone(), + ); + + let mut slate_outer = Slate::blank(2, false); + + let rec_phrase_2 = util::ZeroingString::from( + "hour kingdom ripple lunch razor inquiry coyote clay stamp mean \ + sell finish magic kid tiny wage stand panther inside settle feed song hole exile", + ); + let client2 = LocalWalletClient::new("wallet2", wallet_proxy.tx.clone()); + let mut wallet2 = + Box::new(DefaultWalletImpl::::new(client2.clone()).unwrap()) + as Box< + dyn WalletInst< + 'static, + DefaultLCProvider, + LocalWalletClient, + ExtKeychain, + >, + >; + let lc = wallet2.lc_provider().unwrap(); + let _ = lc.set_top_level_directory(&format!("{}/wallet2", test_dir)); + lc.create_wallet(None, Some(rec_phrase_2), 32, empty_string.clone(), false) + .unwrap(); + let mask2 = lc.open_wallet(None, empty_string, true, true).unwrap(); + let wallet2 = Arc::new(Mutex::new(wallet2)); + + if mask2.is_some() { + println!("WALLET 2 MASK: {:?}", mask2.clone().unwrap()); + } + + wallet_proxy.add_wallet( + "wallet2", + client2.get_send_instance(), + wallet2.clone(), + mask2.clone(), + ); + + // Set the wallet proxy listener running + thread::spawn(move || { + if let Err(e) = wallet_proxy.run() { + error!("Wallet Proxy error: {}", e); + } + }); + + // Mine a few blocks to wallet 1 so there's something to send + for _ in 0..blocks_to_mine { + let _ = test_framework::award_blocks_to_wallet( + &chain, + wallet1.clone(), + (&mask1).as_ref(), + 1 as usize, + false, + ); + //update local outputs after each block, so transaction IDs stay consistent + let (wallet_refreshed, _) = api_impl::owner::retrieve_summary_info( + wallet1.clone(), + (&mask1).as_ref(), + &None, + true, + 1, + ) + .unwrap(); + assert!(wallet_refreshed); + } + + if perform_tx { + let amount = 60_000_000_000; + let mut w_lock = wallet1.lock(); + let w = w_lock.lc_provider().unwrap().wallet_inst().unwrap(); + let proof_address = match payment_proof { + true => { + let address = "783f6528669742a990e0faf0a5fca5d5b3330e37bbb9cd5c628696d03ce4e810"; + let address = OnionV3Address::try_from(address).unwrap(); + Some(SlatepackAddress::try_from(address).unwrap()) + } + false => None, + }; + let args = InitTxArgs { + src_acct_name: None, + amount, + minimum_confirmations: 2, + max_outputs: 500, + num_change_outputs: 1, + selection_strategy_is_use_all: true, + payment_proof_recipient_address: proof_address, + ..Default::default() + }; + let mut slate = + api_impl::owner::init_send_tx(&mut **w, (&mask1).as_ref(), args, true).unwrap(); + println!("INITIAL SLATE"); + println!("{}", serde_json::to_string_pretty(&slate).unwrap()); + { + let mut w_lock = wallet2.lock(); + let w2 = w_lock.lc_provider().unwrap().wallet_inst().unwrap(); + slate = api_impl::foreign::receive_tx(&mut **w2, (&mask2).as_ref(), &slate, None, true) + .unwrap(); + w2.close().unwrap(); + } + // Spit out slate for input to finalize_tx + if lock_tx { + println!("LOCKING TX"); + api_impl::owner::tx_lock_outputs(&mut **w, (&mask1).as_ref(), &slate).unwrap(); + } + println!("RECEIPIENT SLATE"); + println!("{}", serde_json::to_string_pretty(&slate).unwrap()); + if finalize_tx { + slate = api_impl::owner::finalize_tx(&mut **w, (&mask1).as_ref(), &slate).unwrap(); + error!("FINALIZED TX SLATE"); + println!("{}", serde_json::to_string_pretty(&slate).unwrap()); + } + slate_outer = slate; + } + + if payment_proof { + api_impl::owner::post_tx(&client1, slate_outer.tx_or_err().unwrap(), true).unwrap(); + } + + if perform_tx && lock_tx && finalize_tx { + // mine to move the chain on + let _ = test_framework::award_blocks_to_wallet( + &chain, + wallet1.clone(), + (&mask1).as_ref(), + 3 as usize, + false, + ); + } + + let mut api_owner = Owner::new(wallet1, None); + api_owner.doctest_mode = true; + let owner_api = &api_owner as &dyn OwnerRpc; + let res = owner_api.handle_request(request).as_option(); + let _ = fs::remove_dir_all(test_dir); + Ok(res) +} + +#[doc(hidden)] +#[macro_export] +macro_rules! doctest_helper_json_rpc_owner_assert_response { + ($request:expr, $expected_response:expr, $blocks_to_mine:expr, $perform_tx:expr, $lock_tx:expr, $finalize_tx:expr, $payment_proof:expr) => { + // create temporary wallet, run jsonrpc request on owner api of wallet, delete wallet, return + // json response. + // In order to prevent leaking tempdirs, This function should not panic. + + // These cause LMDB to run out of disk space on CircleCI + // disable for now on windows + // TODO: Fix properly + #[cfg(not(target_os = "windows"))] + { + use grin_wallet_api::run_doctest_owner; + use serde_json; + use serde_json::Value; + use tempfile::tempdir; + + let dir = tempdir().map_err(|e| format!("{:#?}", e)).unwrap(); + let dir = dir + .path() + .to_str() + .ok_or("Failed to convert tmpdir path to string.".to_owned()) + .unwrap(); + + let request_val: Value = serde_json::from_str($request).unwrap(); + let expected_response: Value = serde_json::from_str($expected_response).unwrap(); + + let response = run_doctest_owner( + request_val, + dir, + $blocks_to_mine, + $perform_tx, + $lock_tx, + $finalize_tx, + $payment_proof, + ) + .unwrap() + .unwrap(); + + if response != expected_response { + panic!( + "(left != right) \nleft: {}\nright: {}", + serde_json::to_string_pretty(&response).unwrap(), + serde_json::to_string_pretty(&expected_response).unwrap() + ); + } + } + }; +} diff --git a/api/src/types.rs b/api/src/types.rs new file mode 100644 index 0000000..4ac88a3 --- /dev/null +++ b/api/src/types.rs @@ -0,0 +1,340 @@ +// Copyright 2021 The Grin Developers +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::core::libtx::secp_ser; +use crate::libwallet::slate_versions::ser as dalek_ser; +use crate::libwallet::Error; +use crate::util::secp::key::{PublicKey, SecretKey}; +use crate::util::{from_hex, ToHex}; +use ed25519_dalek::SecretKey as DalekSecretKey; + +use base64; +use rand::{thread_rng, Rng}; +use ring::aead; +use serde_json::{self, Value}; +use std::collections::HashMap; + +/// Represents a compliant JSON RPC 2.0 id. +/// Valid id: Integer, String. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +#[serde(untagged)] +pub enum JsonId { + /// Integer Id + IntId(u32), + /// String Id + StrId(String), +} + +/// Wrapper for API Tokens +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(transparent)] +pub struct Token { + #[serde(with = "secp_ser::option_seckey_serde")] + /// Token to XOR mask against the stored wallet seed + pub keychain_mask: Option, +} + +/// Wrapper for ECDH Public keys +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(transparent)] +pub struct ECDHPubkey { + /// public key, flattened + #[serde(with = "secp_ser::pubkey_serde")] + pub ecdh_pubkey: PublicKey, +} + +/// Wrapper for Secret Keys +#[derive(Serialize, Deserialize, Debug)] +#[serde(transparent)] +pub struct Ed25519SecretKey { + #[serde(with = "dalek_ser::dalek_seckey_serde")] + /// Token to XOR mask against the stored wallet seed + pub key: DalekSecretKey, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct EncryptedBody { + /// nonce used for encryption + pub nonce: String, + /// Encrypted base64 body request + pub body_enc: String, +} + +impl EncryptedBody { + /// Encrypts and encodes json as base 64 + pub fn from_json(json_in: &Value, enc_key: &SecretKey) -> Result { + let mut to_encrypt = serde_json::to_string(&json_in) + .map_err(|_| { + Error::APIEncryption("EncryptedBody Enc: Unable to encode JSON".to_owned()) + })? + .as_bytes() + .to_vec(); + + let nonce: [u8; 12] = thread_rng().gen(); + + let unbound_key = aead::UnboundKey::new(&aead::AES_256_GCM, &enc_key.0).unwrap(); + let sealing_key: aead::LessSafeKey = aead::LessSafeKey::new(unbound_key); + let aad = aead::Aad::from(&[]); + let res = sealing_key.seal_in_place_append_tag( + aead::Nonce::assume_unique_for_key(nonce), + aad, + &mut to_encrypt, + ); + if let Err(_) = res { + return Err(Error::APIEncryption("EncryptedBody: encryption failed".to_owned()).into()); + } + + Ok(EncryptedBody { + nonce: nonce.to_hex(), + body_enc: base64::encode(&to_encrypt), + }) + } + + /// return serialize JSON self + pub fn as_json_value(&self) -> Result { + let res = serde_json::to_value(self).map_err(|_| { + Error::APIEncryption("EncryptedBody: JSON serialization failed".to_owned()) + })?; + Ok(res) + } + + /// return serialized JSON self as string + pub fn as_json_str(&self) -> Result { + let res = self.as_json_value()?; + let res = serde_json::to_string(&res).map_err(|_| { + Error::APIEncryption("EncryptedBody: JSON String serialization failed".to_owned()) + })?; + Ok(res) + } + + /// Return original request + pub fn decrypt(&self, dec_key: &SecretKey) -> Result { + let mut to_decrypt = base64::decode(&self.body_enc).map_err(|_| { + Error::APIEncryption( + "EncryptedBody Dec: Encrypted request contains invalid Base64".to_string(), + ) + })?; + let nonce = from_hex(&self.nonce) + .map_err(|_| Error::APIEncryption("EncryptedBody Dec: Invalid Nonce".to_string()))?; + if nonce.len() < 12 { + return Err(Error::APIEncryption( + "EncryptedBody Dec: Invalid Nonce length".to_string(), + ) + .into()); + } + let mut n = [0u8; 12]; + n.copy_from_slice(&nonce[0..12]); + let unbound_key = aead::UnboundKey::new(&aead::AES_256_GCM, &dec_key.0).unwrap(); + let opening_key: aead::LessSafeKey = aead::LessSafeKey::new(unbound_key); + let aad = aead::Aad::from(&[]); + let res = + opening_key.open_in_place(aead::Nonce::assume_unique_for_key(n), aad, &mut to_decrypt); + if let Err(_) = res { + return Err(Error::APIEncryption("EncryptedBody: decryption failed".to_owned()).into()); + } + for _ in 0..aead::AES_256_GCM.tag_len() { + to_decrypt.pop(); + } + + let decrypted = String::from_utf8(to_decrypt) + .map_err(|_| Error::APIEncryption("EncryptedBody Dec: Invalid UTF-8".to_string()))?; + Ok(serde_json::from_str(&decrypted) + .map_err(|_| Error::APIEncryption("EncryptedBody Dec: Invalid JSON".to_string()))?) + } +} + +/// Wrapper for secure JSON requests +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct EncryptedRequest { + /// JSON RPC response + pub jsonrpc: String, + /// method + pub method: String, + /// id + pub id: JsonId, + /// Body params, which includes nonce and encrypted request + pub params: EncryptedBody, +} + +impl EncryptedRequest { + /// from json + pub fn from_json(id: &JsonId, json_in: &Value, enc_key: &SecretKey) -> Result { + Ok(EncryptedRequest { + jsonrpc: "2.0".to_owned(), + method: "encrypted_request_v3".to_owned(), + id: id.clone(), + params: EncryptedBody::from_json(json_in, enc_key)?, + }) + } + + /// return serialize JSON self + pub fn as_json_value(&self) -> Result { + let res = serde_json::to_value(self).map_err(|_| { + Error::APIEncryption("EncryptedRequest: JSON serialization failed".to_owned()) + })?; + Ok(res) + } + + /// return serialized JSON self as string + pub fn as_json_str(&self) -> Result { + let res = self.as_json_value()?; + let res = serde_json::to_string(&res).map_err(|_| { + Error::APIEncryption("EncryptedRequest: JSON String serialization failed".to_owned()) + })?; + Ok(res) + } + + /// Return decrypted body + pub fn decrypt(&self, dec_key: &SecretKey) -> Result { + self.params.decrypt(dec_key) + } +} + +/// Wrapper for secure JSON requests +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct EncryptedResponse { + /// JSON RPC response + pub jsonrpc: String, + /// id + pub id: JsonId, + /// result + pub result: HashMap, +} + +impl EncryptedResponse { + /// from json + pub fn from_json(id: &JsonId, json_in: &Value, enc_key: &SecretKey) -> Result { + let mut result_set = HashMap::new(); + result_set.insert( + "Ok".to_string(), + EncryptedBody::from_json(json_in, enc_key)?, + ); + Ok(EncryptedResponse { + jsonrpc: "2.0".to_owned(), + id: id.clone(), + result: result_set, + }) + } + + /// return serialize JSON self + pub fn as_json_value(&self) -> Result { + let res = serde_json::to_value(self).map_err(|_| { + Error::APIEncryption("EncryptedResponse: JSON serialization failed".to_owned()) + })?; + Ok(res) + } + + /// return serialized JSON self as string + pub fn as_json_str(&self) -> Result { + let res = self.as_json_value()?; + let res = serde_json::to_string(&res).map_err(|_| { + Error::APIEncryption("EncryptedResponse: JSON String serialization failed".to_owned()) + })?; + Ok(res) + } + + /// Return decrypted body + pub fn decrypt(&self, dec_key: &SecretKey) -> Result { + self.result.get("Ok").unwrap().decrypt(dec_key) + } +} + +/// Wrapper for encryption error responses +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct EncryptionError { + /// code + pub code: i32, + /// message + pub message: String, +} + +/// Wrapper for encryption error responses +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct EncryptionErrorResponse { + /// JSON RPC response + pub jsonrpc: String, + /// id + #[serde(with = "secp_ser::string_or_u64")] + pub id: u64, + /// error + pub error: EncryptionError, +} + +impl EncryptionErrorResponse { + /// Create new response + pub fn new(id: u64, code: i32, message: &str) -> Self { + EncryptionErrorResponse { + jsonrpc: "2.0".to_owned(), + id: id, + error: EncryptionError { + code: code, + message: message.to_owned(), + }, + } + } + + /// return serialized JSON self + pub fn as_json_value(&self) -> Value { + let res = serde_json::to_value(self).map_err(|_| { + Error::APIEncryption("EncryptedResponse: JSON serialization failed".to_owned()) + }); + match res { + Ok(r) => r, + // proverbial "should never happen" + Err(r) => serde_json::json!({ + "json_rpc" : "2.0", + "id" : "1", + "error" : { + "message": format!("internal error serialising json error response {}", r), + "code": -32000 + } + } + ), + } + } +} + +#[test] +fn encrypted_request() -> Result<(), Error> { + use crate::util::{from_hex, static_secp_instance}; + + let sec_key_str = "e00dcc4a009e3427c6b1e1a550c538179d46f3827a13ed74c759c860761caf1e"; + let shared_key = { + let secp_inst = static_secp_instance(); + let secp = secp_inst.lock(); + + let sec_key_bytes = from_hex(sec_key_str).unwrap(); + SecretKey::from_slice(&secp, &sec_key_bytes)? + }; + let req = serde_json::json!({ + "jsonrpc": "2.0", + "method": "accounts", + "id": 1, + "params": { + "token": "d202964900000000d302964900000000d402964900000000d502964900000000" + } + }); + let enc_req = + EncryptedRequest::from_json(&JsonId::StrId(String::from("1")), &req, &shared_key)?; + println!("{:?}", enc_req); + let dec_req = enc_req.decrypt(&shared_key)?; + println!("{:?}", dec_req); + assert_eq!(req, dec_req); + let enc_res = EncryptedResponse::from_json(&JsonId::IntId(1), &req, &shared_key)?; + println!("{:?}", enc_res); + println!("{:?}", enc_res.as_json_str()?); + let dec_res = enc_res.decrypt(&shared_key)?; + println!("{:?}", dec_res); + assert_eq!(req, dec_res); + Ok(()) +} diff --git a/api/tests/slate_versioning.rs b/api/tests/slate_versioning.rs new file mode 100644 index 0000000..ede8453 --- /dev/null +++ b/api/tests/slate_versioning.rs @@ -0,0 +1,134 @@ +// Copyright 2021 The Grin Developers +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! core::libtx specific tests +//use grin_wallet_api::foreign_rpc_client; +use grin_wallet_api::run_doctest_foreign; +//use grin_wallet_libwallet::VersionedSlate; +use serde_json; +use serde_json::Value; +use tempfile::tempdir; +//use grin_wallet_libwallet::slate_versions::v1::SlateV1; +//use grin_wallet_libwallet::slate_versions::v2::SlateV2; + +// test all slate conversions +//#[test] +fn _receive_versioned_slate() { + // as in doctests, except exercising versioning functionality + // by accepting and responding with a V1 slate + + let dir = tempdir().map_err(|e| format!("{:#?}", e)).unwrap(); + let dir = dir + .path() + .to_str() + .ok_or("Failed to convert tmpdir path to string.".to_owned()) + .unwrap(); + + let v1_req = include_str!("slates/v1_req.slate"); + let v1_resp = include_str!("slates/v1_res.slate"); + + // leave here for the ability to create earlier slate versions + // for test input + /*let v: Value = serde_json::from_str(v1_req).unwrap(); + let v2_slate = v["params"][0].clone(); + println!("{}", v2_slate); + let v2_slate_str = v2_slate.to_string(); + println!("{}", v2_slate_str); + let v2: SlateV2 = serde_json::from_str(&v2_slate.to_string()).unwrap(); + let v1 = SlateV1::from(v2); + let v1_str = serde_json::to_string_pretty(&v1).unwrap(); + panic!("{}", v1_str);*/ + + let request_val: Value = serde_json::from_str(v1_req).unwrap(); + let expected_response: Value = serde_json::from_str(v1_resp).unwrap(); + + let response = run_doctest_foreign(request_val, dir, false, 5, true, false) + .unwrap() + .unwrap(); + + if response != expected_response { + panic!( + "(left != right) \nleft: {}\nright: {}", + serde_json::to_string_pretty(&response).unwrap(), + serde_json::to_string_pretty(&expected_response).unwrap() + ); + } +} + +// TODO: Re-introduce on a new slate version + +/* +/// call ForeignRpc::receive_tx on vs and return the result +fn receive_tx(vs: VersionedSlate) -> VersionedSlate { + let dir = tempdir().map_err(|e| format!("{:#?}", e)).unwrap(); + let dir = dir + .path() + .to_str() + .ok_or("Failed to convert tmpdir path to string.".to_owned()) + .unwrap(); + let bound_method = foreign_rpc_client::receive_tx( + vs, + None, + Some("Thanks for saving my dog from that tree, bddap.".into()), + ) + .unwrap(); + let (call, tracker) = bound_method.call(); + let json_response = run_doctest_foreign(call.as_request(), dir, 5, false, false) + .unwrap() + .unwrap(); + let mut response = easy_jsonrpc::Response::from_json_response(json_response).unwrap(); + tracker.get_return(&mut response).unwrap().unwrap() +} + +#[test] +fn version_unchanged() { + let req: Value = serde_json::from_str(include_str!("slates/v1_req.slate")).unwrap(); + let slate: VersionedSlate = serde_json::from_value(req["params"][0].clone()).unwrap(); + let slate_req: Slate = slate.into(); + + assert_eq!( + receive_tx(VersionedSlate::into_version( + slate_req.clone(), + SlateVersion::V0 + )) + .version(), + SlateVersion::V0 + ); + + assert_eq!( + receive_tx(VersionedSlate::into_version( + slate_req.clone(), + SlateVersion::V1 + )) + .version(), + SlateVersion::V1 + ); + + assert_eq!( + receive_tx(VersionedSlate::into_version( + slate_req.clone(), + SlateVersion::V2 + )) + .version(), + SlateVersion::V2 + ); + + // compile time test will remind us to update these tests when updating slate format + fn _all_versions_tested(vs: VersionedSlate) { + match vs { + VersionedSlate::V0(_) => (), + VersionedSlate::V1(_) => (), + VersionedSlate::V2(_) => (), + } + } +}*/ diff --git a/api/tests/slates/v1_req.slate b/api/tests/slates/v1_req.slate new file mode 100644 index 0000000..d3ff7a5 --- /dev/null +++ b/api/tests/slates/v1_req.slate @@ -0,0 +1,1037 @@ +{ + "jsonrpc": "2.0", + "method": "receive_tx", + "id": 1, + "params": [ + { + "num_participants": 2, + "id": "0436430c-2b02-624c-2032-570501212b00", + "tx": { + "offset": [ + 210, + 2, + 150, + 73, + 0, + 0, + 0, + 0, + 211, + 2, + 150, + 73, + 0, + 0, + 0, + 0, + 212, + 2, + 150, + 73, + 0, + 0, + 0, + 0, + 213, + 2, + 150, + 73, + 0, + 0, + 0, + 0 + ], + "body": { + "inputs": [ + { + "features": "Coinbase", + "commit": [ + 8, + 125, + 243, + 35, + 4, + 197, + 212, + 174, + 139, + 42, + 240, + 188, + 49, + 231, + 0, + 1, + 157, + 114, + 41, + 16, + 239, + 135, + 221, + 78, + 236, + 49, + 151, + 184, + 11, + 32, + 126, + 48, + 69 + ] + }, + { + "features": "Coinbase", + "commit": [ + 8, + 225, + 218, + 158, + 109, + 196, + 214, + 232, + 8, + 167, + 24, + 178, + 241, + 16, + 169, + 145, + 221, + 119, + 93, + 101, + 206, + 90, + 228, + 8, + 164, + 225, + 240, + 2, + 164, + 150, + 26, + 169, + 231 + ] + } + ], + "outputs": [ + { + "features": "Plain", + "commit": [ + 8, + 18, + 39, + 108, + 199, + 136, + 230, + 135, + 6, + 18, + 41, + 109, + 146, + 108, + 186, + 159, + 14, + 123, + 152, + 16, + 103, + 7, + 16, + 181, + 166, + 230, + 241, + 186, + 0, + 109, + 57, + 87, + 116 + ], + "proof": [ + 220, + 255, + 97, + 117, + 57, + 12, + 96, + 43, + 250, + 146, + 194, + 255, + 209, + 169, + 178, + 216, + 77, + 204, + 158, + 169, + 65, + 246, + 243, + 23, + 189, + 208, + 248, + 117, + 36, + 78, + 242, + 62, + 105, + 111, + 209, + 124, + 113, + 223, + 121, + 118, + 12, + 229, + 206, + 26, + 150, + 170, + 177, + 209, + 93, + 208, + 87, + 53, + 141, + 200, + 53, + 233, + 114, + 254, + 190, + 184, + 109, + 80, + 204, + 236, + 13, + 173, + 124, + 254, + 2, + 70, + 215, + 66, + 235, + 117, + 60, + 247, + 184, + 140, + 4, + 93, + 21, + 188, + 113, + 35, + 248, + 207, + 113, + 85, + 100, + 124, + 207, + 102, + 63, + 202, + 146, + 168, + 60, + 154, + 101, + 208, + 237, + 117, + 110, + 167, + 235, + 255, + 210, + 202, + 201, + 12, + 56, + 10, + 16, + 46, + 217, + 202, + 170, + 53, + 93, + 23, + 94, + 208, + 191, + 88, + 211, + 172, + 47, + 94, + 144, + 157, + 108, + 68, + 125, + 252, + 107, + 96, + 94, + 4, + 146, + 92, + 43, + 23, + 195, + 62, + 189, + 25, + 8, + 201, + 101, + 165, + 84, + 30, + 165, + 210, + 237, + 69, + 160, + 149, + 142, + 100, + 2, + 248, + 157, + 122, + 86, + 223, + 25, + 146, + 224, + 54, + 216, + 54, + 231, + 64, + 23, + 231, + 60, + 202, + 213, + 203, + 58, + 130, + 184, + 225, + 57, + 227, + 9, + 121, + 42, + 49, + 177, + 95, + 63, + 253, + 114, + 237, + 3, + 50, + 83, + 66, + 140, + 21, + 108, + 43, + 151, + 153, + 69, + 138, + 37, + 193, + 218, + 101, + 183, + 25, + 120, + 10, + 34, + 222, + 127, + 231, + 244, + 55, + 174, + 47, + 204, + 210, + 44, + 247, + 234, + 53, + 122, + 181, + 170, + 102, + 165, + 239, + 125, + 113, + 251, + 13, + 198, + 74, + 160, + 181, + 118, + 31, + 104, + 39, + 128, + 98, + 187, + 57, + 187, + 41, + 108, + 120, + 126, + 76, + 171, + 197, + 226, + 162, + 147, + 58, + 65, + 108, + 225, + 201, + 169, + 105, + 97, + 96, + 56, + 100, + 73, + 196, + 55, + 233, + 18, + 15, + 123, + 178, + 110, + 91, + 14, + 116, + 209, + 242, + 231, + 213, + 188, + 215, + 170, + 251, + 42, + 146, + 184, + 125, + 21, + 72, + 241, + 249, + 17, + 251, + 6, + 175, + 123, + 214, + 204, + 19, + 206, + 226, + 159, + 124, + 156, + 183, + 144, + 33, + 174, + 209, + 129, + 134, + 39, + 42, + 240, + 233, + 209, + 137, + 236, + 16, + 124, + 129, + 168, + 163, + 174, + 180, + 120, + 43, + 13, + 149, + 14, + 72, + 129, + 170, + 81, + 183, + 118, + 187, + 104, + 68, + 178, + 91, + 206, + 151, + 3, + 91, + 72, + 169, + 189, + 178, + 174, + 163, + 96, + 134, + 135, + 188, + 221, + 71, + 157, + 79, + 169, + 152, + 181, + 168, + 57, + 255, + 136, + 85, + 142, + 74, + 41, + 223, + 240, + 237, + 19, + 181, + 89, + 0, + 171, + 181, + 212, + 57, + 183, + 7, + 147, + 217, + 2, + 174, + 154, + 211, + 69, + 135, + 177, + 140, + 145, + 159, + 107, + 135, + 92, + 145, + 209, + 77, + 238, + 177, + 195, + 115, + 245, + 231, + 101, + 112, + 213, + 154, + 101, + 73, + 117, + 143, + 101, + 95, + 17, + 40, + 165, + 79, + 22, + 45, + 254, + 136, + 104, + 225, + 88, + 112, + 40, + 226, + 106, + 217, + 30, + 82, + 140, + 90, + 231, + 238, + 147, + 53, + 250, + 88, + 251, + 89, + 2, + 43, + 93, + 226, + 157, + 128, + 240, + 118, + 74, + 153, + 23, + 57, + 13, + 70, + 219, + 137, + 154, + 204, + 106, + 91, + 65, + 110, + 37, + 236, + 201, + 220, + 203, + 113, + 83, + 100, + 106, + 221, + 204, + 129, + 202, + 219, + 95, + 0, + 120, + 254, + 188, + 126, + 5, + 215, + 115, + 90, + 186, + 73, + 79, + 57, + 239, + 5, + 105, + 123, + 188, + 201, + 180, + 123, + 44, + 204, + 121, + 89, + 93, + 117, + 252, + 19, + 200, + 6, + 120, + 181, + 226, + 55, + 237, + 206, + 88, + 215, + 49, + 243, + 76, + 5, + 177, + 221, + 202, + 166, + 73, + 172, + 242, + 216, + 101, + 187, + 188, + 60, + 237, + 161, + 5, + 8, + 188, + 221, + 41, + 208, + 73, + 103, + 68, + 100, + 75, + 241, + 195, + 81, + 111, + 102, + 135, + 223, + 238, + 245, + 100, + 156, + 125, + 255, + 144, + 98, + 125, + 100, + 39, + 57, + 165, + 157, + 145, + 168, + 209, + 208, + 196, + 220, + 85, + 215, + 74, + 148, + 158, + 16, + 116, + 66, + 118, + 100, + 180, + 103, + 153, + 44, + 158, + 15, + 125, + 58, + 249, + 214, + 234, + 121, + 81, + 62, + 137, + 70, + 221, + 192, + 211, + 86, + 186, + 196, + 152, + 120, + 230, + 78, + 106, + 149, + 176, + 163, + 2, + 20, + 33, + 79, + 175, + 44, + 227, + 23, + 250, + 98, + 47, + 243, + 38, + 107, + 50, + 168, + 22, + 225, + 10, + 24, + 230, + 215, + 137, + 165, + 218, + 31, + 35, + 230, + 123, + 79, + 151, + 10, + 104, + 167, + 188, + 217, + 225, + 136, + 37, + 238, + 39, + 75, + 4, + 131, + 137, + 106, + 64 + ] + } + ], + "kernels": [ + { + "features": "Plain", + "fee": 7000000, + "lock_height": 0, + "excess": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "excess_sig": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + } + ] + } + }, + "amount": 60000000000, + "fee": 7000000, + "height": 5, + "lock_height": 0, + "participant_data": [ + { + "id": 0, + "public_blind_excess": [ + 3, + 58, + 194, + 21, + 143, + 160, + 7, + 127, + 8, + 125, + 230, + 12, + 25, + 216, + 228, + 49, + 117, + 59, + 170, + 91, + 99, + 182, + 225, + 71, + 127, + 5, + 162, + 166, + 231, + 25, + 13, + 69, + 146 + ], + "public_nonce": [ + 3, + 27, + 132, + 197, + 86, + 123, + 18, + 100, + 64, + 153, + 93, + 62, + 213, + 170, + 186, + 5, + 101, + 215, + 30, + 24, + 52, + 96, + 72, + 25, + 255, + 156, + 23, + 245, + 233, + 213, + 221, + 7, + 143 + ], + "part_sig": null, + "message": null, + "message_sig": null + } + ], + "version": 1 + }, + null, + "Thanks, Yeastplume" + ] +} diff --git a/api/tests/slates/v1_res.slate b/api/tests/slates/v1_res.slate new file mode 100644 index 0000000..86a3e94 --- /dev/null +++ b/api/tests/slates/v1_res.slate @@ -0,0 +1,1955 @@ +{ + "id": 1, + "jsonrpc": "2.0", + "result": { + "Ok": { + "amount": 60000000000, + "fee": 7000000, + "height": 5, + "id": "0436430c-2b02-624c-2032-570501212b00", + "lock_height": 0, + "num_participants": 2, + "participant_data": [ + { + "id": 0, + "message": null, + "message_sig": null, + "part_sig": null, + "public_blind_excess": [ + 3, + 58, + 194, + 21, + 143, + 160, + 7, + 127, + 8, + 125, + 230, + 12, + 25, + 216, + 228, + 49, + 117, + 59, + 170, + 91, + 99, + 182, + 225, + 71, + 127, + 5, + 162, + 166, + 231, + 25, + 13, + 69, + 146 + ], + "public_nonce": [ + 3, + 27, + 132, + 197, + 86, + 123, + 18, + 100, + 64, + 153, + 93, + 62, + 213, + 170, + 186, + 5, + 101, + 215, + 30, + 24, + 52, + 96, + 72, + 25, + 255, + 156, + 23, + 245, + 233, + 213, + 221, + 7, + 143 + ] + }, + { + "id": 1, + "message": "Thanks, Yeastplume", + "message_sig": [ + 143, + 7, + 221, + 213, + 233, + 245, + 23, + 156, + 255, + 25, + 72, + 96, + 52, + 24, + 30, + 215, + 101, + 5, + 186, + 170, + 213, + 62, + 93, + 153, + 64, + 100, + 18, + 123, + 86, + 197, + 132, + 27, + 48, + 161, + 241, + 178, + 30, + 173, + 225, + 180, + 189, + 33, + 30, + 31, + 19, + 127, + 189, + 188, + 161, + 183, + 141, + 196, + 61, + 162, + 27, + 22, + 149, + 246, + 160, + 237, + 242, + 67, + 127, + 249 + ], + "part_sig": [ + 143, + 7, + 221, + 213, + 233, + 245, + 23, + 156, + 255, + 25, + 72, + 96, + 52, + 24, + 30, + 215, + 101, + 5, + 186, + 170, + 213, + 62, + 93, + 153, + 64, + 100, + 18, + 123, + 86, + 197, + 132, + 27, + 43, + 53, + 189, + 40, + 223, + 210, + 38, + 158, + 6, + 112, + 224, + 207, + 146, + 112, + 189, + 109, + 242, + 208, + 63, + 189, + 100, + 82, + 62, + 228, + 174, + 98, + 35, + 150, + 5, + 91, + 150, + 252 + ], + "public_blind_excess": [ + 3, + 143, + 224, + 68, + 50, + 67, + 218, + 177, + 115, + 192, + 104, + 239, + 95, + 168, + 145, + 178, + 66, + 210, + 181, + 235, + 137, + 14, + 160, + 148, + 117, + 230, + 227, + 129, + 23, + 4, + 66, + 238, + 22 + ], + "public_nonce": [ + 3, + 27, + 132, + 197, + 86, + 123, + 18, + 100, + 64, + 153, + 93, + 62, + 213, + 170, + 186, + 5, + 101, + 215, + 30, + 24, + 52, + 96, + 72, + 25, + 255, + 156, + 23, + 245, + 233, + 213, + 221, + 7, + 143 + ] + } + ], + "tx": { + "body": { + "inputs": [ + { + "commit": [ + 8, + 125, + 243, + 35, + 4, + 197, + 212, + 174, + 139, + 42, + 240, + 188, + 49, + 231, + 0, + 1, + 157, + 114, + 41, + 16, + 239, + 135, + 221, + 78, + 236, + 49, + 151, + 184, + 11, + 32, + 126, + 48, + 69 + ], + "features": "Coinbase" + }, + { + "commit": [ + 8, + 225, + 218, + 158, + 109, + 196, + 214, + 232, + 8, + 167, + 24, + 178, + 241, + 16, + 169, + 145, + 221, + 119, + 93, + 101, + 206, + 90, + 228, + 8, + 164, + 225, + 240, + 2, + 164, + 150, + 26, + 169, + 231 + ], + "features": "Coinbase" + } + ], + "kernels": [ + { + "excess": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "excess_sig": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "features": "Plain", + "fee": 7000000, + "lock_height": 0 + } + ], + "outputs": [ + { + "commit": [ + 8, + 78, + 233, + 125, + 239, + 168, + 195, + 113, + 36, + 212, + 198, + 155, + 170, + 117, + 62, + 37, + 50, + 83, + 95, + 170, + 129, + 247, + 158, + 165, + 224, + 72, + 157, + 178, + 82, + 151, + 213, + 190, + 184 + ], + "features": "Plain", + "proof": [ + 191, + 251, + 38, + 231, + 223, + 75, + 247, + 83, + 244, + 216, + 232, + 16, + 198, + 127, + 181, + 16, + 107, + 23, + 70, + 193, + 135, + 15, + 92, + 185, + 101, + 133, + 83, + 126, + 184, + 226, + 246, + 107, + 55, + 46, + 208, + 95, + 211, + 90, + 225, + 140, + 110, + 133, + 21, + 205, + 159, + 42, + 170, + 232, + 93, + 90, + 118, + 85, + 54, + 28, + 106, + 133, + 115, + 226, + 15, + 189, + 253, + 218, + 110, + 10, + 11, + 37, + 129, + 127, + 192, + 219, + 35, + 220, + 37, + 41, + 115, + 130, + 175, + 55, + 150, + 89, + 216, + 70, + 189, + 128, + 68, + 248, + 7, + 196, + 103, + 114, + 39, + 8, + 211, + 163, + 121, + 123, + 132, + 252, + 235, + 9, + 235, + 41, + 241, + 28, + 119, + 183, + 156, + 124, + 147, + 197, + 120, + 208, + 109, + 149, + 181, + 141, + 132, + 89, + 48, + 83, + 30, + 92, + 172, + 99, + 70, + 209, + 55, + 62, + 225, + 197, + 219, + 105, + 193, + 77, + 10, + 161, + 169, + 194, + 46, + 24, + 125, + 195, + 70, + 21, + 108, + 70, + 133, + 64, + 173, + 22, + 106, + 4, + 144, + 45, + 63, + 175, + 53, + 126, + 211, + 26, + 80, + 119, + 93, + 39, + 73, + 19, + 204, + 201, + 186, + 151, + 108, + 163, + 151, + 126, + 24, + 243, + 131, + 178, + 15, + 12, + 208, + 42, + 8, + 102, + 183, + 180, + 72, + 71, + 191, + 187, + 163, + 92, + 9, + 159, + 94, + 186, + 156, + 151, + 71, + 202, + 217, + 97, + 3, + 51, + 33, + 146, + 95, + 62, + 10, + 212, + 62, + 53, + 122, + 174, + 204, + 80, + 152, + 155, + 187, + 203, + 91, + 68, + 234, + 213, + 143, + 227, + 89, + 197, + 153, + 3, + 83, + 12, + 88, + 191, + 28, + 154, + 111, + 159, + 177, + 32, + 163, + 73, + 46, + 131, + 95, + 171, + 192, + 27, + 184, + 179, + 27, + 82, + 177, + 90, + 206, + 71, + 133, + 160, + 140, + 62, + 169, + 168, + 43, + 209, + 92, + 65, + 199, + 68, + 84, + 66, + 134, + 177, + 20, + 177, + 190, + 115, + 63, + 166, + 35, + 115, + 0, + 207, + 45, + 201, + 158, + 138, + 246, + 248, + 85, + 123, + 217, + 160, + 131, + 186, + 89, + 204, + 26, + 80, + 11, + 223, + 186, + 34, + 139, + 83, + 120, + 90, + 127, + 219, + 245, + 118, + 247, + 220, + 224, + 53, + 118, + 144, + 88, + 188, + 118, + 68, + 4, + 30, + 197, + 115, + 20, + 133, + 229, + 100, + 30, + 172, + 92, + 117, + 166, + 235, + 87, + 228, + 171, + 194, + 135, + 176, + 190, + 142, + 171, + 119, + 199, + 232, + 165, + 18, + 46, + 232, + 212, + 159, + 2, + 241, + 3, + 163, + 175, + 111, + 227, + 139, + 143, + 206, + 205, + 26, + 169, + 187, + 52, + 43, + 62, + 17, + 15, + 64, + 3, + 238, + 108, + 119, + 30, + 217, + 52, + 1, + 202, + 52, + 56, + 220, + 240, + 215, + 81, + 163, + 109, + 187, + 122, + 122, + 69, + 211, + 39, + 9, + 82, + 86, + 134, + 243, + 210, + 229, + 245, + 66, + 199, + 71, + 201, + 199, + 69, + 254, + 80, + 205, + 120, + 154, + 10, + 165, + 84, + 25, + 147, + 74, + 255, + 243, + 99, + 4, + 77, + 60, + 63, + 95, + 118, + 105, + 235, + 185, + 242, + 36, + 91, + 68, + 155, + 253, + 196, + 224, + 157, + 251, + 22, + 97, + 85, + 36, + 133, + 16, + 122, + 251, + 217, + 162, + 181, + 113, + 160, + 100, + 123, + 31, + 195, + 48, + 8, + 154, + 101, + 228, + 181, + 223, + 7, + 245, + 143, + 26, + 156, + 17, + 195, + 218, + 81, + 213, + 108, + 216, + 84, + 242, + 39, + 197, + 17, + 29, + 37, + 202, + 140, + 75, + 236, + 75, + 176, + 251, + 203, + 74, + 35, + 252, + 50, + 136, + 65, + 132, + 35, + 221, + 6, + 73, + 215, + 49, + 182, + 166, + 192, + 136, + 81, + 149, + 78, + 169, + 32, + 4, + 108, + 230, + 122, + 65, + 20, + 211, + 92, + 56, + 118, + 194, + 83, + 97, + 231, + 169, + 148, + 116, + 170, + 4, + 53, + 74, + 78, + 208, + 85, + 95, + 155, + 239, + 82, + 125, + 144, + 47, + 187, + 13, + 29, + 92, + 43, + 66, + 245, + 238, + 165, + 206, + 211, + 89, + 0, + 81, + 33, + 22, + 127, + 153, + 8, + 114, + 153, + 57, + 219, + 166, + 16, + 205, + 171, + 202, + 65, + 247, + 20, + 225, + 68, + 171, + 20, + 143, + 174, + 199, + 127, + 77, + 112, + 86, + 98, + 135, + 103, + 30, + 103, + 134, + 69, + 155, + 215, + 209, + 103, + 135, + 162, + 78, + 18, + 242, + 50, + 139, + 159, + 170, + 177, + 199, + 172, + 128, + 169, + 22, + 210, + 248, + 63, + 18, + 167, + 53, + 26, + 43, + 237, + 255, + 97, + 13, + 51, + 223, + 178, + 223, + 125, + 142, + 87, + 182, + 143, + 180, + 165, + 220, + 192, + 216, + 228, + 250, + 128, + 123, + 32, + 119, + 135, + 122, + 169, + 107, + 167, + 188, + 34, + 230, + 39, + 164, + 246, + 163, + 8, + 211, + 171, + 192, + 145, + 245, + 109, + 81, + 130, + 88, + 240, + 115, + 204, + 27, + 112, + 239, + 129 + ] + }, + { + "commit": [ + 8, + 18, + 39, + 108, + 199, + 136, + 230, + 135, + 6, + 18, + 41, + 109, + 146, + 108, + 186, + 159, + 14, + 123, + 152, + 16, + 103, + 7, + 16, + 181, + 166, + 230, + 241, + 186, + 0, + 109, + 57, + 87, + 116 + ], + "features": "Plain", + "proof": [ + 220, + 255, + 97, + 117, + 57, + 12, + 96, + 43, + 250, + 146, + 194, + 255, + 209, + 169, + 178, + 216, + 77, + 204, + 158, + 169, + 65, + 246, + 243, + 23, + 189, + 208, + 248, + 117, + 36, + 78, + 242, + 62, + 105, + 111, + 209, + 124, + 113, + 223, + 121, + 118, + 12, + 229, + 206, + 26, + 150, + 170, + 177, + 209, + 93, + 208, + 87, + 53, + 141, + 200, + 53, + 233, + 114, + 254, + 190, + 184, + 109, + 80, + 204, + 236, + 13, + 173, + 124, + 254, + 2, + 70, + 215, + 66, + 235, + 117, + 60, + 247, + 184, + 140, + 4, + 93, + 21, + 188, + 113, + 35, + 248, + 207, + 113, + 85, + 100, + 124, + 207, + 102, + 63, + 202, + 146, + 168, + 60, + 154, + 101, + 208, + 237, + 117, + 110, + 167, + 235, + 255, + 210, + 202, + 201, + 12, + 56, + 10, + 16, + 46, + 217, + 202, + 170, + 53, + 93, + 23, + 94, + 208, + 191, + 88, + 211, + 172, + 47, + 94, + 144, + 157, + 108, + 68, + 125, + 252, + 107, + 96, + 94, + 4, + 146, + 92, + 43, + 23, + 195, + 62, + 189, + 25, + 8, + 201, + 101, + 165, + 84, + 30, + 165, + 210, + 237, + 69, + 160, + 149, + 142, + 100, + 2, + 248, + 157, + 122, + 86, + 223, + 25, + 146, + 224, + 54, + 216, + 54, + 231, + 64, + 23, + 231, + 60, + 202, + 213, + 203, + 58, + 130, + 184, + 225, + 57, + 227, + 9, + 121, + 42, + 49, + 177, + 95, + 63, + 253, + 114, + 237, + 3, + 50, + 83, + 66, + 140, + 21, + 108, + 43, + 151, + 153, + 69, + 138, + 37, + 193, + 218, + 101, + 183, + 25, + 120, + 10, + 34, + 222, + 127, + 231, + 244, + 55, + 174, + 47, + 204, + 210, + 44, + 247, + 234, + 53, + 122, + 181, + 170, + 102, + 165, + 239, + 125, + 113, + 251, + 13, + 198, + 74, + 160, + 181, + 118, + 31, + 104, + 39, + 128, + 98, + 187, + 57, + 187, + 41, + 108, + 120, + 126, + 76, + 171, + 197, + 226, + 162, + 147, + 58, + 65, + 108, + 225, + 201, + 169, + 105, + 97, + 96, + 56, + 100, + 73, + 196, + 55, + 233, + 18, + 15, + 123, + 178, + 110, + 91, + 14, + 116, + 209, + 242, + 231, + 213, + 188, + 215, + 170, + 251, + 42, + 146, + 184, + 125, + 21, + 72, + 241, + 249, + 17, + 251, + 6, + 175, + 123, + 214, + 204, + 19, + 206, + 226, + 159, + 124, + 156, + 183, + 144, + 33, + 174, + 209, + 129, + 134, + 39, + 42, + 240, + 233, + 209, + 137, + 236, + 16, + 124, + 129, + 168, + 163, + 174, + 180, + 120, + 43, + 13, + 149, + 14, + 72, + 129, + 170, + 81, + 183, + 118, + 187, + 104, + 68, + 178, + 91, + 206, + 151, + 3, + 91, + 72, + 169, + 189, + 178, + 174, + 163, + 96, + 134, + 135, + 188, + 221, + 71, + 157, + 79, + 169, + 152, + 181, + 168, + 57, + 255, + 136, + 85, + 142, + 74, + 41, + 223, + 240, + 237, + 19, + 181, + 89, + 0, + 171, + 181, + 212, + 57, + 183, + 7, + 147, + 217, + 2, + 174, + 154, + 211, + 69, + 135, + 177, + 140, + 145, + 159, + 107, + 135, + 92, + 145, + 209, + 77, + 238, + 177, + 195, + 115, + 245, + 231, + 101, + 112, + 213, + 154, + 101, + 73, + 117, + 143, + 101, + 95, + 17, + 40, + 165, + 79, + 22, + 45, + 254, + 136, + 104, + 225, + 88, + 112, + 40, + 226, + 106, + 217, + 30, + 82, + 140, + 90, + 231, + 238, + 147, + 53, + 250, + 88, + 251, + 89, + 2, + 43, + 93, + 226, + 157, + 128, + 240, + 118, + 74, + 153, + 23, + 57, + 13, + 70, + 219, + 137, + 154, + 204, + 106, + 91, + 65, + 110, + 37, + 236, + 201, + 220, + 203, + 113, + 83, + 100, + 106, + 221, + 204, + 129, + 202, + 219, + 95, + 0, + 120, + 254, + 188, + 126, + 5, + 215, + 115, + 90, + 186, + 73, + 79, + 57, + 239, + 5, + 105, + 123, + 188, + 201, + 180, + 123, + 44, + 204, + 121, + 89, + 93, + 117, + 252, + 19, + 200, + 6, + 120, + 181, + 226, + 55, + 237, + 206, + 88, + 215, + 49, + 243, + 76, + 5, + 177, + 221, + 202, + 166, + 73, + 172, + 242, + 216, + 101, + 187, + 188, + 60, + 237, + 161, + 5, + 8, + 188, + 221, + 41, + 208, + 73, + 103, + 68, + 100, + 75, + 241, + 195, + 81, + 111, + 102, + 135, + 223, + 238, + 245, + 100, + 156, + 125, + 255, + 144, + 98, + 125, + 100, + 39, + 57, + 165, + 157, + 145, + 168, + 209, + 208, + 196, + 220, + 85, + 215, + 74, + 148, + 158, + 16, + 116, + 66, + 118, + 100, + 180, + 103, + 153, + 44, + 158, + 15, + 125, + 58, + 249, + 214, + 234, + 121, + 81, + 62, + 137, + 70, + 221, + 192, + 211, + 86, + 186, + 196, + 152, + 120, + 230, + 78, + 106, + 149, + 176, + 163, + 2, + 20, + 33, + 79, + 175, + 44, + 227, + 23, + 250, + 98, + 47, + 243, + 38, + 107, + 50, + 168, + 22, + 225, + 10, + 24, + 230, + 215, + 137, + 165, + 218, + 31, + 35, + 230, + 123, + 79, + 151, + 10, + 104, + 167, + 188, + 217, + 225, + 136, + 37, + 238, + 39, + 75, + 4, + 131, + 137, + 106, + 64 + ] + } + ] + }, + "offset": [ + 210, + 2, + 150, + 73, + 0, + 0, + 0, + 0, + 211, + 2, + 150, + 73, + 0, + 0, + 0, + 0, + 212, + 2, + 150, + 73, + 0, + 0, + 0, + 0, + 213, + 2, + 150, + 73, + 0, + 0, + 0, + 0 + ] + }, + "version": 1 + } + } +} diff --git a/config/Cargo.toml b/config/Cargo.toml new file mode 100644 index 0000000..e3e6ee7 --- /dev/null +++ b/config/Cargo.toml @@ -0,0 +1,45 @@ +[package] +name = "grin_wallet_config" +version = "5.4.0-alpha.1" +authors = ["Grin Developers "] +description = "Configuration for grin wallet , a simple, private and scalable cryptocurrency implementation based on the MimbleWimble chain format." +license = "Apache-2.0" +repository = "https://github.com/mimblewimble/grin-wallet" +keywords = [ "crypto", "grin", "mimblewimble" ] +workspace = ".." +edition = "2018" + +[dependencies] +rand = "0.6" +serde = "1" +serde_derive = "1" +toml = "0.5" +dirs = "2.0" +log = "0.4" + +grin_wallet_util = { path = "../util", version = "5.4.0-alpha.1" } + +##### Grin Imports + +# For Release +grin_core = "5.3.3" +grin_util = "5.3.3" + +# For beta release + +#grin_core = { git = "https://github.com/mimblewimble/grin", tag = "v5.2.0-beta.3"} +#grin_util = { git = "https://github.com/mimblewimble/grin", tag = "v5.2.0-beta.3" } + +# For bleeding edge +# grin_core = { git = "https://github.com/mimblewimble/grin", branch = "master" } +# grin_util = { git = "https://github.com/mimblewimble/grin", branch = "master" } + +# For local testing +# grin_core = { path = "../../grin/core"} +# grin_util = { path = "../../grin/util"} + +##### + +[dev-dependencies] +pretty_assertions = "0.6" + diff --git a/config/src/comments.rs b/config/src/comments.rs new file mode 100644 index 0000000..c027042 --- /dev/null +++ b/config/src/comments.rs @@ -0,0 +1,396 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Comments for configuration + injection into output .toml +use std::collections::HashMap; + +/// maps entries to Comments that should precede them +fn comments() -> HashMap { + let mut retval = HashMap::new(); + + retval.insert( + "config_file_version".to_string(), + " +#Version of the Generated Configuration File for the Grin Wallet (DO NOT EDIT) +" + .to_string(), + ); + + retval.insert( + "[wallet]".to_string(), + " +######################################### +### WALLET CONFIGURATION ### +######################################### +" + .to_string(), + ); + + retval.insert( + "api_listen_port".to_string(), + " +#path of TLS certificate file, self-signed certificates are not supported +#tls_certificate_file = \"\" +#private key for the TLS certificate +#tls_certificate_key = \"\" + +#port for wallet listener +" + .to_string(), + ); + + retval.insert( + "owner_api_listen_port".to_string(), + " +#port for wallet owner api +" + .to_string(), + ); + + retval.insert( + "api_secret_path".to_string(), + " +#path of the secret token used by the API to authenticate the calls +#comment it to disable basic auth +" + .to_string(), + ); + retval.insert( + "check_node_api_http_addr".to_string(), + " +#where the wallet should find a running node +" + .to_string(), + ); + retval.insert( + "node_api_secret_path".to_string(), + " +#location of the node api secret for basic auth on the Grin API +" + .to_string(), + ); + retval.insert( + "owner_api_include_foreign".to_string(), + " +#include the foreign API endpoints on the same port as the owner +#API. Useful for networking environments like AWS ECS that make +#it difficult to access multiple ports on a single service. +" + .to_string(), + ); + retval.insert( + "data_file_dir".to_string(), + " +#where to find wallet files (seed, data, etc) +" + .to_string(), + ); + retval.insert( + "no_commit_cache".to_string(), + " +#If true, don't store calculated commits in the database +#better privacy, but at a performance cost of having to +#re-calculate commits every time they're used +" + .to_string(), + ); + retval.insert( + "dark_background_color_scheme".to_string(), + " +#Whether to use the black background color scheme for command line +" + .to_string(), + ); + retval.insert( + "accept_fee_base".to_string(), + " +#Minimum acceptable fee per unit of transaction weight +" + .to_string(), + ); + retval.insert( + "[logging]".to_string(), + " +#Type of proxy, eg \"socks4\", \"socks5\", \"http\", \"https\" +#transport = \"https\" + +#Proxy address, eg IP:PORT or Hostname +#server = \"\" + +#Username for the proxy server authentification +#user = \"\" + +#Password for the proxy server authentification +#pass = \"\" + +#This computer goes through a firewall that only allows connections to certain ports (Optional) +#allowed_port = [80, 443] + + +######################################### +### LOGGING CONFIGURATION ### +######################################### +" + .to_string(), + ); + + retval.insert( + "log_to_stdout".to_string(), + " +#whether to log to stdout +" + .to_string(), + ); + + retval.insert( + "stdout_log_level".to_string(), + " +#log level for stdout: Error, Warning, Info, Debug, Trace +" + .to_string(), + ); + + retval.insert( + "log_to_file".to_string(), + " +#whether to log to a file +" + .to_string(), + ); + + retval.insert( + "file_log_level".to_string(), + " +#log level for file: Error, Warning, Info, Debug, Trace +" + .to_string(), + ); + + retval.insert( + "log_file_path".to_string(), + " +#log file path +" + .to_string(), + ); + + retval.insert( + "log_file_append".to_string(), + " +#whether to append to the log file (true), or replace it on every run (false) +" + .to_string(), + ); + + retval.insert( + "log_max_size".to_string(), + " +#maximum log file size in bytes before performing log rotation +#comment it to disable log rotation +" + .to_string(), + ); + + retval.insert( + "[tor]".to_string(), + " +######################################### +### TOR CONFIGURATION (Experimental) ### +######################################### +" + .to_string(), + ); + + retval.insert( + "skip_send_attempt".to_string(), + " +#Whether to skip send attempts (used for debugging) +" + .to_string(), + ); + + retval.insert( + "use_tor_listener".to_string(), + " +#Whether to start tor listener on listener startup (default true) +" + .to_string(), + ); + + retval.insert( + "socks_proxy_addr".to_string(), + " +#Address of the running TOR (SOCKS) server +" + .to_string(), + ); + + retval.insert( + "send_config_dir".to_string(), + " +#Directory to output TOR configuration to when sending +" + .to_string(), + ); + + retval.insert( + "[tor.bridge]".to_string(), + " +######################################### +### TOR BRIDGE ### +######################################### +" + .to_string(), + ); + + retval.insert( + "[tor.proxy]".to_string(), + " +#Tor bridge relay: allow to send and receive via TOR in a country where it is censored. +#Enable it by entering a single bridge line. To disable it, you must comment it. +#Support of the transport: obfs4, meek and snowflake. +#obfs4proxy or snowflake client binary must be installed and on your path. +#For example, the bridge line must be in the following format for obfs4 transport: \"obfs4 [IP:PORT] [FINGERPRINT] cert=[CERT] iat-mode=[IAT-MODE]\" +#bridge_line = \"\" + +#Plugging client option, needed only for snowflake (let it empty if you want to use the default option of tor) or debugging purpose +#client_option = \"\" + + +######################################### +### TOR PROXY ### +######################################### +" + .to_string(), + ); + + retval +} + +fn get_key(line: &str) -> String { + if line.contains('[') && line.contains(']') { + line.to_owned() + } else if line.contains('=') { + line.split('=').collect::>()[0].trim().to_owned() + } else { + "NOT_FOUND".to_owned() + } +} + +pub fn insert_comments(orig: String) -> String { + let comments = comments(); + let lines: Vec<&str> = orig.split('\n').collect(); + let mut out_lines = vec![]; + for l in lines { + let key = get_key(l); + if let Some(v) = comments.get(&key) { + out_lines.push(v.to_owned()); + } + out_lines.push(l.to_owned()); + out_lines.push("\n".to_owned()); + } + let mut ret_val = String::from(""); + for l in out_lines { + ret_val.push_str(&l); + } + ret_val +} + +pub fn migrate_comments( + old_config: String, + new_config: String, + old_version: Option, +) -> String { + let comments = comments(); + // Prohibe the key we are basing on to introduce new comments for [tor.proxy] + let prohibited_key = match old_version { + None => vec!["[logging]"], + Some(_) => vec![], + }; + let mut vec_old_conf = vec![]; + let mut hm_key_cmt_old = HashMap::new(); + let old_conf: Vec<&str> = old_config.split_inclusive('\n').collect(); + // collect old key in a vec and insert old key/comments from the old conf in a hashmap + let vec_key_old = old_conf + .iter() + .filter_map(|line| { + let line_nospace = line.trim(); + let is_ascii_control = line_nospace.chars().all(|x| x.is_ascii_control()); + match line.contains("#") || is_ascii_control { + true => { + vec_old_conf.push(line.to_owned()); + None + } + false => { + let comments: String = vec_old_conf.iter().flat_map(|s| s.chars()).collect(); + let key = get_key(line_nospace); + match key != "NOT_FOUND" { + true => { + vec_old_conf.clear(); + hm_key_cmt_old.insert(key.clone(), comments); + Some(key) + } + false => None, + } + } + } + }) + .collect::>(); + + let new_conf: Vec<&str> = new_config.split_inclusive('\n').collect(); + // collect new key and the whole key line from the new config + let vec_key_cmt_new = new_conf + .iter() + .filter_map(|line| { + let line_nospace = line.trim(); + let is_ascii_control = line_nospace.chars().all(|x| x.is_ascii_control()); + match !line.contains("#") && !is_ascii_control { + true => { + let key = get_key(line_nospace); + match key != "NOT_FOUND" { + true => Some((key, line_nospace.to_string())), + false => None, + } + } + false => None, + } + }) + .collect::>(); + + let mut new_config_str = String::from(""); + // Merging old comments in the new config (except if the key is contained in the prohibited vec) with all new introduced key comments + for (key, key_line) in vec_key_cmt_new { + let old_key_exist = vec_key_old.iter().any(|old_key| *old_key == key); + let key_fmt = format!("{}\n", key_line); + if old_key_exist { + if prohibited_key.contains(&key.as_str()) { + // push new config key/comments + let value = comments.get(&key).unwrap(); + new_config_str.push_str(value); + new_config_str.push_str(&key_fmt); + } else { + // push old config key/comment + let value = hm_key_cmt_old.get(&key).unwrap(); + new_config_str.push_str(value); + new_config_str.push_str(&key_fmt); + } + } else { + // old key does not exist, we push new key/comments + let value = comments.get(&key).unwrap(); + new_config_str.push_str(value); + new_config_str.push_str(&key_fmt); + } + } + new_config_str +} diff --git a/config/src/config.rs b/config/src/config.rs new file mode 100644 index 0000000..f64a396 --- /dev/null +++ b/config/src/config.rs @@ -0,0 +1,478 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Configuration file management + +use crate::comments::{insert_comments, migrate_comments}; +use crate::core::global; +use crate::types::{ + ConfigError, GlobalWalletConfig, GlobalWalletConfigMembers, TorBridgeConfig, TorProxyConfig, +}; +use crate::types::{TorConfig, WalletConfig}; +use crate::util::logger::LoggingConfig; +use rand::distributions::{Alphanumeric, Distribution}; +use rand::thread_rng; +use std::env; +use std::fs::{self, File}; +use std::io::prelude::*; +use std::io::BufReader; +use std::path::{Path, PathBuf}; +use toml; + +/// Wallet configuration file name +pub const WALLET_CONFIG_FILE_NAME: &str = "grin-wallet.toml"; +const WALLET_LOG_FILE_NAME: &str = "grin-wallet.log"; +/// .grin folder, usually in home/.grin +pub const GRIN_HOME: &str = ".grin"; +/// Wallet data directory +pub const GRIN_WALLET_DIR: &str = "wallet_data"; +/// Node API secret +pub const API_SECRET_FILE_NAME: &str = ".foreign_api_secret"; +/// Owner API secret +pub const OWNER_API_SECRET_FILE_NAME: &str = ".owner_api_secret"; + +/// Function to locate the wallet dir and wallet.toml in the order +/// a) config in top-dir if provided, b) in working dir, c) default dir +/// Function to get wallet dir and create dirs if not existing +pub fn get_wallet_path( + chain_type: &global::ChainTypes, + create_path: bool, +) -> Result { + // A - Detect grin-wallet.toml in working dir + let mut config_path = std::env::current_dir()?; + config_path.push(WALLET_CONFIG_FILE_NAME); + if create_path == false && config_path.exists() { + config_path.pop(); + println!("Detected 'wallet.toml' in working dir - opening associated wallet"); + return Ok(config_path); + }; + // B - Select home directory + let mut wallet_path = match dirs::home_dir() { + Some(p) => p, + None => PathBuf::new(), + }; + wallet_path.push(GRIN_HOME); + wallet_path.push(chain_type.shortname()); + // Create if the default path doesn't exist + if !wallet_path.exists() && create_path { + fs::create_dir_all(wallet_path.clone())?; + } + // Throw an error if the path still does not exist + if !wallet_path.exists() { + Err(ConfigError::PathNotFoundError(String::from( + wallet_path.to_str().unwrap(), + ))) + } else { + Ok(wallet_path) + } +} + +/// Smart function to detect the the nodes .api_secret in the order +/// a) top-dir, b) home directory, create directory if needed +pub fn get_node_path( + data_path: Option, + chain_type: &global::ChainTypes, +) -> Result { + let node_path = match data_path { + // 1) A If top dir provided and api_secret exist, return top dir + Some(path) => { + let mut node_path = path; + node_path.push(GRIN_HOME); + node_path.push(chain_type.shortname()); + node_path.push(API_SECRET_FILE_NAME); + if node_path.exists() { + node_path.pop(); + Ok(node_path) + // 1) B If top dir exists, but no api_secret, return home dir + } else { + let mut node_path = match dirs::home_dir() { + Some(p) => p, + None => PathBuf::new(), + }; + node_path.push(GRIN_HOME); + node_path.push(chain_type.shortname()); + Ok(node_path) + } + } + // 2) If there is no top_dir provided, always return home dir + None => { + let mut node_path = match dirs::home_dir() { + Some(p) => p, + None => PathBuf::new(), + }; + node_path.push(GRIN_HOME); + node_path.push(chain_type.shortname()); + Ok(node_path) + } + }; + node_path +} + +/// Checks if config in current working dir +fn check_config_current_dir(path: &str) -> Option { + let p = env::current_dir(); + let mut c = match p { + Ok(c) => c, + Err(_) => { + return None; + } + }; + c.push(path); + if c.exists() { + return Some(c); + } + None +} + +/// Whether a config file exists at the given directory +pub fn config_file_exists(path: &str) -> bool { + let mut path = PathBuf::from(path); + path.push(WALLET_CONFIG_FILE_NAME); + path.exists() +} + +/// Create file with api secret +pub fn init_api_secret(api_secret_path: &PathBuf) -> Result<(), ConfigError> { + let mut api_secret_file = File::create(api_secret_path)?; + let api_secret: String = Alphanumeric + .sample_iter(&mut thread_rng()) + .take(20) + .collect(); + api_secret_file.write_all(api_secret.as_bytes())?; + Ok(()) +} + +/// Check if file contains a secret and nothing else +pub fn check_api_secret(api_secret_path: &PathBuf) -> Result<(), ConfigError> { + let api_secret_file = File::open(api_secret_path)?; + let buf_reader = BufReader::new(api_secret_file); + let mut lines_iter = buf_reader.lines(); + let first_line = lines_iter.next(); + if first_line.is_none() || first_line.unwrap().is_err() { + fs::remove_file(api_secret_path)?; + init_api_secret(api_secret_path)?; + } + Ok(()) +} + +/// Check that the api secret file exists and is valid +fn check_api_secret_file( + chain_type: &global::ChainTypes, + data_path: Option, + file_name: &str, +) -> Result<(), ConfigError> { + let grin_path = match data_path { + Some(p) => p, + None => get_node_path(data_path, chain_type)?, + }; + let mut api_secret_path = grin_path; + api_secret_path.push(file_name); + if !api_secret_path.exists() { + init_api_secret(&api_secret_path) + } else { + check_api_secret(&api_secret_path) + } +} + +/// Initial wallet setup does the following +/// 1) Load wallet config if run without 'init' 2) create wallet if run with 'init'' +/// Try in thiss order a) current dir as template, b) in top path, or c) .grin home +/// - load default config values +/// - update the wallet and node dir to the correct paths +/// - if grin-wallet.toml exists, but the wallet data dir does not, load config and continue wallet generation +/// - Automatically detect grin-wallet.toml in current directory +pub fn initial_setup_wallet( + chain_type: &global::ChainTypes, + mut data_path: Option, + create_path: bool, +) -> Result { + // Fixing the input path when run with -here or -t (top-dir) + // - Fix top-dir path to compensate for bug on Linux to handle "\" + // - Convert top-dir path to be always absolute for config generation + // - Fix for Windows 10/11 to strip the '\\?\' prefix added to the path + if let Some(p) = &data_path { + if let Some(p_str) = p.to_str() { + let fixed_str = p_str.replace("\\", "/"); + let fixed_path = PathBuf::from(fixed_str); + if create_path { + fs::create_dir_all(&fixed_path)?; + } + let absolute_path = if fixed_path.is_absolute() { + fixed_path.canonicalize()? + } else { + env::current_dir()?.join(&fixed_path).canonicalize()? + }; + let absolute_path = + std::path::PathBuf::from(absolute_path.to_str().unwrap().replace(r"\\?\", "")); + data_path = Some(absolute_path); // Store the updated path + } + } + + // Get wallet data_dir path if none provided + let wallet_path = match data_path { + Some(p) => p, + None => get_wallet_path(chain_type, create_path)?, + }; + println!("wallet path: {}", wallet_path.display()); + // Get path to the node directory, + let node_path = get_node_path(Some(wallet_path.clone()), chain_type)?; + + // Get config path and data path + let mut config_path = wallet_path.clone(); + config_path.push(WALLET_CONFIG_FILE_NAME); + let mut data_dir = wallet_path.clone(); + data_dir.push(GRIN_WALLET_DIR); + // Check if a config exists in theworking dir, if so load it + let (path, config) = match config_path.clone().exists() { + // If the config does not exist, load default and updated node and wallet dir + false => { + let mut default_config = GlobalWalletConfig::for_chain(chain_type); + default_config.config_file_path = Some(config_path.clone()); + default_config.update_paths(&wallet_path, &node_path); + + // Write config file + let res = + default_config.write_to_file(config_path.to_str().unwrap(), false, None, None); + + if let Err(e) = res { + let msg = format!( + "Error creating config file as ({}): {}", + config_path.to_str().unwrap(), + e + ); + return Err(ConfigError::SerializationError(msg)); + } + + (wallet_path, default_config) + } + + // Return config if not run with init + true => { + // If run with init and seed does not yet exists, continue, else throw error + if data_dir.exists() && create_path == true { + let msg = format!( + "{} already exists in the target directory ({}). Please remove it first", + config_path.to_str().unwrap(), + data_dir.to_str().unwrap(), + ); + return Err(ConfigError::SerializationError(msg)); + } else { + let config = GlobalWalletConfig::new(config_path.to_str().unwrap())?; + (wallet_path, config) + } + } + }; + + // Check API secrets, if ok, return config + check_api_secret_file(chain_type, Some(path.clone()), OWNER_API_SECRET_FILE_NAME)?; + check_api_secret_file(chain_type, Some(path), API_SECRET_FILE_NAME)?; + + Ok(config) +} + +impl Default for GlobalWalletConfigMembers { + fn default() -> GlobalWalletConfigMembers { + GlobalWalletConfigMembers { + config_file_version: Some(2), + logging: Some(LoggingConfig::default()), + tor: Some(TorConfig::default()), + wallet: WalletConfig::default(), + } + } +} + +impl Default for GlobalWalletConfig { + fn default() -> GlobalWalletConfig { + GlobalWalletConfig { + config_file_path: None, + members: Some(GlobalWalletConfigMembers::default()), + } + } +} + +impl GlobalWalletConfig { + /// Same as GlobalConfig::default() but further tweaks parameters to + /// apply defaults for each chain type + pub fn for_chain(chain_type: &global::ChainTypes) -> GlobalWalletConfig { + let mut defaults_conf = GlobalWalletConfig::default(); + let defaults = &mut defaults_conf.members.as_mut().unwrap().wallet; + defaults.chain_type = Some(*chain_type); + + match *chain_type { + global::ChainTypes::Mainnet => {} + global::ChainTypes::Testnet => { + defaults.api_listen_port = 13415; + defaults.check_node_api_http_addr = "http://127.0.0.1:13413".to_owned(); + } + global::ChainTypes::UserTesting => { + defaults.api_listen_port = 23415; + defaults.check_node_api_http_addr = "http://127.0.0.1:23413".to_owned(); + } + _ => {} + } + defaults_conf + } + /// Requires the path to a config file + pub fn new(file_path: &str) -> Result { + let mut return_value = GlobalWalletConfig::default(); + return_value.config_file_path = Some(PathBuf::from(&file_path)); + + // Config file path is given but not valid + let config_file = return_value.config_file_path.clone().unwrap(); + if !config_file.exists() { + return Err(ConfigError::FileNotFoundError(String::from( + config_file.to_str().unwrap(), + ))); + } + + // Try to parse the config file if it exists, explode if it does exist but + // something's wrong with it + return_value.read_config() + } + + /// Read config + fn read_config(mut self) -> Result { + let config_file_path = self.config_file_path.as_mut().unwrap(); + let contents = fs::read_to_string(config_file_path.clone())?; + let migrated = GlobalWalletConfig::migrate_config_file_version_none_to_2( + contents, + config_file_path.to_owned(), + )?; + let fixed = GlobalWalletConfig::fix_warning_level(migrated); + let decoded: Result = toml::from_str(&fixed); + match decoded { + Ok(gc) => { + self.members = Some(gc); + Ok(self) + } + Err(e) => Err(ConfigError::ParseError( + String::from(self.config_file_path.as_mut().unwrap().to_str().unwrap()), + format!("{}", e), + )), + } + } + + /// Update paths + pub fn update_paths(&mut self, wallet_home: &PathBuf, node_home: &Path) { + let mut data_file_dir = wallet_home.to_path_buf(); + let mut node_secret_path = node_home.to_path_buf(); + let mut secret_path = wallet_home.to_path_buf(); + let mut log_path = wallet_home.to_path_buf(); + let tor_path = wallet_home.to_path_buf(); + node_secret_path.push(API_SECRET_FILE_NAME); + data_file_dir.push(GRIN_WALLET_DIR); + secret_path.push(OWNER_API_SECRET_FILE_NAME); + log_path.push(WALLET_LOG_FILE_NAME); + self.members.as_mut().unwrap().wallet.data_file_dir = + data_file_dir.to_str().unwrap().to_owned(); + self.members.as_mut().unwrap().wallet.node_api_secret_path = + Some(node_secret_path.to_str().unwrap().to_owned()); + self.members.as_mut().unwrap().wallet.api_secret_path = + Some(secret_path.to_str().unwrap().to_owned()); + self.members + .as_mut() + .unwrap() + .logging + .as_mut() + .unwrap() + .log_file_path = log_path.to_str().unwrap().to_owned(); + self.members + .as_mut() + .unwrap() + .tor + .as_mut() + .unwrap() + .send_config_dir = tor_path.to_str().unwrap().to_owned(); + } + + /// Serialize config + pub fn ser_config(&mut self) -> Result { + let encoded: Result = + toml::to_string(self.members.as_mut().unwrap()); + match encoded { + Ok(enc) => Ok(enc), + Err(e) => Err(ConfigError::SerializationError(format!("{}", e))), + } + } + + /// Write configuration to a file + pub fn write_to_file( + &mut self, + name: &str, + migration: bool, + old_config: Option, + old_version: Option, + ) -> Result<(), ConfigError> { + let conf_out = self.ser_config()?; + let commented_config = if migration { + migrate_comments(old_config.unwrap(), conf_out, old_version) + } else { + let fixed_config = GlobalWalletConfig::fix_log_level(conf_out); + insert_comments(fixed_config) + }; + let mut file = File::create(name)?; + file.write_all(commented_config.as_bytes())?; + Ok(()) + } + /// This migration does the following: + /// - Adds "config_file_version = 2" + /// - Introduce new key config_file_version, [tor.bridge] and [tor.proxy] + /// - Migrate old config key/value and comments while it does not conflict with newly indroduced key and comments + fn migrate_config_file_version_none_to_2( + config_str: String, + config_file_path: PathBuf, + ) -> Result { + let config: GlobalWalletConfigMembers = + toml::from_str(&GlobalWalletConfig::fix_warning_level(config_str.clone())).unwrap(); + if config.config_file_version.is_some() { + return Ok(config_str); + } + let adjusted_config = GlobalWalletConfigMembers { + config_file_version: GlobalWalletConfigMembers::default().config_file_version, + tor: Some(TorConfig { + bridge: TorBridgeConfig::default(), + proxy: TorProxyConfig::default(), + ..config.tor.unwrap_or_default() + }), + ..config + }; + let mut gc = GlobalWalletConfig { + members: Some(adjusted_config), + config_file_path: Some(config_file_path.clone()), + }; + let str_path = config_file_path.into_os_string().into_string().unwrap(); + gc.write_to_file( + &str_path, + true, + Some(config_str), + config.config_file_version, + )?; + let adjusted_config_str = fs::read_to_string(str_path.clone())?; + Ok(adjusted_config_str) + } + + // For forwards compatibility old config needs `Warning` log level changed to standard log::Level `WARN` + fn fix_warning_level(conf: String) -> String { + conf.replace("Warning", "WARN") + } + + // For backwards compatibility only first letter of log level should be capitalised. + fn fix_log_level(conf: String) -> String { + conf.replace("TRACE", "Trace") + .replace("DEBUG", "Debug") + .replace("INFO", "Info") + .replace("WARN", "Warning") + .replace("ERROR", "Error") + } +} diff --git a/config/src/lib.rs b/config/src/lib.rs new file mode 100644 index 0000000..188ca57 --- /dev/null +++ b/config/src/lib.rs @@ -0,0 +1,38 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Crate wrapping up the Grin binary and configuration file + +#![deny(non_upper_case_globals)] +#![deny(non_camel_case_types)] +#![deny(non_snake_case)] +#![deny(unused_mut)] +#![warn(missing_docs)] + +#[macro_use] +extern crate serde_derive; + +use grin_core as core; +use grin_util as util; + +mod comments; +pub mod config; +pub mod types; + +pub use crate::config::{ + config_file_exists, initial_setup_wallet, GRIN_WALLET_DIR, WALLET_CONFIG_FILE_NAME, +}; +pub use crate::types::{ + ConfigError, GlobalWalletConfig, GlobalWalletConfigMembers, TorConfig, WalletConfig, +}; diff --git a/config/src/types.rs b/config/src/types.rs new file mode 100644 index 0000000..6a57c54 --- /dev/null +++ b/config/src/types.rs @@ -0,0 +1,273 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Public types for config modules + +use std::fmt; +use std::io; +use std::path::PathBuf; + +use crate::core::global::ChainTypes; +use crate::util::logger::LoggingConfig; + +/// Command-line wallet configuration +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct WalletConfig { + /// Chain parameters (default to Mainnet if none at the moment) + pub chain_type: Option, + /// The port this wallet will run on + pub api_listen_port: u16, + /// The port this wallet's owner API will run on + pub owner_api_listen_port: Option, + /// Location of the secret for basic auth on the Owner API + pub api_secret_path: Option, + /// Location of the node api secret for basic auth on the Grin API + pub node_api_secret_path: Option, + /// The api address of a running server node against which transaction inputs + /// will be checked during send + pub check_node_api_http_addr: String, + /// Whether to include foreign API endpoints on the Owner API + pub owner_api_include_foreign: Option, + /// The directory in which wallet files are stored + pub data_file_dir: String, + /// If Some(true), don't cache commits alongside output data + /// speed improvement, but your commits are in the database + pub no_commit_cache: Option, + /// TLS certificate file + pub tls_certificate_file: Option, + /// TLS certificate private key file + pub tls_certificate_key: Option, + /// Whether to use the black background color scheme for command line + /// if enabled, wallet command output color will be suitable for black background terminal + pub dark_background_color_scheme: Option, + /// Scaling factor from transaction weight to transaction fee + /// should match accept_fee_base parameter in grin-server + pub accept_fee_base: Option, +} + +impl Default for WalletConfig { + fn default() -> WalletConfig { + WalletConfig { + chain_type: Some(ChainTypes::Mainnet), + api_listen_port: 3415, + owner_api_listen_port: Some(WalletConfig::default_owner_api_listen_port()), + api_secret_path: Some(".owner_api_secret".to_string()), + node_api_secret_path: Some(".foreign_api_secret".to_string()), + check_node_api_http_addr: "http://127.0.0.1:3413".to_string(), + owner_api_include_foreign: Some(false), + data_file_dir: ".".to_string(), + no_commit_cache: Some(false), + tls_certificate_file: None, + tls_certificate_key: None, + dark_background_color_scheme: Some(true), + accept_fee_base: None, + } + } +} + +impl WalletConfig { + /// API Listen address + pub fn api_listen_addr(&self) -> String { + format!("127.0.0.1:{}", self.api_listen_port) + } + + /// Default listener port + pub fn default_owner_api_listen_port() -> u16 { + 3420 + } + + /// Default listener port + pub fn default_accept_fee_base() -> u64 { + 500_000 + } + + /// Use value from config file, defaulting to sensible value if missing. + pub fn owner_api_listen_port(&self) -> u16 { + self.owner_api_listen_port + .unwrap_or_else(WalletConfig::default_owner_api_listen_port) + } + + /// Owner API listen address + pub fn owner_api_listen_addr(&self) -> String { + format!("127.0.0.1:{}", self.owner_api_listen_port()) + } + + /// Accept fee base + pub fn accept_fee_base(&self) -> u64 { + self.accept_fee_base + .unwrap_or_else(|| WalletConfig::default_accept_fee_base()) + } +} +/// Error type wrapping config errors. +#[derive(Debug)] +pub enum ConfigError { + /// Error with parsing of config file + ParseError(String, String), + + /// Error with fileIO while reading config file + FileIOError(String, String), + + /// No file found + FileNotFoundError(String), + + /// Error serializing config values + SerializationError(String), + + /// Path doesn't exist + PathNotFoundError(String), +} + +impl fmt::Display for ConfigError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + ConfigError::ParseError(ref file_name, ref message) => write!( + f, + "Error parsing configuration file at {} - {}", + file_name, message + ), + ConfigError::FileIOError(ref file_name, ref message) => { + write!(f, "{} {}", message, file_name) + } + ConfigError::FileNotFoundError(ref file_name) => { + write!(f, "Configuration file not found: {}", file_name) + } + ConfigError::SerializationError(ref message) => { + write!(f, "Error serializing configuration: {}", message) + } + ConfigError::PathNotFoundError(ref message) => write!(f, "Path not found: {}", message), + } + } +} + +impl From for ConfigError { + fn from(error: io::Error) -> ConfigError { + ConfigError::FileIOError( + String::from(""), + format!("Error loading config file: {}", error), + ) + } +} + +/// Tor configuration +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct TorConfig { + /// whether to skip any attempts to send via TOR + pub skip_send_attempt: Option, + /// Whether to start tor listener on listener startup (default true) + pub use_tor_listener: bool, + /// Just the address of the socks proxy for now + pub socks_proxy_addr: String, + /// Send configuration directory + pub send_config_dir: String, + /// tor bridge config + #[serde(default)] + pub bridge: TorBridgeConfig, + /// tor proxy config + #[serde(default)] + pub proxy: TorProxyConfig, +} + +impl Default for TorConfig { + fn default() -> TorConfig { + TorConfig { + skip_send_attempt: Some(false), + use_tor_listener: true, + socks_proxy_addr: "127.0.0.1:59050".to_owned(), + send_config_dir: ".".into(), + bridge: TorBridgeConfig::default(), + proxy: TorProxyConfig::default(), + } + } +} + +/// Tor Bridge Config +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct TorBridgeConfig { + /// Bridge Line + pub bridge_line: Option, + /// Client Option + pub client_option: Option, +} + +impl Default for TorBridgeConfig { + fn default() -> TorBridgeConfig { + TorBridgeConfig { + bridge_line: None, + client_option: None, + } + } +} + +impl fmt::Display for TorBridgeConfig { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +/// Tor Proxy configuration (useful for protocols such as shadowsocks) +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct TorProxyConfig { + /// socks4 |socks5 | http(s) + pub transport: Option, + /// ip or dns + pub address: Option, + /// user for auth - socks5|https(s) + pub username: Option, + /// pass for auth - socks5|https(s) + pub password: Option, + /// allowed port - proxy + pub allowed_port: Option>, +} + +impl Default for TorProxyConfig { + fn default() -> TorProxyConfig { + TorProxyConfig { + transport: None, + address: None, + username: None, + password: None, + allowed_port: None, + } + } +} + +impl fmt::Display for TorProxyConfig { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +/// Wallet should be split into a separate configuration file +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct GlobalWalletConfig { + /// Keep track of the file we've read + pub config_file_path: Option, + /// Wallet members + pub members: Option, +} + +/// Wallet internal members +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct GlobalWalletConfigMembers { + /// Config file version (None == version 1) + #[serde(default)] + pub config_file_version: Option, + /// Wallet configuration + #[serde(default)] + pub wallet: WalletConfig, + /// Tor config + pub tor: Option, + /// Logging config + pub logging: Option, +} diff --git a/controller/Cargo.toml b/controller/Cargo.toml new file mode 100644 index 0000000..33a9172 --- /dev/null +++ b/controller/Cargo.toml @@ -0,0 +1,88 @@ +[package] +name = "grin_wallet_controller" +version = "5.4.0-alpha.1" +authors = ["Grin Developers "] +description = "Controllers for grin wallet instantiation" +license = "Apache-2.0" +repository = "https://github.com/mimblewimble/grin-wallet" +keywords = [ "crypto", "grin", "mimblewimble" ] +exclude = ["**/*.grin", "**/*.grin2"] +#build = "src/build/build.rs" +edition = "2018" + +[dependencies] +futures = "0.3" +hyper = "0.13" +rand = "0.7" +serde = "1" +serde_derive = "1" +serde_json = "1" +log = "0.4" +prettytable-rs = "0.10" +ring = "0.16" +term = "0.6" +tokio = { version = "0.2", features = ["full"] } +uuid = { version = "0.8", features = ["serde", "v4"] } +url = "2.1" +chrono = { version = "0.4.11", features = ["serde"] } +easy-jsonrpc-mw = "0.5.4" +lazy_static = "1" +thiserror = "1" +qr_code = "1.1.0" + +grin_wallet_util = { path = "../util", version = "5.4.0-alpha.1" } +grin_wallet_api = { path = "../api", version = "5.4.0-alpha.1" } +grin_wallet_impls = { path = "../impls", version = "5.4.0-alpha.1" } +grin_wallet_libwallet = { path = "../libwallet", version = "5.4.0-alpha.1" } +grin_wallet_config = { path = "../config", version = "5.4.0-alpha.1" } + +##### Grin Imports + +# For Release +grin_core = "5.3.3" +grin_keychain = "5.3.3" +grin_util = "5.3.3" +grin_api = "5.3.3" + +# For beta release + +# grin_core = { git = "https://github.com/mimblewimble/grin", tag = "v5.2.0-beta.3"} +# grin_keychain = { git = "https://github.com/mimblewimble/grin", tag = "v5.2.0-beta.3" } +# grin_util = { git = "https://github.com/mimblewimble/grin", tag = "v5.2.0-beta.3" } +# grin_api = { git = "https://github.com/mimblewimble/grin", tag = "v5.2.0-beta.3" } + +# For bleeding edge +# grin_core = { git = "https://github.com/mimblewimble/grin", branch = "master" } +# grin_keychain = { git = "https://github.com/mimblewimble/grin", branch = "master" } +# grin_util = { git = "https://github.com/mimblewimble/grin", branch = "master" } +# grin_api = { git = "https://github.com/mimblewimble/grin", branch = "master" } + +# For local testing +# grin_core = { path = "../../grin/core"} +# grin_keychain = { path = "../../grin/keychain"} +# grin_util = { path = "../../grin/util"} +# grin_api = { path = "../../grin/api"} + +##### + +[dev-dependencies] +ed25519-dalek = "1.0.0-pre.4" +remove_dir_all = "0.7" + +##### Grin Imports + +# For Release +grin_chain = "5.3.3" + +# For beta release + +# grin_chain = { git = "https://github.com/mimblewimble/grin", tag = "v5.2.0-beta.3" } + +# For bleeding edge +# grin_chain = { git = "https://github.com/mimblewimble/grin", branch = "master" } + +# For local testing +# grin_chain = { path = "../../grin/chain"} + +##### + diff --git a/controller/src/command.rs b/controller/src/command.rs new file mode 100644 index 0000000..1e883dd --- /dev/null +++ b/controller/src/command.rs @@ -0,0 +1,1482 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Grin wallet command-line function implementations + +use crate::api::TLSConfig; +use crate::apiwallet::{try_slatepack_sync_workflow, Owner}; +use crate::config::{TorConfig, WalletConfig, WALLET_CONFIG_FILE_NAME}; +use crate::core::{core, global}; +use crate::error::Error; +use crate::impls::PathToSlatepack; +use crate::impls::SlateGetter as _; +use crate::keychain; +use crate::libwallet::{ + self, InitTxArgs, IssueInvoiceTxArgs, NodeClient, PaymentProof, Slate, SlateState, Slatepack, + SlatepackAddress, Slatepacker, SlatepackerArgs, WalletLCProvider, +}; +use crate::util::secp::key::SecretKey; +use crate::util::{Mutex, ZeroingString}; +use crate::{controller, display}; +use ::core::time; +use qr_code::QrCode; +use serde_json as json; +use std::convert::TryFrom; +use std::fs::File; +use std::io::{Read, Write}; +use std::sync::atomic::Ordering; +use std::sync::Arc; +use std::thread; +use std::time::Duration; +use uuid::Uuid; + +fn show_recovery_phrase(phrase: ZeroingString) { + println!("Your recovery phrase is:"); + println!(); + println!("{}", &*phrase); + println!(); + println!("Please back-up these words in a non-digital format."); +} + +/// Arguments common to all wallet commands +#[derive(Clone)] +pub struct GlobalArgs { + pub account: String, + pub api_secret: Option, + pub node_api_secret: Option, + pub show_spent: bool, + pub password: Option, + pub tls_conf: Option, +} + +/// Arguments for init command +pub struct InitArgs { + /// BIP39 recovery phrase length + pub list_length: usize, + pub password: ZeroingString, + pub config: WalletConfig, + pub recovery_phrase: Option, + pub restore: bool, +} + +/// Write config (default if None), initiate the wallet +pub fn init( + owner_api: &mut Owner, + _g_args: &GlobalArgs, + args: InitArgs, + test_mode: bool, +) -> Result<(), Error> +where + L: WalletLCProvider<'static, C, K> + 'static, + C: NodeClient + 'static, + K: keychain::Keychain + 'static, +{ + let chain_type = global::get_chain_type(); + let mut w_lock = owner_api.wallet_inst.lock(); + let p = w_lock.lc_provider()?; + println!("Checkpoint"); + p.create_config( + &chain_type, + WALLET_CONFIG_FILE_NAME, + Some(args.config), + None, + None, + )?; + p.create_wallet( + None, + args.recovery_phrase, + args.list_length, + args.password.clone(), + test_mode, + )?; + + let m = p.get_mnemonic(None, args.password)?; + show_recovery_phrase(m); + Ok(()) +} + +/// Argument for recover +pub struct RecoverArgs { + pub passphrase: ZeroingString, +} + +pub fn recover(owner_api: &mut Owner, args: RecoverArgs) -> Result<(), Error> +where + L: WalletLCProvider<'static, C, K> + 'static, + C: NodeClient + 'static, + K: keychain::Keychain + 'static, +{ + let mut w_lock = owner_api.wallet_inst.lock(); + let p = w_lock.lc_provider()?; + let m = p.get_mnemonic(None, args.passphrase)?; + show_recovery_phrase(m); + Ok(()) +} + +pub fn rewind_hash<'a, L, C, K>( + owner_api: &mut Owner, + keychain_mask: Option<&SecretKey>, +) -> Result<(), Error> +where + L: WalletLCProvider<'static, C, K>, + C: NodeClient + 'static, + K: keychain::Keychain + 'static, +{ + controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { + let rewind_hash = api.get_rewind_hash(m)?; + println!(); + println!("Wallet Rewind Hash"); + println!("-------------------------------------"); + println!("{}", rewind_hash); + println!(); + Ok(()) + })?; + Ok(()) +} + +/// Arguments for rewind hash view wallet scan command +pub struct ViewWalletScanArgs { + pub rewind_hash: String, + pub start_height: Option, + pub backwards_from_tip: Option, +} + +pub fn scan_rewind_hash( + owner_api: &mut Owner, + args: ViewWalletScanArgs, + dark_scheme: bool, +) -> Result<(), Error> +where + L: WalletLCProvider<'static, C, K> + 'static, + C: NodeClient + 'static, + K: keychain::Keychain + 'static, +{ + controller::owner_single_use(None, None, Some(owner_api), |api, m| { + let rewind_hash = args.rewind_hash; + let tip_height = api.node_height(m)?.height; + let start_height = match args.backwards_from_tip { + Some(b) => tip_height.saturating_sub(b), + None => match args.start_height { + Some(s) => s, + None => 1, + }, + }; + warn!( + "Starting view wallet output scan from height {} ...", + start_height + ); + let result = api.scan_rewind_hash(rewind_hash, Some(start_height)); + let deci_sec = time::Duration::from_millis(100); + thread::sleep(deci_sec); + match result { + Ok(res) => { + warn!("View wallet check complete"); + if res.total_balance != 0 { + display::view_wallet_output(res.clone(), tip_height, dark_scheme)?; + } + display::view_wallet_balance(res.clone(), tip_height, dark_scheme); + Ok(()) + } + Err(e) => { + error!("View wallet check failed: {}", e); + Err(e) + } + } + })?; + Ok(()) +} + +/// Arguments for listen command +pub struct ListenArgs {} + +pub fn listen( + owner_api: &mut Owner, + keychain_mask: Arc>>, + config: &WalletConfig, + tor_config: &TorConfig, + _args: &ListenArgs, + g_args: &GlobalArgs, + cli_mode: bool, + test_mode: bool, +) -> Result<(), Error> +where + L: WalletLCProvider<'static, C, K> + 'static, + C: NodeClient + 'static, + K: keychain::Keychain + 'static, +{ + let wallet_inst = owner_api.wallet_inst.clone(); + let config = config.clone(); + let tor_config = tor_config.clone(); + let g_args = g_args.clone(); + let api_thread = thread::Builder::new() + .name("wallet-http-listener".to_string()) + .spawn(move || { + let res = controller::foreign_listener( + wallet_inst, + keychain_mask, + &config.api_listen_addr(), + g_args.tls_conf.clone(), + tor_config.use_tor_listener, + test_mode, + Some(tor_config.clone()), + ); + if let Err(e) = res { + error!("Error starting listener: {}", e); + } + }); + if let Ok(t) = api_thread { + if !cli_mode { + let r = t.join(); + if let Err(_) = r { + error!("Error starting listener"); + return Err(Error::ListenerError); + } + } + } + Ok(()) +} + +pub fn owner_api( + owner_api: &mut Owner, + keychain_mask: Option, + config: &WalletConfig, + tor_config: &TorConfig, + g_args: &GlobalArgs, + test_mode: bool, +) -> Result<(), Error> +where + L: WalletLCProvider<'static, C, K> + Send + Sync + 'static, + C: NodeClient + 'static, + K: keychain::Keychain + 'static, +{ + // keychain mask needs to be a sinlge instance, in case the foreign API is + // also being run at the same time + let km = Arc::new(Mutex::new(keychain_mask)); + let res = controller::owner_listener( + owner_api.wallet_inst.clone(), + km, + config.owner_api_listen_addr().as_str(), + g_args.api_secret.clone(), + g_args.tls_conf.clone(), + config.owner_api_include_foreign, + Some(tor_config.clone()), + test_mode, + ); + if let Err(e) = res { + return Err(Error::LibWallet(e)); + } + Ok(()) +} + +/// Arguments for account command +pub struct AccountArgs { + pub create: Option, +} + +pub fn account( + owner_api: &mut Owner, + keychain_mask: Option<&SecretKey>, + args: AccountArgs, +) -> Result<(), Error> +where + L: WalletLCProvider<'static, C, K> + 'static, + C: NodeClient + 'static, + K: keychain::Keychain + 'static, +{ + if args.create.is_none() { + let res = controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { + let acct_mappings = api.accounts(m)?; + // give logging thread a moment to catch up + thread::sleep(Duration::from_millis(200)); + display::accounts(acct_mappings); + Ok(()) + }); + if let Err(e) = res { + error!("Error listing accounts: {}", e); + return Err(Error::LibWallet(e)); + } + } else { + let label = args.create.unwrap(); + let res = controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { + api.create_account_path(m, &label)?; + thread::sleep(Duration::from_millis(200)); + info!("Account: '{}' Created!", label); + Ok(()) + }); + if let Err(e) = res { + thread::sleep(Duration::from_millis(200)); + error!("Error creating account '{}': {}", label, e); + return Err(Error::LibWallet(e)); + } + } + Ok(()) +} + +/// Arguments for the send command +#[derive(Clone)] +pub struct SendArgs { + pub amount: u64, + pub amount_includes_fee: bool, + pub use_max_amount: bool, + pub minimum_confirmations: u64, + pub selection_strategy: String, + pub estimate_selection_strategies: bool, + pub late_lock: bool, + pub dest: String, + pub change_outputs: usize, + pub fluff: bool, + pub max_outputs: usize, + pub target_slate_version: Option, + pub payment_proof_address: Option, + pub ttl_blocks: Option, + pub skip_tor: bool, + pub outfile: Option, + pub bridge: Option, + pub slatepack_qr: bool, +} + +pub fn send( + owner_api: &mut Owner, + keychain_mask: Option<&SecretKey>, + tor_config: Option, + args: SendArgs, + dark_scheme: bool, + test_mode: bool, +) -> Result<(), Error> +where + L: WalletLCProvider<'static, C, K> + 'static, + C: NodeClient + 'static, + K: keychain::Keychain + 'static, +{ + let mut slate = Slate::blank(2, false); + let mut amount = args.amount; + if args.use_max_amount { + controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { + let (_, wallet_info) = + api.retrieve_summary_info(m, true, args.minimum_confirmations)?; + amount = wallet_info.amount_currently_spendable; + Ok(()) + })?; + }; + controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { + if args.estimate_selection_strategies { + let strategies = vec!["smallest", "all"] + .into_iter() + .map(|strategy| { + let init_args = InitTxArgs { + src_acct_name: None, + amount: amount, + amount_includes_fee: Some(args.amount_includes_fee), + minimum_confirmations: args.minimum_confirmations, + max_outputs: args.max_outputs as u32, + num_change_outputs: args.change_outputs as u32, + selection_strategy_is_use_all: strategy == "all", + estimate_only: Some(true), + ..Default::default() + }; + let slate = api.init_send_tx(m, init_args)?; + Ok((strategy, slate.amount, slate.fee_fields)) + }) + .collect::, grin_wallet_libwallet::Error>>()?; + display::estimate(amount, strategies, dark_scheme); + return Ok(()); + } else { + let init_args = InitTxArgs { + src_acct_name: None, + amount: amount, + amount_includes_fee: Some(args.amount_includes_fee), + minimum_confirmations: args.minimum_confirmations, + max_outputs: args.max_outputs as u32, + num_change_outputs: args.change_outputs as u32, + selection_strategy_is_use_all: args.selection_strategy == "all", + target_slate_version: args.target_slate_version, + payment_proof_recipient_address: args.payment_proof_address.clone(), + ttl_blocks: args.ttl_blocks, + send_args: None, + late_lock: Some(args.late_lock), + ..Default::default() + }; + let result = api.init_send_tx(m, init_args); + slate = match result { + Ok(s) => { + info!( + "Tx created: {} grin to {} (strategy '{}')", + core::amount_to_hr_string(amount, false), + args.dest, + args.selection_strategy, + ); + s + } + Err(e) => { + info!("Tx not created: {}", e); + return Err(e); + } + }; + } + Ok(()) + })?; + + if args.estimate_selection_strategies { + return Ok(()); + } + + let tor_config = match tor_config { + Some(mut c) => { + if let Some(b) = args.bridge.clone() { + c.bridge.bridge_line = Some(b); + } + c.skip_send_attempt = Some(args.skip_tor); + Some(c) + } + None => None, + }; + + let res = try_slatepack_sync_workflow(&slate, &args.dest, tor_config, None, false, test_mode); + + match res { + Ok(Some(s)) => { + controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { + api.tx_lock_outputs(m, &s)?; + let ret_slate = api.finalize_tx(m, &s)?; + let result = api.post_tx(m, &ret_slate, args.fluff); + match result { + Ok(_) => { + println!("Tx sent successfully",); + Ok(()) + } + Err(e) => { + error!("Tx sent fail: {}", e); + Err(e.into()) + } + } + })?; + } + Ok(None) => { + output_slatepack( + owner_api, + keychain_mask, + &slate, + args.dest.as_str(), + args.outfile, + true, + false, + args.slatepack_qr, + )?; + } + Err(e) => return Err(e.into()), + } + Ok(()) +} + +pub fn output_slatepack( + owner_api: &mut Owner, + keychain_mask: Option<&SecretKey>, + slate: &Slate, + dest: &str, + out_file_override: Option, + lock: bool, + finalizing: bool, + show_qr: bool, +) -> Result<(), libwallet::Error> +where + L: WalletLCProvider<'static, C, K> + 'static, + C: NodeClient + 'static, + K: keychain::Keychain + 'static, +{ + // Output the slatepack file to stdout and to a file + let mut message = String::from(""); + let mut address = None; + let mut tld = String::from(""); + controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { + address = match SlatepackAddress::try_from(dest) { + Ok(a) => Some(a), + Err(_) => None, + }; + // encrypt for recipient by default + let recipients = match address.clone() { + Some(a) => vec![a], + None => vec![], + }; + message = api.create_slatepack_message(m, &slate, Some(0), recipients)?; + tld = api.get_top_level_directory()?; + Ok(()) + })?; + + // create a directory to which files will be output + let slate_dir = format!("{}/{}", tld, "slatepack"); + let _ = std::fs::create_dir_all(slate_dir.clone()); + let out_file_name = match out_file_override { + None => format!("{}/{}.{}.slatepack", slate_dir, slate.id, slate.state), + Some(f) => f, + }; + + if lock { + controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { + api.tx_lock_outputs(m, &slate)?; + Ok(()) + })?; + } + + println!("{}", out_file_name); + let mut output = File::create(out_file_name.clone())?; + output.write_all(&message.as_bytes())?; + output.sync_all()?; + + println!(); + if !finalizing { + println!("Slatepack data follows. Please provide this output to the other party"); + } else { + println!("Slatepack data follows."); + } + println!(); + println!("--- CUT BELOW THIS LINE ---"); + println!(); + println!("{}", message); + println!("--- CUT ABOVE THIS LINE ---"); + println!(); + println!("Slatepack data was also output to"); + println!(); + println!("{}", out_file_name); + println!(); + if show_qr { + if let Ok(qr_string) = QrCode::new(message) { + println!("{}", qr_string.to_string(false, 3)); + println!(); + } + } + if address.is_some() { + println!("The slatepack data is encrypted for the recipient only"); + } else { + println!("The slatepack data is NOT encrypted"); + } + println!(); + Ok(()) +} + +// Parse a slate and slatepack from a message +pub fn parse_slatepack( + owner_api: &mut Owner, + keychain_mask: Option<&SecretKey>, + filename: Option, + message: Option, +) -> Result<(Slate, Option), Error> +where + L: WalletLCProvider<'static, C, K>, + C: NodeClient + 'static, + K: keychain::Keychain + 'static, +{ + let mut ret_address = None; + let slate = match filename { + Some(f) => { + // otherwise, get slate from slatepack + let mut sl = None; + controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { + let dec_key = api.get_slatepack_secret_key(m, 0)?; + let packer = Slatepacker::new(SlatepackerArgs { + sender: None, + recipients: vec![], + dec_key: Some(&dec_key), + }); + let pts = PathToSlatepack::new(f.into(), &packer, true); + sl = Some(pts.get_tx()?.0); + ret_address = pts.get_slatepack(true)?.sender; + Ok(()) + })?; + sl + } + None => None, + }; + + let slate = match slate { + Some(s) => s, + None => { + // try and parse directly from input_slatepack_message + let mut slate = Slate::blank(2, false); + match message { + Some(message) => { + controller::owner_single_use( + None, + keychain_mask, + Some(owner_api), + |api, m| { + slate = + api.slate_from_slatepack_message(m, message.clone(), vec![0])?; + let slatepack = + api.decode_slatepack_message(m, message.clone(), vec![0])?; + ret_address = slatepack.sender; + Ok(()) + }, + )?; + } + None => { + let msg = "No slate provided via file or direct input"; + return Err(Error::GenericError(msg.into()).into()); + } + } + slate + } + }; + Ok((slate, ret_address)) +} + +/// Receive command argument +#[derive(Clone)] +pub struct ReceiveArgs { + pub input_file: Option, + pub input_slatepack_message: Option, + pub skip_tor: bool, + pub outfile: Option, + pub bridge: Option, + pub slatepack_qr: bool, +} + +pub fn receive( + owner_api: &mut Owner, + keychain_mask: Option<&SecretKey>, + g_args: &GlobalArgs, + args: ReceiveArgs, + tor_config: Option, + test_mode: bool, +) -> Result<(), Error> +where + L: WalletLCProvider<'static, C, K>, + C: NodeClient + 'static, + K: keychain::Keychain + 'static, +{ + let (mut slate, ret_address) = parse_slatepack( + owner_api, + keychain_mask, + args.input_file, + args.input_slatepack_message, + )?; + + let km = match keychain_mask.as_ref() { + None => None, + Some(&m) => Some(m.to_owned()), + }; + + let tor_config = match tor_config { + Some(mut c) => { + if let Some(b) = args.bridge { + c.bridge.bridge_line = Some(b); + } + c.skip_send_attempt = Some(args.skip_tor); + Some(c) + } + None => None, + }; + + controller::foreign_single_use(owner_api.wallet_inst.clone(), km, |api| { + slate = api.receive_tx(&slate, Some(&g_args.account), None)?; + Ok(()) + })?; + + let dest = match ret_address { + Some(a) => String::try_from(&a).unwrap(), + None => String::from(""), + }; + + let res = try_slatepack_sync_workflow(&slate, &dest, tor_config, None, true, test_mode); + + match res { + Ok(Some(_)) => { + println!(); + println!( + "Transaction recieved and sent back to sender at {} for finalization.", + dest + ); + println!(); + Ok(()) + } + Ok(None) => { + output_slatepack( + owner_api, + keychain_mask, + &slate, + &dest, + args.outfile, + false, + false, + args.slatepack_qr, + )?; + Ok(()) + } + Err(e) => Err(e.into()), + } +} + +pub fn unpack( + owner_api: &mut Owner, + keychain_mask: Option<&SecretKey>, + args: ReceiveArgs, +) -> Result<(), Error> +where + L: WalletLCProvider<'static, C, K> + 'static, + C: NodeClient + 'static, + K: keychain::Keychain + 'static, +{ + let mut slatepack = match args.input_file { + Some(f) => { + let packer = Slatepacker::new(SlatepackerArgs { + sender: None, + recipients: vec![], + dec_key: None, + }); + PathToSlatepack::new(f.into(), &packer, true).get_slatepack(false)? + } + None => match args.input_slatepack_message { + Some(mes) => { + let mut sp = Slatepack::default(); + controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { + sp = api.decode_slatepack_message(m, mes, vec![])?; + Ok(()) + })?; + sp + } + None => { + return Err(Error::ArgumentError("Invalid Slatepack Input".into()).into()); + } + }, + }; + println!(); + println!("SLATEPACK CONTENTS"); + println!("------------------"); + println!("{}", slatepack); + println!("------------------"); + + let packer = Slatepacker::new(SlatepackerArgs { + sender: None, + recipients: vec![], + dec_key: None, + }); + + if slatepack.mode == 1 { + controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { + let dec_key = api.get_slatepack_secret_key(m, 0)?; + match slatepack.try_decrypt_payload(Some(&dec_key)) { + Ok(_) => { + println!("Slatepack is encrypted for this wallet"); + println!(); + println!("DECRYPTED SLATEPACK"); + println!("-------------------"); + println!("{}", slatepack); + let slate = packer.get_slate(&slatepack)?; + println!(); + println!("DECRYPTED SLATE"); + println!("---------------"); + println!("{}", slate); + } + Err(_) => { + println!("Slatepack payload cannot be decrypted by this wallet"); + } + } + Ok(()) + })?; + } else { + let slate = packer.get_slate(&slatepack)?; + println!("Slatepack is not encrypted"); + println!(); + println!("SLATE"); + println!("-----"); + println!("{}", slate); + } + Ok(()) +} + +/// Finalize command args +#[derive(Clone)] +pub struct FinalizeArgs { + pub input_file: Option, + pub input_slatepack_message: Option, + pub fluff: bool, + pub nopost: bool, + pub outfile: Option, + pub slatepack_qr: bool, +} + +pub fn finalize( + owner_api: &mut Owner, + keychain_mask: Option<&SecretKey>, + args: FinalizeArgs, +) -> Result<(), Error> +where + L: WalletLCProvider<'static, C, K> + 'static, + C: NodeClient + 'static, + K: keychain::Keychain + 'static, +{ + let (mut slate, _ret_address) = parse_slatepack( + owner_api, + keychain_mask, + args.input_file.clone(), + args.input_slatepack_message.clone(), + )?; + + // Rather than duplicating the entire command, we'll just + // try to determine what kind of finalization this is + // based on the slate state + let is_invoice = slate.state == SlateState::Invoice2; + + if is_invoice { + let km = match keychain_mask.as_ref() { + None => None, + Some(&m) => Some(m.to_owned()), + }; + controller::foreign_single_use(owner_api.wallet_inst.clone(), km, |api| { + slate = api.finalize_tx(&slate, false)?; + Ok(()) + })?; + } else { + controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { + slate = api.finalize_tx(m, &slate)?; + Ok(()) + })?; + } + + if !&args.nopost { + controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { + let result = api.post_tx(m, &slate, args.fluff); + match result { + Ok(_) => { + info!( + "Transaction sent successfully, check the wallet again for confirmation." + ); + println!("Transaction posted"); + Ok(()) + } + Err(e) => { + error!("Tx not sent: {}", e); + Err(e) + } + } + })?; + } + + println!("Transaction finalized successfully"); + + output_slatepack( + owner_api, + keychain_mask, + &slate, + "", + args.outfile, + false, + true, + args.slatepack_qr, + )?; + + Ok(()) +} + +/// Issue Invoice Args +pub struct IssueInvoiceArgs { + /// Slatepack address + pub dest: String, + /// issue invoice tx args + pub issue_args: IssueInvoiceTxArgs, + /// output file override + pub outfile: Option, + /// show slatepack as QR code + pub slatepack_qr: bool, +} + +pub fn issue_invoice_tx( + owner_api: &mut Owner, + keychain_mask: Option<&SecretKey>, + args: IssueInvoiceArgs, +) -> Result<(), Error> +where + L: WalletLCProvider<'static, C, K> + 'static, + C: NodeClient + 'static, + K: keychain::Keychain + 'static, +{ + let issue_args = args.issue_args.clone(); + + let mut slate = Slate::blank(2, false); + controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { + slate = api.issue_invoice_tx(m, issue_args)?; + Ok(()) + })?; + + output_slatepack( + owner_api, + keychain_mask, + &slate, + args.dest.as_str(), + args.outfile, + false, + false, + args.slatepack_qr, + )?; + Ok(()) +} + +/// Arguments for the process_invoice command +pub struct ProcessInvoiceArgs { + pub minimum_confirmations: u64, + pub selection_strategy: String, + pub ret_address: Option, + pub max_outputs: usize, + pub slate: Slate, + pub estimate_selection_strategies: bool, + pub ttl_blocks: Option, + pub skip_tor: bool, + pub outfile: Option, + pub bridge: Option, + pub slatepack_qr: bool, +} + +/// Process invoice +pub fn process_invoice( + owner_api: &mut Owner, + keychain_mask: Option<&SecretKey>, + tor_config: Option, + args: ProcessInvoiceArgs, + dark_scheme: bool, + test_mode: bool, +) -> Result<(), Error> +where + L: WalletLCProvider<'static, C, K> + 'static, + C: NodeClient + 'static, + K: keychain::Keychain + 'static, +{ + let mut slate = args.slate.clone(); + let dest = match args.ret_address.clone() { + Some(a) => String::try_from(&a).unwrap(), + None => String::from(""), + }; + + controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { + if args.estimate_selection_strategies { + let strategies = vec!["smallest", "all"] + .into_iter() + .map(|strategy| { + let init_args = InitTxArgs { + src_acct_name: None, + amount: slate.amount, + minimum_confirmations: args.minimum_confirmations, + max_outputs: args.max_outputs as u32, + num_change_outputs: 1u32, + selection_strategy_is_use_all: strategy == "all", + estimate_only: Some(true), + ..Default::default() + }; + let slate = api.init_send_tx(m, init_args).unwrap(); + (strategy, slate.amount, slate.fee_fields) + }) + .collect(); + display::estimate(slate.amount, strategies, dark_scheme); + return Ok(()); + } else { + let init_args = InitTxArgs { + src_acct_name: None, + amount: 0, + minimum_confirmations: args.minimum_confirmations, + max_outputs: args.max_outputs as u32, + num_change_outputs: 1u32, + selection_strategy_is_use_all: args.selection_strategy == "all", + ttl_blocks: args.ttl_blocks, + send_args: None, + ..Default::default() + }; + let result = api.process_invoice_tx(m, &slate, init_args); + slate = match result { + Ok(s) => { + info!( + "Invoice processed: {} grin (strategy '{}')", + core::amount_to_hr_string(slate.amount, false), + args.selection_strategy, + ); + s + } + Err(e) => { + info!("Tx not created: {}", e); + return Err(e); + } + }; + } + Ok(()) + })?; + + let tor_config = match tor_config { + Some(mut c) => { + if let Some(b) = args.bridge { + c.bridge.bridge_line = Some(b); + } + c.skip_send_attempt = Some(args.skip_tor); + Some(c) + } + None => None, + }; + + let res = try_slatepack_sync_workflow(&slate, &dest, tor_config, None, true, test_mode); + + match res { + Ok(Some(_)) => { + println!(); + println!( + "Transaction paid and sent back to initiator at {} for finalization.", + dest + ); + println!(); + Ok(()) + } + Ok(None) => { + output_slatepack( + owner_api, + keychain_mask, + &slate, + &dest, + args.outfile, + true, + false, + args.slatepack_qr, + )?; + Ok(()) + } + Err(e) => Err(e.into()), + } +} + +/// Info command args +pub struct InfoArgs { + pub minimum_confirmations: u64, +} + +pub fn info( + owner_api: &mut Owner, + keychain_mask: Option<&SecretKey>, + g_args: &GlobalArgs, + args: InfoArgs, + dark_scheme: bool, +) -> Result<(), Error> +where + L: WalletLCProvider<'static, C, K> + 'static, + C: NodeClient + 'static, + K: keychain::Keychain + 'static, +{ + let updater_running = owner_api.updater_running.load(Ordering::Relaxed); + controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { + let (validated, wallet_info) = + api.retrieve_summary_info(m, true, args.minimum_confirmations)?; + display::info( + &g_args.account, + &wallet_info, + validated || updater_running, + dark_scheme, + ); + Ok(()) + })?; + Ok(()) +} + +pub fn outputs( + owner_api: &mut Owner, + keychain_mask: Option<&SecretKey>, + g_args: &GlobalArgs, + dark_scheme: bool, +) -> Result<(), Error> +where + L: WalletLCProvider<'static, C, K> + 'static, + C: NodeClient + 'static, + K: keychain::Keychain + 'static, +{ + let updater_running = owner_api.updater_running.load(Ordering::Relaxed); + controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { + let res = api.node_height(m)?; + let (validated, outputs) = api.retrieve_outputs(m, g_args.show_spent, true, None)?; + display::outputs( + &g_args.account, + res.height, + validated || updater_running, + outputs, + dark_scheme, + )?; + Ok(()) + })?; + Ok(()) +} + +/// Txs command args +pub struct TxsArgs { + pub id: Option, + pub tx_slate_id: Option, + pub count: Option, +} + +pub fn txs( + owner_api: &mut Owner, + keychain_mask: Option<&SecretKey>, + g_args: &GlobalArgs, + args: TxsArgs, + dark_scheme: bool, +) -> Result<(), Error> +where + L: WalletLCProvider<'static, C, K> + 'static, + C: NodeClient + 'static, + K: keychain::Keychain + 'static, +{ + let updater_running = owner_api.updater_running.load(Ordering::Relaxed); + controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { + let res = api.node_height(m)?; + // Note advanced query args not currently supported by command line client + let (validated, txs) = api.retrieve_txs(m, true, args.id, args.tx_slate_id, None)?; + let include_status = !args.id.is_some() && !args.tx_slate_id.is_some(); + // If view count is specified, restrict the TX list to `txs.len() - count` + let first_tx = args + .count + .map_or(0, |c| txs.len().saturating_sub(c as usize)); + display::txs( + &g_args.account, + res.height, + validated || updater_running, + &txs[first_tx..], + include_status, + dark_scheme, + )?; + + // if given a particular transaction id or uuid, also get and display associated + // inputs/outputs and messages + let id = if args.id.is_some() { + args.id + } else if args.tx_slate_id.is_some() { + if let Some(tx) = txs.iter().find(|t| t.tx_slate_id == args.tx_slate_id) { + Some(tx.id) + } else { + println!("Could not find a transaction matching given txid.\n"); + None + } + } else { + None + }; + + if id.is_some() { + let (_, outputs) = api.retrieve_outputs(m, true, false, id)?; + display::outputs( + &g_args.account, + res.height, + validated || updater_running, + outputs, + dark_scheme, + )?; + // should only be one here, but just in case + for tx in txs { + display::payment_proof(&tx)?; + } + } + + Ok(()) + })?; + Ok(()) +} + +/// Post +#[derive(Clone)] +pub struct PostArgs { + pub input_file: Option, + pub input_slatepack_message: Option, + pub fluff: bool, +} + +pub fn post( + owner_api: &mut Owner, + keychain_mask: Option<&SecretKey>, + args: PostArgs, +) -> Result<(), Error> +where + L: WalletLCProvider<'static, C, K> + 'static, + C: NodeClient + 'static, + K: keychain::Keychain + 'static, +{ + let (slate, _ret_address) = parse_slatepack( + owner_api, + keychain_mask, + args.input_file, + args.input_slatepack_message, + )?; + + let fluff = args.fluff; + controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { + api.post_tx(m, &slate, fluff)?; + info!("Posted transaction"); + return Ok(()); + })?; + Ok(()) +} + +/// Repost +pub struct RepostArgs { + pub id: u32, + pub dump_file: Option, + pub fluff: bool, +} + +pub fn repost( + owner_api: &mut Owner, + keychain_mask: Option<&SecretKey>, + args: RepostArgs, +) -> Result<(), Error> +where + L: WalletLCProvider<'static, C, K> + 'static, + C: NodeClient + 'static, + K: keychain::Keychain + 'static, +{ + controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { + let stored_tx_slate = match api.get_stored_tx(m, Some(args.id), None)? { + None => { + error!( + "Transaction with id {} does not have transaction data. Not reposting.", + args.id + ); + return Ok(()); + } + Some(s) => s, + }; + let (_, txs) = api.retrieve_txs(m, true, Some(args.id), None, None)?; + match args.dump_file { + None => { + if txs[0].confirmed { + error!( + "Transaction with id {} is confirmed. Not reposting.", + args.id + ); + return Ok(()); + } + if libwallet::sig_is_blank( + &stored_tx_slate.tx.as_ref().unwrap().kernels()[0].excess_sig, + ) { + error!("Transaction at {} has not been finalized.", args.id); + return Ok(()); + } + + match api.post_tx(m, &stored_tx_slate, args.fluff) { + Ok(_) => info!("Reposted transaction at {}", args.id), + Err(e) => error!("Could not repost transaction at {}. Reason: {}", args.id, e), + } + return Ok(()); + } + Some(f) => { + let mut tx_file = File::create(f.clone())?; + tx_file.write_all( + json::to_string(&stored_tx_slate.tx.unwrap()) + .unwrap() + .as_bytes(), + )?; + tx_file.sync_all()?; + info!("Dumped transaction data for tx {} to {}", args.id, f); + return Ok(()); + } + } + })?; + Ok(()) +} + +/// Cancel +pub struct CancelArgs { + pub tx_id: Option, + pub tx_slate_id: Option, + pub tx_id_string: String, +} + +pub fn cancel( + owner_api: &mut Owner, + keychain_mask: Option<&SecretKey>, + args: CancelArgs, +) -> Result<(), Error> +where + L: WalletLCProvider<'static, C, K> + 'static, + C: NodeClient + 'static, + K: keychain::Keychain + 'static, +{ + controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { + let result = api.cancel_tx(m, args.tx_id, args.tx_slate_id); + match result { + Ok(_) => { + info!("Transaction {} Cancelled", args.tx_id_string); + Ok(()) + } + Err(e) => { + error!("TX Cancellation failed: {}", e); + Err(e) + } + } + })?; + Ok(()) +} + +/// wallet check +pub struct CheckArgs { + pub delete_unconfirmed: bool, + pub start_height: Option, + pub backwards_from_tip: Option, +} + +pub fn scan( + owner_api: &mut Owner, + keychain_mask: Option<&SecretKey>, + args: CheckArgs, +) -> Result<(), Error> +where + L: WalletLCProvider<'static, C, K> + 'static, + C: NodeClient + 'static, + K: keychain::Keychain + 'static, +{ + controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { + let tip_height = api.node_height(m)?.height; + let start_height = match args.backwards_from_tip { + Some(b) => tip_height.saturating_sub(b), + None => match args.start_height { + Some(s) => s, + None => 1, + }, + }; + warn!("Starting output scan from height {} ...", start_height); + let result = api.scan(m, Some(start_height), args.delete_unconfirmed); + match result { + Ok(_) => { + warn!("Wallet check complete",); + Ok(()) + } + Err(e) => { + error!("Wallet check failed: {}", e); + Err(e) + } + } + })?; + Ok(()) +} + +/// Payment Proof Address +pub fn address( + owner_api: &mut Owner, + g_args: &GlobalArgs, + keychain_mask: Option<&SecretKey>, +) -> Result<(), Error> +where + L: WalletLCProvider<'static, C, K> + 'static, + C: NodeClient + 'static, + K: keychain::Keychain + 'static, +{ + controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { + // Just address at derivation index 0 for now + let address = api.get_slatepack_address(m, 0)?; + println!(); + println!("Address for account - {}", g_args.account); + println!("-------------------------------------"); + println!("{}", address); + println!(); + Ok(()) + })?; + Ok(()) +} + +/// Proof Export Args +pub struct ProofExportArgs { + pub output_file: String, + pub id: Option, + pub tx_slate_id: Option, +} + +pub fn proof_export( + owner_api: &mut Owner, + keychain_mask: Option<&SecretKey>, + args: ProofExportArgs, +) -> Result<(), Error> +where + L: WalletLCProvider<'static, C, K> + 'static, + C: NodeClient + 'static, + K: keychain::Keychain + 'static, +{ + controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { + let result = api.retrieve_payment_proof(m, true, args.id, args.tx_slate_id); + match result { + Ok(p) => { + // actually export proof + let mut proof_file = File::create(args.output_file.clone())?; + proof_file.write_all(json::to_string_pretty(&p).unwrap().as_bytes())?; + proof_file.sync_all()?; + warn!("Payment proof exported to {}", args.output_file); + Ok(()) + } + Err(e) => { + error!("Proof export failed: {}", e); + Err(e) + } + } + })?; + Ok(()) +} + +/// Proof Verify Args +pub struct ProofVerifyArgs { + pub input_file: String, +} + +pub fn proof_verify( + owner_api: &mut Owner, + keychain_mask: Option<&SecretKey>, + args: ProofVerifyArgs, +) -> Result<(), Error> +where + L: WalletLCProvider<'static, C, K> + 'static, + C: NodeClient + 'static, + K: keychain::Keychain + 'static, +{ + controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { + let mut proof_f = match File::open(&args.input_file) { + Ok(p) => p, + Err(e) => { + let msg = format!("{}", e); + error!( + "Unable to open payment proof file at {}: {}", + args.input_file, e + ); + return Err(libwallet::Error::PaymentProofParsing(msg)); + } + }; + let mut proof = String::new(); + proof_f.read_to_string(&mut proof)?; + // read + let proof: PaymentProof = match json::from_str(&proof) { + Ok(p) => p, + Err(e) => { + let msg = format!("{}", e); + error!("Unable to parse payment proof file: {}", e); + return Err(libwallet::Error::PaymentProofParsing(msg)); + } + }; + let result = api.verify_payment_proof(m, &proof); + match result { + Ok((iam_sender, iam_recipient)) => { + println!("Payment proof's signatures are valid."); + if iam_sender { + println!("The proof's sender address belongs to this wallet."); + } + if iam_recipient { + println!("The proof's recipient address belongs to this wallet."); + } + if !iam_recipient && !iam_sender { + println!( + "Neither the proof's sender nor recipient address belongs to this wallet." + ); + } + Ok(()) + } + Err(e) => { + error!("Proof not valid: {}", e); + Err(e) + } + } + })?; + Ok(()) +} diff --git a/controller/src/controller.rs b/controller/src/controller.rs new file mode 100644 index 0000000..388c4a0 --- /dev/null +++ b/controller/src/controller.rs @@ -0,0 +1,875 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Controller for wallet.. instantiates and handles listeners (or single-run +//! invocations) as needed. +use crate::api::{self, ApiServer, BasicAuthMiddleware, ResponseFuture, Router, TLSConfig}; +use crate::config::TorConfig; +use crate::keychain::Keychain; +use crate::libwallet::{ + address, Error, NodeClient, NodeVersionInfo, Slate, SlatepackAddress, WalletInst, + WalletLCProvider, GRIN_BLOCK_HEADER_VERSION, +}; +use crate::util::secp::key::SecretKey; +use crate::util::{from_hex, static_secp_instance, to_base64, Mutex}; +use futures::channel::oneshot; +use grin_wallet_api::JsonId; +use grin_wallet_config::types::{TorBridgeConfig, TorProxyConfig}; +use grin_wallet_util::OnionV3Address; +use hyper::body; +use hyper::header::HeaderValue; +use hyper::{Body, Request, Response, StatusCode}; +use qr_code::QrCode; +use serde::{Deserialize, Serialize}; +use serde_json; +use std::collections::HashMap; +use std::convert::TryFrom; +use std::net::SocketAddr; +use std::sync::Arc; + +use crate::impls::tor::config as tor_config; +use crate::impls::tor::process as tor_process; +use crate::impls::tor::{bridge as tor_bridge, proxy as tor_proxy}; + +use crate::apiwallet::{ + EncryptedRequest, EncryptedResponse, EncryptionErrorResponse, Foreign, + ForeignCheckMiddlewareFn, ForeignRpc, Owner, OwnerRpc, +}; +use easy_jsonrpc_mw; +use easy_jsonrpc_mw::{Handler, MaybeReply}; + +lazy_static! { + pub static ref GRIN_OWNER_BASIC_REALM: HeaderValue = + HeaderValue::from_str("Basic realm=GrinOwnerAPI").unwrap(); +} + +fn check_middleware( + name: ForeignCheckMiddlewareFn, + node_version_info: Option, + slate: Option<&Slate>, +) -> Result<(), Error> { + match name { + // allow coinbases to be built regardless + ForeignCheckMiddlewareFn::BuildCoinbase => Ok(()), + _ => { + let mut bhv = 3; + if let Some(n) = node_version_info { + bhv = n.block_header_version; + } + if let Some(s) = slate { + if bhv > 4 && s.version_info.block_header_version < GRIN_BLOCK_HEADER_VERSION { + Err(Error::Compatibility( + "Incoming Slate is not compatible with this wallet. \ + Please upgrade the node or use a different one." + .into(), + ))?; + } + } + Ok(()) + } + } +} + +/// initiate the tor listener +fn init_tor_listener( + wallet: Arc + 'static>>>, + keychain_mask: Arc>>, + addr: &str, + bridge: TorBridgeConfig, + tor_proxy: TorProxyConfig, +) -> Result<(tor_process::TorProcess, SlatepackAddress), Error> +where + L: WalletLCProvider<'static, C, K> + 'static, + C: NodeClient + 'static, + K: Keychain + 'static, +{ + let mut process = tor_process::TorProcess::new(); + let mask = keychain_mask.lock(); + // eventually want to read a list of service config keys + let mut w_lock = wallet.lock(); + let lc = w_lock.lc_provider()?; + let w_inst = lc.wallet_inst()?; + let k = w_inst.keychain((&mask).as_ref())?; + let parent_key_id = w_inst.parent_key_id(); + let tor_dir = format!("{}/tor/listener", lc.get_top_level_directory()?); + let sec_key = address::address_from_derivation_path(&k, &parent_key_id, 0) + .map_err(|e| Error::TorConfig(format!("{:?}", e)))?; + let onion_address = OnionV3Address::from_private(&sec_key.0) + .map_err(|e| Error::TorConfig(format!("{:?}", e)))?; + let sp_address = SlatepackAddress::try_from(onion_address.clone())?; + + let mut hm_tor_bridge: HashMap = HashMap::new(); + let mut tor_timeout = 20; + if bridge.bridge_line.is_some() { + tor_timeout = 40; + let bridge_config = tor_bridge::TorBridge::try_from(bridge) + .map_err(|e| Error::TorConfig(format!("{}", e).into()))?; + hm_tor_bridge = bridge_config + .to_hashmap() + .map_err(|e| Error::TorConfig(format!("{}", e).into()))?; + } + + let mut hm_tor_poxy: HashMap = HashMap::new(); + if tor_proxy.transport.is_some() || tor_proxy.allowed_port.is_some() { + let proxy_config = tor_proxy::TorProxy::try_from(tor_proxy) + .map_err(|e| Error::TorConfig(format!("{}", e).into()))?; + hm_tor_poxy = proxy_config + .to_hashmap() + .map_err(|e| Error::TorConfig(format!("{}", e).into()))?; + } + + warn!( + "Starting Tor Hidden Service for API listener at address {}, binding to {}", + onion_address, addr + ); + tor_config::output_tor_listener_config( + &tor_dir, + addr, + &vec![sec_key], + hm_tor_bridge, + hm_tor_poxy, + ) + .map_err(|e| Error::TorConfig(format!("{:?}", e).into()))?; + // Start TOR process + process + .torrc_path(&format!("{}/torrc", tor_dir)) + .working_dir(&tor_dir) + .timeout(tor_timeout) + .completion_percent(100) + .launch() + .map_err(|e| Error::TorProcess(format!("{:?}", e)))?; + Ok((process, sp_address)) +} + +/// Instantiate wallet Owner API for a single-use (command line) call +/// Return a function containing a loaded API context to call +pub fn owner_single_use( + wallet: Option>>>>, + keychain_mask: Option<&SecretKey>, + api_context: Option<&mut Owner>, + f: F, +) -> Result<(), Error> +where + L: WalletLCProvider<'static, C, K> + 'static, + F: FnOnce(&mut Owner, Option<&SecretKey>) -> Result<(), Error>, + C: NodeClient + 'static, + K: Keychain + 'static, +{ + match api_context { + Some(c) => f(c, keychain_mask)?, + None => { + let wallet = match wallet { + Some(w) => w, + None => { + return Err(Error::GenericError(format!( + "Instantiated wallet or Owner API context must be provided" + ))) + } + }; + f(&mut Owner::new(wallet, None), keychain_mask)? + } + } + Ok(()) +} + +/// Instantiate wallet Foreign API for a single-use (command line) call +/// Return a function containing a loaded API context to call +pub fn foreign_single_use<'a, L, F, C, K>( + wallet: Arc>>>, + keychain_mask: Option, + f: F, +) -> Result<(), Error> +where + L: WalletLCProvider<'a, C, K>, + F: FnOnce(&mut Foreign<'a, L, C, K>) -> Result<(), Error>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + f(&mut Foreign::new( + wallet, + keychain_mask, + Some(check_middleware), + false, + ))?; + Ok(()) +} + +/// Listener version, providing same API but listening for requests on a +/// port and wrapping the calls +/// Note keychain mask is only provided here in case the foreign listener is also being used +/// in the same wallet instance +pub fn owner_listener( + wallet: Arc + 'static>>>, + keychain_mask: Arc>>, + addr: &str, + api_secret: Option, + tls_config: Option, + owner_api_include_foreign: Option, + tor_config: Option, + test_mode: bool, +) -> Result<(), Error> +where + L: WalletLCProvider<'static, C, K> + 'static, + C: NodeClient + 'static, + K: Keychain + 'static, +{ + let mut router = Router::new(); + if api_secret.is_some() { + let api_basic_auth = + "Basic ".to_string() + &to_base64(&("grin:".to_string() + &api_secret.unwrap())); + let basic_auth_middleware = Arc::new(BasicAuthMiddleware::new( + api_basic_auth, + &GRIN_OWNER_BASIC_REALM, + Some("/v2/foreign".into()), + )); + router.add_middleware(basic_auth_middleware); + } + let mut running_foreign = false; + if owner_api_include_foreign.unwrap_or(false) { + running_foreign = true; + } + + let api_handler_v3 = OwnerAPIHandlerV3::new( + wallet.clone(), + keychain_mask.clone(), + tor_config.clone(), + running_foreign, + ); + + router + .add_route("/v3/owner", Arc::new(api_handler_v3)) + .map_err(|_| Error::GenericError("Router failed to add route".to_string()))?; + + // If so configured, add the foreign API to the same port + if running_foreign { + warn!("Starting HTTP Foreign API on Owner server at {}.", addr); + let foreign_api_handler_v2 = + ForeignAPIHandlerV2::new(wallet, keychain_mask, test_mode, Mutex::new(tor_config)); + router + .add_route("/v2/foreign", Arc::new(foreign_api_handler_v2)) + .map_err(|_| Error::GenericError("Router failed to add route".to_string()))?; + } + + let api_chan: &'static mut (oneshot::Sender<()>, oneshot::Receiver<()>) = + Box::leak(Box::new(oneshot::channel::<()>())); + + let mut apis = ApiServer::new(); + warn!("Starting HTTP Owner API server at {}.", addr); + let socket_addr: SocketAddr = addr.parse().expect("unable to parse socket address"); + let api_thread = apis + .start(socket_addr, router, tls_config, api_chan) + .map_err(|_| Error::GenericError("API thread failed to start".to_string()))?; + warn!("HTTP Owner listener started."); + api_thread + .join() + .map_err(|e| Error::GenericError(format!("API thread panicked :{:?}", e))) +} + +/// Listener version, providing same API but listening for requests on a +/// port and wrapping the calls +pub fn foreign_listener( + wallet: Arc + 'static>>>, + keychain_mask: Arc>>, + addr: &str, + tls_config: Option, + use_tor: bool, + test_mode: bool, + tor_config: Option, +) -> Result<(), Error> +where + L: WalletLCProvider<'static, C, K> + 'static, + C: NodeClient + 'static, + K: Keychain + 'static, +{ + // Check if wallet has been opened first + { + let mut w_lock = wallet.lock(); + let lc = w_lock.lc_provider()?; + let _ = lc.wallet_inst()?; + } + + let (tor_bridge, tor_proxy) = match tor_config.clone() { + Some(s) => (s.bridge, s.proxy), + None => (TorBridgeConfig::default(), TorProxyConfig::default()), + }; + + // need to keep in scope while the main listener is running + let (_tor_process, address) = match use_tor { + true => { + match init_tor_listener( + wallet.clone(), + keychain_mask.clone(), + addr, + tor_bridge, + tor_proxy, + ) { + Ok((tp, addr)) => (Some(tp), Some(addr)), + Err(e) => { + warn!("Unable to start TOR listener; Check that TOR executable is installed and on your path"); + error!("Tor Error: {}", e); + warn!("Listener will be available via HTTP only"); + (None, None) + } + } + } + false => (None, None), + }; + + let api_handler_v2 = + ForeignAPIHandlerV2::new(wallet, keychain_mask, test_mode, Mutex::new(tor_config)); + let mut router = Router::new(); + + router + .add_route("/v2/foreign", Arc::new(api_handler_v2)) + .map_err(|_| Error::GenericError("Router failed to add route".to_string()))?; + + let api_chan: &'static mut (oneshot::Sender<()>, oneshot::Receiver<()>) = + Box::leak(Box::new(oneshot::channel::<()>())); + + let mut apis = ApiServer::new(); + warn!("Starting HTTP Foreign listener API server at {}.", addr); + let socket_addr: SocketAddr = addr.parse().expect("unable to parse socket address"); + let api_thread = apis + .start(socket_addr, router, tls_config, api_chan) + .map_err(|_| Error::GenericError("API thread failed to start".to_string()))?; + + warn!("HTTP Foreign listener started."); + if let Some(a) = address { + let qr_string = match QrCode::new(a.to_string()) { + Ok(qr) => qr.to_string(false, 3), + Err(_) => "Failed to generate QR code!".to_string(), + }; + warn!("Slatepack Address is: {}\n{}", a, qr_string); + } + + api_thread + .join() + .map_err(|e| Error::GenericError(format!("API thread panicked :{:?}", e))) +} + +/// V3 API Handler/Wrapper for owner functions, which include a secure +/// mode + lifecycle functions +pub struct OwnerAPIHandlerV3 +where + L: WalletLCProvider<'static, C, K> + 'static, + C: NodeClient + 'static, + K: Keychain + 'static, +{ + /// Wallet instance + pub wallet: Arc + 'static>>>, + + /// Handle to Owner API + owner_api: Arc>, + + /// ECDH shared key + pub shared_key: Arc>>, + + /// Keychain mask (to change if also running the foreign API) + pub keychain_mask: Arc>>, + + /// Whether we're running the foreign API on the same port, and therefore + /// have to store the mask in-process + pub running_foreign: bool, +} + +pub struct OwnerV3Helpers; + +impl OwnerV3Helpers { + /// Checks whether a request is to init the secure API + pub fn is_init_secure_api(val: &serde_json::Value) -> bool { + matches!(val["method"].as_str(), Some("init_secure_api")) + } + + /// Checks whether a request is to open the wallet + pub fn is_open_wallet(val: &serde_json::Value) -> bool { + matches!(val["method"].as_str(), Some("open_wallet")) + } + + /// Checks whether a request is an encrypted request + pub fn is_encrypted_request(val: &serde_json::Value) -> bool { + matches!(val["method"].as_str(), Some("encrypted_request_v3")) + } + + /// whether encryption is enabled + pub fn encryption_enabled(key: Arc>>) -> bool { + let share_key_ref = key.lock(); + share_key_ref.is_some() + } + + /// If incoming is an encrypted request, check there is a shared key, + /// Otherwise return an error value + pub fn check_encryption_started( + key: Arc>>, + ) -> Result<(), serde_json::Value> { + match OwnerV3Helpers::encryption_enabled(key) { + true => Ok(()), + false => Err(EncryptionErrorResponse::new( + 1, + -32001, + "Encryption must be enabled. Please call 'init_secure_api` first", + ) + .as_json_value()), + } + } + + /// Update the statically held owner API shared key + pub fn update_owner_api_shared_key( + key: Arc>>, + val: &serde_json::Value, + new_key: Option, + ) { + if let Some(_) = val["result"]["Ok"].as_str() { + let mut share_key_ref = key.lock(); + *share_key_ref = new_key; + } + } + + /// Update the shared mask, in case of foreign API being run + pub fn update_mask(mask: Arc>>, val: &serde_json::Value) { + if let Some(key) = val["result"]["Ok"].as_str() { + let key_bytes = match from_hex(key) { + Ok(k) => k, + Err(_) => return, + }; + let secp_inst = static_secp_instance(); + let secp = secp_inst.lock(); + let sk = match SecretKey::from_slice(&secp, &key_bytes) { + Ok(s) => s, + Err(_) => return, + }; + + let mut shared_mask_ref = mask.lock(); + *shared_mask_ref = Some(sk); + } + } + + /// Decrypt an encrypted request + pub fn decrypt_request( + key: Arc>>, + req: &serde_json::Value, + ) -> Result<(JsonId, serde_json::Value), serde_json::Value> { + let share_key_ref = key.lock(); + let shared_key = share_key_ref.as_ref().unwrap(); + let enc_req: EncryptedRequest = serde_json::from_value(req.clone()).map_err(|e| { + EncryptionErrorResponse::new( + 1, + -32002, + &format!("Encrypted request format error: {}", e), + ) + .as_json_value() + })?; + let id = enc_req.id.clone(); + let res = enc_req.decrypt(&shared_key).map_err(|e| { + EncryptionErrorResponse::new(1, -32002, &format!("Decryption error: {}", e)) + .as_json_value() + })?; + Ok((id, res)) + } + + /// Encrypt a response + pub fn encrypt_response( + key: Arc>>, + id: &JsonId, + res: &serde_json::Value, + ) -> Result { + let share_key_ref = key.lock(); + let shared_key = share_key_ref.as_ref().unwrap(); + let enc_res = EncryptedResponse::from_json(id, res, &shared_key).map_err(|e| { + EncryptionErrorResponse::new(1, -32003, &format!("EncryptionError: {}", e)) + .as_json_value() + })?; + let res = enc_res.as_json_value().map_err(|e| { + EncryptionErrorResponse::new( + 1, + -32002, + &format!("Encrypted response format error: {}", e), + ) + .as_json_value() + })?; + Ok(res) + } + + /// convert an internal error (if exists) as proper JSON-RPC + pub fn check_error_response(val: &serde_json::Value) -> (bool, serde_json::Value) { + // check for string first. This ensures that error messages + // that are just strings aren't given weird formatting + let err_string = if val["result"]["Err"].is_object() { + let mut retval; + let hashed: Result, serde_json::Error> = + serde_json::from_value(val["result"]["Err"].clone()); + retval = match hashed { + Err(e) => { + debug!("Can't cast value to Hashmap {}", e); + None + } + Ok(h) => { + let mut r = "".to_owned(); + for (k, v) in h.iter() { + r = format!("{}: {}", k, v); + } + Some(r) + } + }; + // Otherwise, see if error message is a map that needs + // to be stringified (and accept weird formatting) + if retval.is_none() { + let hashed: Result, serde_json::Error> = + serde_json::from_value(val["result"]["Err"].clone()); + retval = match hashed { + Err(e) => { + debug!("Can't cast value to Hashmap {}", e); + None + } + Ok(h) => { + let mut r = "".to_owned(); + for (k, v) in h.iter() { + r = format!("{}: {}", k, v); + } + Some(r) + } + } + } + retval + } else if val["result"]["Err"].is_string() { + let parsed = serde_json::from_value::(val["result"]["Err"].clone()); + match parsed { + Ok(p) => Some(p), + Err(_) => None, + } + } else { + None + }; + match err_string { + Some(s) => { + return ( + true, + serde_json::json!({ + "jsonrpc": "2.0", + "id": val["id"], + "error": { + "message": s, + "code": -32099 + } + }), + ) + } + None => (false, val.clone()), + } + } +} + +impl OwnerAPIHandlerV3 +where + L: WalletLCProvider<'static, C, K>, + C: NodeClient + 'static, + K: Keychain + 'static, +{ + /// Create a new owner API handler for GET methods + pub fn new( + wallet: Arc + 'static>>>, + keychain_mask: Arc>>, + tor_config: Option, + running_foreign: bool, + ) -> OwnerAPIHandlerV3 { + let owner_api = Owner::new(wallet.clone(), None); + owner_api.set_tor_config(tor_config); + let owner_api = Arc::new(owner_api); + OwnerAPIHandlerV3 { + wallet, + owner_api, + shared_key: Arc::new(Mutex::new(None)), + keychain_mask: keychain_mask, + running_foreign, + } + } + + async fn call_api( + req: Request, + key: Arc>>, + mask: Arc>>, + running_foreign: bool, + api: Arc>, + ) -> Result { + let mut val: serde_json::Value = parse_body(req).await?; + let mut is_init_secure_api = OwnerV3Helpers::is_init_secure_api(&val); + let mut was_encrypted = false; + let mut encrypted_req_id = JsonId::StrId(String::from("")); + if !is_init_secure_api { + if let Err(v) = OwnerV3Helpers::check_encryption_started(key.clone()) { + return Ok(v); + } + let res = OwnerV3Helpers::decrypt_request(key.clone(), &val); + match res { + Err(e) => return Ok(e), + Ok(v) => { + encrypted_req_id = v.0.clone(); + val = v.1; + } + } + was_encrypted = true; + } + // check again, in case it was an encrypted call to init_secure_api + is_init_secure_api = OwnerV3Helpers::is_init_secure_api(&val); + // also need to intercept open/close wallet requests + let is_open_wallet = OwnerV3Helpers::is_open_wallet(&val); + match ::handle_request(&*api, val) { + MaybeReply::Reply(mut r) => { + let (_was_error, unencrypted_intercept) = + OwnerV3Helpers::check_error_response(&r.clone()); + if is_open_wallet && running_foreign { + OwnerV3Helpers::update_mask(mask, &r.clone()); + } + if was_encrypted { + let res = OwnerV3Helpers::encrypt_response( + key.clone(), + &encrypted_req_id, + &unencrypted_intercept, + ); + r = match res { + Ok(v) => v, + Err(v) => return Ok(v), + } + } + // intercept init_secure_api response (after encryption, + // in case it was an encrypted call to 'init_api_secure') + if is_init_secure_api { + OwnerV3Helpers::update_owner_api_shared_key( + key.clone(), + &unencrypted_intercept, + api.shared_key.lock().clone(), + ); + } + Ok(r) + } + MaybeReply::DontReply => { + // Since it's http, we need to return something. We return [] because jsonrpc + // clients will parse it as an empty batch response. + Ok(serde_json::json!([])) + } + } + } + + async fn handle_post_request( + req: Request, + key: Arc>>, + mask: Arc>>, + running_foreign: bool, + api: Arc>, + ) -> Result, Error> { + let res = Self::call_api(req, key, mask, running_foreign, api).await?; + Ok(json_response_pretty(&res)) + } +} + +impl api::Handler for OwnerAPIHandlerV3 +where + L: WalletLCProvider<'static, C, K> + 'static, + C: NodeClient + 'static, + K: Keychain + 'static, +{ + fn post(&self, req: Request) -> ResponseFuture { + let key = self.shared_key.clone(); + let mask = self.keychain_mask.clone(); + let running_foreign = self.running_foreign; + let api = self.owner_api.clone(); + + Box::pin(async move { + match Self::handle_post_request(req, key, mask, running_foreign, api).await { + Ok(r) => Ok(r), + Err(e) => { + error!("Request Error: {:?}", e); + Ok(create_error_response(e)) + } + } + }) + } + + fn options(&self, _req: Request) -> ResponseFuture { + Box::pin(async { Ok(create_ok_response("{}")) }) + } +} +/// V2 API Handler/Wrapper for foreign functions +pub struct ForeignAPIHandlerV2 +where + L: WalletLCProvider<'static, C, K> + 'static, + C: NodeClient + 'static, + K: Keychain + 'static, +{ + /// Wallet instance + pub wallet: Arc + 'static>>>, + /// Keychain mask + pub keychain_mask: Arc>>, + /// run in doctest mode + pub test_mode: bool, + /// tor config + pub tor_config: Mutex>, +} + +impl ForeignAPIHandlerV2 +where + L: WalletLCProvider<'static, C, K> + 'static, + C: NodeClient + 'static, + K: Keychain + 'static, +{ + /// Create a new foreign API handler for GET methods + pub fn new( + wallet: Arc + 'static>>>, + keychain_mask: Arc>>, + test_mode: bool, + tor_config: Mutex>, + ) -> ForeignAPIHandlerV2 { + ForeignAPIHandlerV2 { + wallet, + keychain_mask, + test_mode, + tor_config, + } + } + + async fn call_api( + req: Request, + api: Foreign<'static, L, C, K>, + ) -> Result { + let val: serde_json::Value = parse_body(req).await?; + match ::handle_request(&api, val) { + MaybeReply::Reply(r) => Ok(r), + MaybeReply::DontReply => { + // Since it's http, we need to return something. We return [] because jsonrpc + // clients will parse it as an empty batch response. + Ok(serde_json::json!([])) + } + } + } + + async fn handle_post_request( + req: Request, + mask: Option, + wallet: Arc + 'static>>>, + test_mode: bool, + tor_config: Option, + ) -> Result, Error> { + let api = Foreign::new(wallet, mask, Some(check_middleware), test_mode); + api.set_tor_config(tor_config); + let res = Self::call_api(req, api).await?; + Ok(json_response_pretty(&res)) + } +} + +impl api::Handler for ForeignAPIHandlerV2 +where + L: WalletLCProvider<'static, C, K> + 'static, + C: NodeClient + 'static, + K: Keychain + 'static, +{ + fn post(&self, req: Request) -> ResponseFuture { + let mask = self.keychain_mask.lock().clone(); + let wallet = self.wallet.clone(); + let test_mode = self.test_mode; + let tor_config = self.tor_config.lock().clone(); + + Box::pin(async move { + match Self::handle_post_request(req, mask, wallet, test_mode, tor_config).await { + Ok(v) => Ok(v), + Err(e) => { + error!("Request Error: {:?}", e); + Ok(create_error_response(e)) + } + } + }) + } + + fn options(&self, _req: Request) -> ResponseFuture { + Box::pin(async { Ok(create_ok_response("{}")) }) + } +} + +// Utility to serialize a struct into JSON and produce a sensible Response +// out of it. +fn _json_response(s: &T) -> Response +where + T: Serialize, +{ + match serde_json::to_string(s) { + Ok(json) => response(StatusCode::OK, json), + Err(_) => response(StatusCode::INTERNAL_SERVER_ERROR, ""), + } +} + +// pretty-printed version of above +fn json_response_pretty(s: &T) -> Response +where + T: Serialize, +{ + match serde_json::to_string_pretty(s) { + Ok(json) => response(StatusCode::OK, json), + Err(_) => response(StatusCode::INTERNAL_SERVER_ERROR, ""), + } +} + +fn create_error_response(e: Error) -> Response { + Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .header("access-control-allow-origin", "*") + .header( + "access-control-allow-headers", + "Content-Type, Authorization", + ) + .body(format!("{}", e).into()) + .unwrap() +} + +fn create_ok_response(json: &str) -> Response { + Response::builder() + .status(StatusCode::OK) + .header("access-control-allow-origin", "*") + .header( + "access-control-allow-headers", + "Content-Type, Authorization", + ) + .header(hyper::header::CONTENT_TYPE, "application/json") + .body(json.to_string().into()) + .unwrap() +} + +/// Build a new hyper Response with the status code and body provided. +/// +/// Whenever the status code is `StatusCode::OK` the text parameter should be +/// valid JSON as the content type header will be set to `application/json' +fn response>(status: StatusCode, text: T) -> Response { + let mut builder = Response::builder() + .status(status) + .header("access-control-allow-origin", "*") + .header( + "access-control-allow-headers", + "Content-Type, Authorization", + ); + + if status == StatusCode::OK { + builder = builder.header(hyper::header::CONTENT_TYPE, "application/json"); + } + + builder.body(text.into()).unwrap() +} + +async fn parse_body(req: Request) -> Result +where + for<'de> T: Deserialize<'de> + Send + 'static, +{ + let body = body::to_bytes(req.into_body()) + .await + .map_err(|_| Error::GenericError("Failed to read request".to_string()))?; + + serde_json::from_reader(&body[..]) + .map_err(|e| Error::GenericError(format!("Invalid request body: {}", e))) +} diff --git a/controller/src/display.rs b/controller/src/display.rs new file mode 100644 index 0000000..5668485 --- /dev/null +++ b/controller/src/display.rs @@ -0,0 +1,630 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::core::core::FeeFields; +use crate::core::core::{self, amount_to_hr_string}; +use crate::core::global; +use crate::libwallet::{ + AcctPathMapping, Error, OutputCommitMapping, OutputStatus, TxLogEntry, ViewWallet, WalletInfo, +}; +use crate::util::ToHex; +use grin_wallet_util::OnionV3Address; +use prettytable; +use std::io::prelude::Write; +use term; + +/// Display outputs in a pretty way +pub fn outputs( + account: &str, + cur_height: u64, + validated: bool, + outputs: Vec, + dark_background_color_scheme: bool, +) -> Result<(), Error> { + let title = format!( + "Wallet Outputs - Account '{}' - Block Height: {}", + account, cur_height + ); + println!(); + if term::stdout().is_none() { + println!("Could not open terminal"); + return Ok(()); + } + let mut t = term::stdout().unwrap(); + t.fg(term::color::MAGENTA).unwrap(); + writeln!(t, "{}", title).unwrap(); + t.reset().unwrap(); + + let mut table = table!(); + + table.set_titles(row![ + bMG->"Output Commitment", + bMG->"MMR Index", + bMG->"Block Height", + bMG->"Locked Until", + bMG->"Status", + bMG->"Coinbase?", + bMG->"# Confirms", + bMG->"Value", + bMG->"Tx" + ]); + + for m in outputs { + let commit = format!("{}", m.commit.as_ref().to_hex()); + let index = match m.output.mmr_index { + None => "None".to_owned(), + Some(t) => t.to_string(), + }; + let height = format!("{}", m.output.height); + let lock_height = format!("{}", m.output.lock_height); + let is_coinbase = format!("{}", m.output.is_coinbase); + + // Mark unconfirmed coinbase outputs as "Mining" instead of "Unconfirmed" + let status = match m.output.status { + OutputStatus::Unconfirmed if m.output.is_coinbase => "Mining".to_string(), + _ => format!("{}", m.output.status), + }; + + let num_confirmations = format!("{}", m.output.num_confirmations(cur_height)); + let value = format!("{}", core::amount_to_hr_string(m.output.value, false)); + let tx = match m.output.tx_log_entry { + None => "".to_owned(), + Some(t) => t.to_string(), + }; + + if dark_background_color_scheme { + table.add_row(row![ + bFC->commit, + bFB->index, + bFB->height, + bFB->lock_height, + bFR->status, + bFY->is_coinbase, + bFB->num_confirmations, + bFG->value, + bFC->tx, + ]); + } else { + table.add_row(row![ + bFD->commit, + bFB->index, + bFB->height, + bFB->lock_height, + bFR->status, + bFD->is_coinbase, + bFB->num_confirmations, + bFG->value, + bFD->tx, + ]); + } + } + + table.set_format(*prettytable::format::consts::FORMAT_NO_COLSEP); + table.printstd(); + println!(); + + if !validated { + println!( + "\nWARNING: Wallet failed to verify data. \ + The above is from local cache and possibly invalid! \ + (is your `grin server` offline or broken?)" + ); + } + Ok(()) +} + +/// Display transaction log in a pretty way +pub fn txs( + account: &str, + cur_height: u64, + validated: bool, + txs: &[TxLogEntry], + include_status: bool, + dark_background_color_scheme: bool, +) -> Result<(), Error> { + let title = format!( + "Transaction Log - Account '{}' - Block Height: {}", + account, cur_height + ); + println!(); + if term::stdout().is_none() { + println!("Could not open terminal"); + return Ok(()); + } + let mut t = term::stdout().unwrap(); + t.fg(term::color::MAGENTA).unwrap(); + writeln!(t, "{}", title).unwrap(); + t.reset().unwrap(); + + let mut table = table!(); + + table.set_titles(row![ + bMG->"Id", + bMG->"Type", + bMG->"Shared Transaction Id", + bMG->"Creation Time", + bMG->"TTL Cutoff Height", + bMG->"Confirmed?", + bMG->"Confirmation Time", + bMG->"Num. \nInputs", + bMG->"Num. \nOutputs", + bMG->"Amount \nCredited", + bMG->"Amount \nDebited", + bMG->"Fee", + bMG->"Net \nDifference", + bMG->"Payment \nProof", + bMG->"Kernel", + bMG->"Tx \nData", + ]); + + for t in txs { + let id = format!("{}", t.id); + let slate_id = match t.tx_slate_id { + Some(m) => format!("{}", m), + None => "None".to_owned(), + }; + let entry_type = format!("{}", t.tx_type); + let creation_ts = format!("{}", t.creation_ts.format("%Y-%m-%d %H:%M:%S")); + let ttl_cutoff_height = match t.ttl_cutoff_height { + Some(b) => format!("{}", b), + None => "None".to_owned(), + }; + let confirmation_ts = match t.confirmation_ts { + Some(m) => format!("{}", m.format("%Y-%m-%d %H:%M:%S")), + None => "None".to_owned(), + }; + let confirmed = format!("{}", t.confirmed); + let num_inputs = format!("{}", t.num_inputs); + let num_outputs = format!("{}", t.num_outputs); + let amount_debited_str = core::amount_to_hr_string(t.amount_debited, true); + let amount_credited_str = core::amount_to_hr_string(t.amount_credited, true); + let fee = match t.fee { + Some(f) => format!("{}", core::amount_to_hr_string(f.fee(), true)), + None => "None".to_owned(), + }; + let net_diff = if t.amount_credited >= t.amount_debited { + core::amount_to_hr_string(t.amount_credited - t.amount_debited, true) + } else { + format!( + "-{}", + core::amount_to_hr_string(t.amount_debited - t.amount_credited, true) + ) + }; + let tx_data = match t.stored_tx { + Some(_) => "Yes".to_owned(), + None => "None".to_owned(), + }; + let kernel_excess = match t.kernel_excess { + Some(e) => { + let excess: &[u8] = e.0.as_ref(); + excess.to_hex() + } + None => "None".to_owned(), + }; + let payment_proof = match t.payment_proof { + Some(_) => "Yes".to_owned(), + None => "None".to_owned(), + }; + if dark_background_color_scheme { + table.add_row(row![ + bFC->id, + bFC->entry_type, + bFC->slate_id, + bFB->creation_ts, + bFB->ttl_cutoff_height, + bFC->confirmed, + bFB->confirmation_ts, + bFC->num_inputs, + bFC->num_outputs, + bFG->amount_credited_str, + bFR->amount_debited_str, + bFR->fee, + bFY->net_diff, + bfG->payment_proof, + bFB->kernel_excess, + bFb->tx_data, + ]); + } else { + if t.confirmed { + table.add_row(row![ + bFD->id, + bFb->entry_type, + bFD->slate_id, + bFB->creation_ts, + bFg->confirmed, + bFB->confirmation_ts, + bFD->num_inputs, + bFD->num_outputs, + bFG->amount_credited_str, + bFD->amount_debited_str, + bFD->fee, + bFG->net_diff, + bfG->payment_proof, + bFB->kernel_excess, + bFB->tx_data, + ]); + } else { + table.add_row(row![ + bFD->id, + bFb->entry_type, + bFD->slate_id, + bFB->creation_ts, + bFR->confirmed, + bFB->confirmation_ts, + bFD->num_inputs, + bFD->num_outputs, + bFG->amount_credited_str, + bFD->amount_debited_str, + bFD->fee, + bFG->net_diff, + bfG->payment_proof, + bFB->kernel_excess, + bFB->tx_data, + ]); + } + } + } + + table.set_format(*prettytable::format::consts::FORMAT_NO_COLSEP); + table.printstd(); + println!(); + + if !validated && include_status { + println!( + "\nWARNING: Wallet failed to verify data. \ + The above is from local cache and possibly invalid! \ + (is your `grin server` offline or broken?)" + ); + } + Ok(()) +} + +pub fn view_wallet_balance(w: ViewWallet, cur_height: u64, dark_background_color_scheme: bool) { + println!( + "\n____ View Wallet Summary Info - Block Height: {} ____\n Rewind Hash - {}\n", + cur_height, w.rewind_hash + ); + let mut table = table!(); + + if dark_background_color_scheme { + table.add_row(row![ + bFG->"Total Balance", + FG->amount_to_hr_string(w.total_balance, false) + ]); + } else { + table.add_row(row![ + bFG->"Total Balance", + FG->amount_to_hr_string(w.total_balance, false) + ]); + }; + table.set_format(*prettytable::format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); + table.printstd(); + println!(); +} + +pub fn view_wallet_output( + view_wallet: ViewWallet, + cur_height: u64, + dark_background_color_scheme: bool, +) -> Result<(), Error> { + println!(); + let title = format!("View Wallet Outputs - Block Height: {}", cur_height); + + if term::stdout().is_none() { + println!("Could not open terminal"); + return Ok(()); + } + + let mut t = term::stdout().unwrap(); + t.fg(term::color::MAGENTA).unwrap(); + writeln!(t, "{}", title).unwrap(); + t.reset().unwrap(); + + let mut table = table!(); + + table.set_titles(row![ + bMG->"Output Commitment", + bMG->"MMR Index", + bMG->"Block Height", + bMG->"Locked Until", + bMG->"Coinbase?", + bMG->"# Confirms", + bMG->"Value", + ]); + + for m in view_wallet.output_result { + let commit = format!("{}", m.commit); + let index = m.mmr_index; + let height = format!("{}", m.height); + let lock_height = format!("{}", m.lock_height); + let is_coinbase = format!("{}", m.is_coinbase); + let num_confirmations = format!("{}", m.num_confirmations(cur_height)); + let value = format!("{}", core::amount_to_hr_string(m.value, false)); + + if dark_background_color_scheme { + table.add_row(row![ + bFC->commit, + bFB->index, + bFB->height, + bFB->lock_height, + bFY->is_coinbase, + bFB->num_confirmations, + bFG->value, + ]); + } else { + table.add_row(row![ + bFD->commit, + bFB->index, + bFB->height, + bFB->lock_height, + bFD->is_coinbase, + bFB->num_confirmations, + bFG->value, + ]); + } + } + + table.set_format(*prettytable::format::consts::FORMAT_NO_COLSEP); + table.printstd(); + println!(); + Ok(()) +} + +/// Display summary info in a pretty way +pub fn info( + account: &str, + wallet_info: &WalletInfo, + validated: bool, + dark_background_color_scheme: bool, +) { + println!( + "\n____ Wallet Summary Info - Account '{}' as of height {} ____\n", + account, wallet_info.last_confirmed_height, + ); + + let mut table = table!(); + + if dark_background_color_scheme { + table.add_row(row![ + bFG->"Confirmed Total", + FG->amount_to_hr_string(wallet_info.total, false) + ]); + if wallet_info.amount_reverted > 0 { + table.add_row(row![ + Fr->format!("Reverted"), + Fr->amount_to_hr_string(wallet_info.amount_reverted, false) + ]); + } + // Only dispay "Immature Coinbase" if we have related outputs in the wallet. + // This row just introduces confusion if the wallet does not receive coinbase rewards. + if wallet_info.amount_immature > 0 { + table.add_row(row![ + bFY->format!("Immature Coinbase (< {})", global::coinbase_maturity()), + FY->amount_to_hr_string(wallet_info.amount_immature, false) + ]); + } + table.add_row(row![ + bFY->format!("Awaiting Confirmation (< {})", wallet_info.minimum_confirmations), + FY->amount_to_hr_string(wallet_info.amount_awaiting_confirmation, false) + ]); + table.add_row(row![ + bFB->format!("Awaiting Finalization"), + FB->amount_to_hr_string(wallet_info.amount_awaiting_finalization, false) + ]); + table.add_row(row![ + Fr->"Locked by previous transaction", + Fr->amount_to_hr_string(wallet_info.amount_locked, false) + ]); + table.add_row(row![ + Fw->"--------------------------------", + Fw->"-------------" + ]); + table.add_row(row![ + bFG->"Currently Spendable", + FG->amount_to_hr_string(wallet_info.amount_currently_spendable, false) + ]); + } else { + table.add_row(row![ + bFG->"Total", + FG->amount_to_hr_string(wallet_info.total, false) + ]); + if wallet_info.amount_reverted > 0 { + table.add_row(row![ + Fr->format!("Reverted"), + Fr->amount_to_hr_string(wallet_info.amount_reverted, false) + ]); + } + // Only dispay "Immature Coinbase" if we have related outputs in the wallet. + // This row just introduces confusion if the wallet does not receive coinbase rewards. + if wallet_info.amount_immature > 0 { + table.add_row(row![ + bFB->format!("Immature Coinbase (< {})", global::coinbase_maturity()), + FB->amount_to_hr_string(wallet_info.amount_immature, false) + ]); + } + table.add_row(row![ + bFB->format!("Awaiting Confirmation (< {})", wallet_info.minimum_confirmations), + FB->amount_to_hr_string(wallet_info.amount_awaiting_confirmation, false) + ]); + table.add_row(row![ + Fr->"Locked by previous transaction", + Fr->amount_to_hr_string(wallet_info.amount_locked, false) + ]); + table.add_row(row![ + Fw->"--------------------------------", + Fw->"-------------" + ]); + table.add_row(row![ + bFG->"Currently Spendable", + FG->amount_to_hr_string(wallet_info.amount_currently_spendable, false) + ]); + }; + table.set_format(*prettytable::format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); + table.printstd(); + println!(); + if !validated { + println!( + "\nWARNING: Wallet failed to verify data against a live chain. \ + The above is from local cache and only valid up to the given height! \ + (is your `grin server` offline or broken?)" + ); + } +} + +/// Display summary info in a pretty way +pub fn estimate( + amount: u64, + strategies: Vec<( + &str, // strategy + u64, // total amount to be locked + FeeFields, // fee + )>, + dark_background_color_scheme: bool, +) { + println!( + "\nEstimation for sending {}:\n", + amount_to_hr_string(amount, false) + ); + + let mut table = table!(); + + table.set_titles(row![ + bMG->"Selection strategy", + bMG->"Fee", + bMG->"Will be locked", + ]); + + for (strategy, total, fee_fields) in strategies { + if dark_background_color_scheme { + table.add_row(row![ + bFC->strategy, + FR->amount_to_hr_string(fee_fields.fee(), false), // apply fee mask past HF4 + FY->amount_to_hr_string(total, false), + ]); + } else { + table.add_row(row![ + bFD->strategy, + FR->amount_to_hr_string(fee_fields.fee(), false), // apply fee mask past HF4 + FY->amount_to_hr_string(total, false), + ]); + } + } + table.printstd(); + println!(); +} + +/// Display list of wallet accounts in a pretty way +pub fn accounts(acct_mappings: Vec) { + println!("\n____ Wallet Accounts ____\n",); + let mut table = table!(); + + table.set_titles(row![ + mMG->"Name", + bMG->"Parent BIP-32 Derivation Path", + ]); + for m in acct_mappings { + table.add_row(row![ + bFC->m.label, + bGC->m.path.to_bip_32_string(), + ]); + } + table.set_format(*prettytable::format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); + table.printstd(); + println!(); +} + +/// Display individual Payment Proof +pub fn payment_proof(tx: &TxLogEntry) -> Result<(), Error> { + let title = format!("Payment Proof - Transaction '{}'", tx.id,); + println!(); + if term::stdout().is_none() { + println!("Could not open terminal"); + return Ok(()); + } + let mut t = term::stdout().unwrap(); + t.fg(term::color::MAGENTA).unwrap(); + writeln!(t, "{}", title).unwrap(); + t.reset().unwrap(); + + let pp = match &tx.payment_proof { + None => { + writeln!(t, "None").unwrap(); + t.reset().unwrap(); + return Ok(()); + } + Some(p) => p.clone(), + }; + + t.fg(term::color::WHITE).unwrap(); + writeln!(t).unwrap(); + let receiver_signature = match pp.receiver_signature { + Some(s) => { + let sig_bytes = s.to_bytes(); + let sig_ref: &[u8] = sig_bytes.as_ref(); + sig_ref.to_hex() + } + None => "None".to_owned(), + }; + let fee = match tx.fee { + Some(f) => f.fee(), // apply fee mask past HF4 + None => 0, + }; + let amount = if tx.amount_credited >= tx.amount_debited { + core::amount_to_hr_string(tx.amount_credited - tx.amount_debited, true) + } else { + format!( + "{}", + core::amount_to_hr_string(tx.amount_debited - tx.amount_credited - fee, true) + ) + }; + + let sender_signature = match pp.sender_signature { + Some(s) => { + let sig_bytes = s.to_bytes(); + let sig_ref: &[u8] = sig_bytes.as_ref(); + sig_ref.to_hex() + } + None => "None".to_owned(), + }; + let kernel_excess = match tx.kernel_excess { + Some(e) => { + let excess: &[u8] = e.0.as_ref(); + excess.to_hex() + } + None => "None".to_owned(), + }; + + writeln!( + t, + "Receiver Address: {}", + OnionV3Address::from_bytes(pp.receiver_address.to_bytes()) + ) + .unwrap(); + writeln!(t, "Receiver Signature: {}", receiver_signature).unwrap(); + writeln!(t, "Amount: {}", amount).unwrap(); + writeln!(t, "Kernel Excess: {}", kernel_excess).unwrap(); + writeln!( + t, + "Sender Address: {}", + OnionV3Address::from_bytes(pp.sender_address.to_bytes()) + ) + .unwrap(); + writeln!(t, "Sender Signature: {}", sender_signature).unwrap(); + + t.reset().unwrap(); + + println!(); + + Ok(()) +} diff --git a/controller/src/error.rs b/controller/src/error.rs new file mode 100644 index 0000000..e2ad77b --- /dev/null +++ b/controller/src/error.rs @@ -0,0 +1,105 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Implementation specific error types +use crate::api; +use crate::core::core::transaction; +use crate::core::libtx; +use crate::impls; +use crate::keychain; +use crate::libwallet; + +/// Wallet errors, mostly wrappers around underlying crypto or I/O errors. +#[derive(Clone, Eq, PartialEq, Debug, thiserror::Error)] +pub enum Error { + /// LibTX Error + #[error("LibTx Error")] + LibTX(#[from] libtx::Error), + + /// Impls error + #[error("Impls Error")] + Impls(#[from] impls::Error), + + /// LibWallet Error + #[error("LibWallet Error: {0}")] + LibWallet(#[from] libwallet::Error), + + /// Keychain error + #[error("Keychain error")] + Keychain(#[from] keychain::Error), + + /// Transaction Error + #[error("Transaction error")] + Transaction(#[from] transaction::Error), + + /// Secp Error + #[error("Secp error")] + Secp, + + /// Filewallet error + #[error("Wallet data error: {0}")] + FileWallet(&'static str), + + /// Error when formatting json + #[error("IO error")] + IO, + + /// Error when formatting json + #[error("Serde JSON error")] + Format, + + /// Error when contacting a node through its API + #[error("Node API error")] + Node(#[from] api::Error), + + /// Error originating from hyper. + #[error("Hyper error")] + Hyper, + + /// Error originating from hyper uri parsing. + #[error("Uri parsing error")] + Uri, + + /// Attempt to use duplicate transaction id in separate transactions + #[error("Duplicate transaction ID error")] + DuplicateTransactionId, + + /// Wallet seed already exists + #[error("Wallet seed file exists: {0}")] + WalletSeedExists(String), + + /// Wallet seed doesn't exist + #[error("Wallet seed doesn't exist error")] + WalletSeedDoesntExist, + + /// Enc/Decryption Error + #[error("Enc/Decryption error (check password?)")] + Encryption, + + /// BIP 39 word list + #[error("BIP39 Mnemonic (word list) Error")] + Mnemonic, + + /// Command line argument error + #[error("{0}")] + ArgumentError(String), + + /// Other + #[error("Listener Startup Error")] + ListenerError, + + /// Other + #[error("Generic error: {0}")] + GenericError(String), +} diff --git a/controller/src/lib.rs b/controller/src/lib.rs new file mode 100644 index 0000000..430f975 --- /dev/null +++ b/controller/src/lib.rs @@ -0,0 +1,38 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Library module for the main wallet functionalities provided by Grin. + +#[macro_use] +extern crate prettytable; + +#[macro_use] +extern crate log; +#[macro_use] +extern crate lazy_static; +use grin_api as api; +use grin_core as core; +use grin_keychain as keychain; +use grin_util as util; +use grin_wallet_api as apiwallet; +use grin_wallet_config as config; +use grin_wallet_impls as impls; +use grin_wallet_libwallet as libwallet; + +pub mod command; +pub mod controller; +pub mod display; +mod error; + +pub use crate::error::Error; diff --git a/controller/tests/accounts.rs b/controller/tests/accounts.rs new file mode 100644 index 0000000..744a5e1 --- /dev/null +++ b/controller/tests/accounts.rs @@ -0,0 +1,276 @@ +// Copyright 2021 The Grin Developers +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! tests differing accounts in the same wallet +#[macro_use] +extern crate log; +extern crate grin_wallet_controller as wallet; +extern crate grin_wallet_impls as impls; + +use grin_core as core; +use grin_keychain as keychain; + +use self::core::global; +use self::keychain::{ExtKeychain, Keychain}; +use grin_wallet_libwallet as libwallet; +use impls::test_framework::{self, LocalWalletClient}; +use libwallet::InitTxArgs; +use std::sync::atomic::Ordering; +use std::thread; +use std::time::Duration; + +#[macro_use] +mod common; +use common::{clean_output_dir, create_wallet_proxy, setup}; + +/// Various tests on accounts within the same wallet +fn accounts_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { + // Create a new proxy to simulate server and wallet responses + let mut wallet_proxy = create_wallet_proxy(test_dir); + let chain = wallet_proxy.chain.clone(); + let stopper = wallet_proxy.running.clone(); + + create_wallet_and_add!( + client1, + wallet1, + mask1_i, + test_dir, + "wallet1", + None, + &mut wallet_proxy, + false + ); + + let mask1 = (&mask1_i).as_ref(); + + create_wallet_and_add!( + client2, + wallet2, + mask2_i, + test_dir, + "wallet2", + None, + &mut wallet_proxy, + false + ); + + let mask2 = (&mask2_i).as_ref(); + + // Set the wallet proxy listener running + thread::spawn(move || { + if let Err(e) = wallet_proxy.run() { + error!("Wallet Proxy error: {}", e); + } + }); + + // few values to keep things shorter + let reward = core::consensus::REWARD; + let cm = global::coinbase_maturity(); // assume all testing precedes soft fork height + + // test default accounts exist + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let accounts = api.accounts(m)?; + assert_eq!(accounts[0].label, "default"); + assert_eq!(accounts[0].path, ExtKeychain::derive_key_id(2, 0, 0, 0, 0)); + Ok(()) + })?; + + // add some accounts + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let new_path = api.create_account_path(m, "account1").unwrap(); + assert_eq!(new_path, ExtKeychain::derive_key_id(2, 1, 0, 0, 0)); + let new_path = api.create_account_path(m, "account2").unwrap(); + assert_eq!(new_path, ExtKeychain::derive_key_id(2, 2, 0, 0, 0)); + let new_path = api.create_account_path(m, "account3").unwrap(); + assert_eq!(new_path, ExtKeychain::derive_key_id(2, 3, 0, 0, 0)); + // trying to add same label again should fail + let res = api.create_account_path(m, "account1"); + assert!(res.is_err()); + Ok(()) + })?; + + // add account to wallet 2 + wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| { + let new_path = api.create_account_path(m, "listener_account").unwrap(); + assert_eq!(new_path, ExtKeychain::derive_key_id(2, 1, 0, 0, 0)); + Ok(()) + })?; + + // Default wallet 2 to listen on that account + { + wallet_inst!(wallet2, w); + w.set_parent_key_id_by_name("listener_account")?; + } + + // Mine into two different accounts in the same wallet + { + wallet_inst!(wallet1, w); + w.set_parent_key_id_by_name("account1")?; + assert_eq!(w.parent_key_id(), ExtKeychain::derive_key_id(2, 1, 0, 0, 0)); + } + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 7, false); + + { + wallet_inst!(wallet1, w); + w.set_parent_key_id_by_name("account2")?; + assert_eq!(w.parent_key_id(), ExtKeychain::derive_key_id(2, 2, 0, 0, 0)); + } + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 5, false); + + // Should have 5 in account1 (5 spendable), 5 in account (2 spendable) + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(wallet1_refreshed); + assert_eq!(wallet1_info.last_confirmed_height, 12); + assert_eq!(wallet1_info.total, 5 * reward); + assert_eq!(wallet1_info.amount_currently_spendable, (5 - cm) * reward); + // check tx log as well + let (_, txs) = api.retrieve_txs(m, true, None, None, None)?; + assert_eq!(txs.len(), 5); + Ok(()) + })?; + + // now check second account + { + // let mut w_lock = wallet1.lock(); + // let lc = w_lock.lc_provider()?; + // let w = lc.wallet_inst()?; + wallet_inst!(wallet1, w); + w.set_parent_key_id_by_name("account1")?; + } + + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + // check last confirmed height on this account is different from above (should be 0) + let (_, wallet1_info) = api.retrieve_summary_info(m, false, 1)?; + assert_eq!(wallet1_info.last_confirmed_height, 0); + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(wallet1_refreshed); + assert_eq!(wallet1_info.last_confirmed_height, 12); + assert_eq!(wallet1_info.total, 7 * reward); + assert_eq!(wallet1_info.amount_currently_spendable, 7 * reward); + // check tx log as well + let (_, txs) = api.retrieve_txs(m, true, None, None, None)?; + assert_eq!(txs.len(), 7); + Ok(()) + })?; + + // should be nothing in default account + { + wallet_inst!(wallet1, w); + w.set_parent_key_id_by_name("default")?; + } + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let (_, wallet1_info) = api.retrieve_summary_info(m, false, 1)?; + assert_eq!(wallet1_info.last_confirmed_height, 0); + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(wallet1_refreshed); + assert_eq!(wallet1_info.last_confirmed_height, 12); + assert_eq!(wallet1_info.total, 0,); + assert_eq!(wallet1_info.amount_currently_spendable, 0,); + // check tx log as well + let (_, txs) = api.retrieve_txs(m, true, None, None, None)?; + assert_eq!(txs.len(), 0); + Ok(()) + })?; + + // Send a tx to another wallet + { + wallet_inst!(wallet1, w); + w.set_parent_key_id_by_name("account1")?; + } + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let args = InitTxArgs { + src_acct_name: None, + amount: reward, + minimum_confirmations: 2, + max_outputs: 500, + num_change_outputs: 1, + selection_strategy_is_use_all: true, + ..Default::default() + }; + let mut slate = api.init_send_tx(m, args)?; + slate = client1.send_tx_slate_direct("wallet2", &slate)?; + api.tx_lock_outputs(m, &slate)?; + slate = api.finalize_tx(m, &slate)?; + api.post_tx(m, &slate, false)?; + Ok(()) + })?; + + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(wallet1_refreshed); + assert_eq!(wallet1_info.last_confirmed_height, 13); + let (_, txs) = api.retrieve_txs(m, true, None, None, None)?; + assert_eq!(txs.len(), 9); + Ok(()) + })?; + + // other account should be untouched + { + wallet_inst!(wallet1, w); + w.set_parent_key_id_by_name("account2")?; + } + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let (_, wallet1_info) = api.retrieve_summary_info(m, false, 1)?; + assert_eq!(wallet1_info.last_confirmed_height, 12); + let (_, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + assert_eq!(wallet1_info.last_confirmed_height, 13); + let (_, txs) = api.retrieve_txs(m, true, None, None, None)?; + println!("{:?}", txs); + assert_eq!(txs.len(), 5); + Ok(()) + })?; + + // wallet 2 should only have this tx on the listener account + wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| { + let (wallet2_refreshed, wallet2_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(wallet2_refreshed); + assert_eq!(wallet2_info.last_confirmed_height, 13); + let (_, txs) = api.retrieve_txs(m, true, None, None, None)?; + assert_eq!(txs.len(), 1); + Ok(()) + })?; + // Default account on wallet 2 should be untouched + { + wallet_inst!(wallet2, w); + w.set_parent_key_id_by_name("default")?; + } + wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| { + let (_, wallet2_info) = api.retrieve_summary_info(m, false, 1)?; + assert_eq!(wallet2_info.last_confirmed_height, 0); + let (wallet2_refreshed, wallet2_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(wallet2_refreshed); + assert_eq!(wallet2_info.last_confirmed_height, 13); + assert_eq!(wallet2_info.total, 0,); + assert_eq!(wallet2_info.amount_currently_spendable, 0,); + // check tx log as well + let (_, txs) = api.retrieve_txs(m, true, None, None, None)?; + assert_eq!(txs.len(), 0); + Ok(()) + })?; + + // let logging finish + stopper.store(false, Ordering::Relaxed); + thread::sleep(Duration::from_millis(200)); + Ok(()) +} + +#[test] +fn accounts() { + let test_dir = "test_output/accounts"; + setup(test_dir); + if let Err(e) = accounts_test_impl(test_dir) { + panic!("Libwallet Error: {}", e); + } + clean_output_dir(test_dir); +} diff --git a/controller/tests/build_chain.rs b/controller/tests/build_chain.rs new file mode 100644 index 0000000..cd9a2d8 --- /dev/null +++ b/controller/tests/build_chain.rs @@ -0,0 +1,171 @@ +// Copyright 2021 The Grin Developers +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! tests whose only purpose is to build up a 'real' looking chain with +//! actual transactions for testing purposes + +#[macro_use] +extern crate log; +extern crate grin_wallet_controller as wallet; +extern crate grin_wallet_impls as impls; +extern crate grin_wallet_libwallet as libwallet; + +use grin_core as core; + +use self::libwallet::{InitTxArgs, Slate}; +use impls::test_framework::{self, LocalWalletClient}; +use rand::Rng; +use std::sync::atomic::Ordering; +use std::thread; +use std::time::Duration; + +mod common; +use common::{clean_output_dir, create_wallet_proxy, setup}; + +/// Builds a chain with real transactions up to the given height +fn build_chain(test_dir: &'static str, block_height: usize) -> Result<(), libwallet::Error> { + // Create a new proxy to simulate server and wallet responses + let mut wallet_proxy = create_wallet_proxy(test_dir); + let chain = wallet_proxy.chain.clone(); + let stopper = wallet_proxy.running.clone(); + + create_wallet_and_add!( + client1, + wallet1, + mask1_i, + test_dir, + "wallet1", + None, + &mut wallet_proxy, + true + ); + let mask1 = (&mask1_i).as_ref(); + debug!("Mask1: {:?}", mask1); + create_wallet_and_add!( + client2, + wallet2, + mask2_i, + test_dir, + "wallet2", + None, + &mut wallet_proxy, + false + ); + let mask2 = (&mask2_i).as_ref(); + debug!("Mask2: {:?}", mask2); + + // Set the wallet proxy listener running + thread::spawn(move || { + if let Err(e) = wallet_proxy.run() { + error!("Wallet Proxy error: {}", e); + } + }); + + // Stop the scanning updater threads because it extends the time needed to build the chain + // exponentially + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, _m| { + api.stop_updater()?; + Ok(()) + })?; + + wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, _m| { + api.stop_updater()?; + Ok(()) + })?; + + // few values to keep things shorter + let reward = core::consensus::REWARD; + let mut rng = rand::thread_rng(); + + // Start off with a few blocks + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 3, false); + + for _ in 0..block_height { + let mut wallet_1_has_funds = false; + + // Check wallet 1 contents + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let (_, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + debug!( + "Wallet 1 spendable - {}", + wallet1_info.amount_currently_spendable + ); + if wallet1_info.amount_currently_spendable > reward { + wallet_1_has_funds = true; + } + Ok(()) + })?; + + // let's say 1 in every 3 blocks has a transaction (i.e. random 0 here and wallet1 has funds) + let transact = rng.gen_range(0, 2) == 0; + if !transact || !wallet_1_has_funds { + let _ = + test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 1, false); + continue; + } + + // send a random tx or three + let num_txs = rng.gen_range(0, 3); + for _ in 0..num_txs { + let amount: u64 = rng.gen_range(1, 10_000_000_001); + let mut slate = Slate::blank(1, false); + debug!("Creating TX for {}", amount); + wallet::controller::owner_single_use( + Some(wallet1.clone()), + mask1, + None, + |sender_api, m| { + // note this will increment the block count as part of the transaction "Posting" + let args = InitTxArgs { + src_acct_name: None, + amount: amount, + minimum_confirmations: 1, + max_outputs: 500, + num_change_outputs: 1, + selection_strategy_is_use_all: false, + ..Default::default() + }; + let slate_i = sender_api.init_send_tx(m, args)?; + slate = client1.send_tx_slate_direct("wallet2", &slate_i)?; + sender_api.tx_lock_outputs(m, &slate)?; + slate = sender_api.finalize_tx(m, &slate)?; + Ok(()) + }, + )?; + } + } + // let logging finish + stopper.store(false, Ordering::Relaxed); + thread::sleep(Duration::from_millis(200)); + Ok(()) +} + +#[test] +#[ignore] +fn build_chain_to_height() { + // ****************** + // If letting this run for a while to build a chain, recommend also tweaking scan threshold around 1112 of owner.rs: + // *** + // let start_index = last_scanned_block.height.saturating_sub(1); + // *** + // TODO: Make this parameter somehow + // ****************** + + let test_dir = "test_output/build_chain"; + clean_output_dir(test_dir); + setup(test_dir); + if let Err(e) = build_chain(test_dir, 2048) { + panic!("Libwallet Error: {}", e); + } + // don't clean to get the result for testing +} diff --git a/controller/tests/build_output.rs b/controller/tests/build_output.rs new file mode 100644 index 0000000..7f05a2a --- /dev/null +++ b/controller/tests/build_output.rs @@ -0,0 +1,101 @@ +// Copyright 2021 The Grin Developers +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#[macro_use] +extern crate log; +extern crate grin_wallet_controller as wallet; +extern crate grin_wallet_impls as impls; +extern crate grin_wallet_util; + +use grin_core::core::OutputFeatures; +use grin_keychain::{ + mnemonic, BlindingFactor, ExtKeychain, ExtKeychainPath, Keychain, SwitchCommitmentType, +}; +use grin_util::{secp, ZeroingString}; +use grin_wallet_libwallet as libwallet; +use impls::test_framework::LocalWalletClient; +use rand::{thread_rng, Rng}; +use std::sync::atomic::Ordering; +use std::thread; +use std::time::Duration; + +#[macro_use] +mod common; +use common::{clean_output_dir, create_wallet_proxy, setup}; + +fn build_output_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { + // Generate seed so we can verify the blinding factor is derived correctly + let seed: [u8; 32] = thread_rng().gen(); + let keychain = ExtKeychain::from_seed(&seed, false).unwrap(); + let mnemonic = mnemonic::from_entropy(&seed).unwrap(); + + // Create a new proxy to simulate server and wallet responses + let mut wallet_proxy = create_wallet_proxy(test_dir); + let stopper = wallet_proxy.running.clone(); + + create_wallet_and_add!( + client1, + wallet1, + mask1_i, + test_dir, + "wallet1", + Some(ZeroingString::from(mnemonic)), + &mut wallet_proxy, + false + ); + + let mask1 = (&mask1_i).as_ref(); + + // Set the wallet proxy listener running + thread::spawn(move || { + if let Err(e) = wallet_proxy.run() { + error!("Wallet Proxy error: {}", e); + } + }); + + let secp = secp::Secp256k1::with_caps(secp::ContextFlag::Commit); + let features = OutputFeatures::Plain; + let amount = 60_000_000_000; + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |sender_api, m| { + let built_output = sender_api.build_output(m, features, amount)?; + + let key_id = built_output.key_id; + assert_eq!(key_id.to_path(), ExtKeychainPath::new(3, 0, 0, 0, 0)); + + let blind = built_output.blind; + let key = keychain.derive_key(amount, &key_id, SwitchCommitmentType::Regular)?; + assert_eq!(blind, BlindingFactor::from_secret_key(key.clone())); + + let output = built_output.output; + assert_eq!(output.features(), features); + assert_eq!(output.commitment(), secp.commit(amount, key)?); + output.verify_proof()?; + + Ok(()) + })?; + + // let logging finish + stopper.store(false, Ordering::Relaxed); + thread::sleep(Duration::from_millis(200)); + Ok(()) +} + +#[test] +fn build_output() { + let test_dir = "test_output/build_output"; + setup(test_dir); + if let Err(e) = build_output_test_impl(test_dir) { + panic!("Libwallet Error: {}", e); + } + clean_output_dir(test_dir); +} diff --git a/controller/tests/check.rs b/controller/tests/check.rs new file mode 100644 index 0000000..634cdd2 --- /dev/null +++ b/controller/tests/check.rs @@ -0,0 +1,878 @@ +// Copyright 2021 The Grin Developers +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! tests differing accounts in the same wallet +#[macro_use] +extern crate log; +extern crate grin_wallet_controller as wallet; +extern crate grin_wallet_impls as impls; + +use grin_core as core; +use grin_util as util; + +use self::core::consensus; +use self::core::global; +use grin_wallet_libwallet as libwallet; +use impls::test_framework::{self, LocalWalletClient}; +use impls::{PathToSlate, SlatePutter as _}; +use libwallet::{InitTxArgs, NodeClient}; +use std::sync::atomic::Ordering; +use std::thread; +use std::time::Duration; +use util::ZeroingString; + +#[macro_use] +mod common; +use common::{clean_output_dir, create_wallet_proxy, setup}; + +macro_rules! send_to_dest { + ($a:expr, $m: expr, $b:expr, $c:expr, $d:expr) => { + test_framework::send_to_dest($a, $m, $b, $c, $d, false) + }; +} + +macro_rules! wallet_info { + ($a:expr, $m:expr) => { + test_framework::wallet_info($a, $m) + }; +} + +/// Various tests on checking functionality +fn scan_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { + // Create a new proxy to simulate server and wallet responses + let mut wallet_proxy = create_wallet_proxy(test_dir); + let chain = wallet_proxy.chain.clone(); + let stopper = wallet_proxy.running.clone(); + + // Create a new wallet test client, and set its queues to communicate with the + // proxy + create_wallet_and_add!( + client1, + wallet1, + mask1_i, + test_dir, + "wallet1", + None, + &mut wallet_proxy, + false + ); + let mask1 = (&mask1_i).as_ref(); + create_wallet_and_add!( + client2, + wallet2, + mask2_i, + test_dir, + "wallet2", + None, + &mut wallet_proxy, + false + ); + let mask2 = (&mask2_i).as_ref(); + + // Set the wallet proxy listener running + thread::spawn(move || { + if let Err(e) = wallet_proxy.run() { + error!("Wallet Proxy error: {}", e); + } + }); + + // few values to keep things shorter + let reward = core::consensus::REWARD; + let cm = global::coinbase_maturity() as u64; // assume all testing precedes soft fork height + + // add some accounts + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + api.create_account_path(m, "named_account_1")?; + api.create_account_path(m, "account_2")?; + api.create_account_path(m, "account_3")?; + api.set_active_account(m, "named_account_1")?; + Ok(()) + })?; + + // add account to wallet 2 + wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| { + api.create_account_path(m, "account_1")?; + api.set_active_account(m, "account_1")?; + Ok(()) + })?; + + // Do some mining + let bh = 20u64; + let _ = + test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, bh as usize, false); + + // Sanity check contents + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(wallet1_refreshed); + assert_eq!(wallet1_info.last_confirmed_height, bh); + assert_eq!(wallet1_info.total, bh * reward); + assert_eq!(wallet1_info.amount_currently_spendable, (bh - cm) * reward); + // check tx log as well + let (_, txs) = api.retrieve_txs(m, true, None, None, None)?; + let (c, _) = libwallet::TxLogEntry::sum_confirmed(&txs); + assert_eq!(wallet1_info.total, c); + assert_eq!(txs.len(), bh as usize); + Ok(()) + })?; + + // Accidentally delete some outputs + let mut w1_outputs_commits = vec![]; + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + w1_outputs_commits = api.retrieve_outputs(m, false, true, None)?.1; + Ok(()) + })?; + let w1_outputs: Vec = + w1_outputs_commits.into_iter().map(|m| m.output).collect(); + { + wallet_inst!(wallet1, w); + { + let mut batch = w.batch(mask1)?; + batch.delete(&w1_outputs[4].key_id, &None)?; + batch.delete(&w1_outputs[10].key_id, &None)?; + let mut accidental_spent = w1_outputs[13].clone(); + accidental_spent.status = libwallet::OutputStatus::Spent; + batch.save(accidental_spent)?; + batch.commit()?; + } + } + + // check we have a problem now + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let (_, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + let (_, txs) = api.retrieve_txs(m, true, None, None, None)?; + let (c, _) = libwallet::TxLogEntry::sum_confirmed(&txs); + assert!(wallet1_info.total != c); + Ok(()) + })?; + + // this should restore our missing outputs + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + api.scan(m, None, true)?; + Ok(()) + })?; + + // check our outputs match again + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(wallet1_refreshed); + assert_eq!(wallet1_info.total, bh * reward); + // And check account names haven't been splatted + let accounts = api.accounts(m)?; + assert_eq!(accounts.len(), 4); + assert!(api.set_active_account(m, "account_1").is_err()); + assert!(api.set_active_account(m, "named_account_1").is_ok()); + Ok(()) + })?; + + // perform a transaction, but don't let it finish + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + // send to send + let args = InitTxArgs { + src_acct_name: None, + amount: reward * 2, + minimum_confirmations: cm, + max_outputs: 500, + num_change_outputs: 1, + selection_strategy_is_use_all: true, + ..Default::default() + }; + let slate = api.init_send_tx(m, args)?; + // output tx file + let send_file = format!("{}/part_tx_1.tx", test_dir); + PathToSlate(send_file.into()).put_tx(&slate, false)?; + api.tx_lock_outputs(m, &slate)?; + Ok(()) + })?; + + // check we're all locked + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(wallet1_refreshed); + assert!(wallet1_info.amount_currently_spendable == 0); + Ok(()) + })?; + + // unlock/restore + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + api.scan(m, None, true)?; + Ok(()) + })?; + + // check spendable amount again + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let (_, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + assert_eq!(wallet1_info.amount_currently_spendable, (bh - cm) * reward); + Ok(()) + })?; + + // let logging finish + stopper.store(false, Ordering::Relaxed); + thread::sleep(Duration::from_millis(200)); + Ok(()) +} + +fn two_wallets_one_seed_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { + let seed_phrase = "affair pistol cancel crush garment candy ancient flag work \ + market crush dry stand focus mutual weapon offer ceiling rival turn team spring \ + where swift"; + let seed_phrase = Some(ZeroingString::from(seed_phrase)); + + // Create a new proxy to simulate server and wallet responses + let mut wallet_proxy = create_wallet_proxy(test_dir); + let chain = wallet_proxy.chain.clone(); + let stopper = wallet_proxy.running.clone(); + + // Create a new wallet test client, and set its queues to communicate with the + // proxy + create_wallet_and_add!( + m_client, + miner, + miner_mask_i, + test_dir, + "miner", + None, + &mut wallet_proxy, + false + ); + let miner_mask = (&miner_mask_i).as_ref(); + + // non-mining recipient wallets + create_wallet_and_add!( + client1, + wallet1, + mask1_i, + test_dir, + "wallet1", + seed_phrase, + &mut wallet_proxy, + false + ); + let mask1 = (&mask1_i).as_ref(); + create_wallet_and_add!( + client2, + wallet2, + mask2_i, + test_dir, + "wallet2", + seed_phrase, + &mut wallet_proxy, + false + ); + let mask2 = (&mask2_i).as_ref(); + // we'll restore into here + create_wallet_and_add!( + client3, + wallet3, + mask3_i, + test_dir, + "wallet3", + seed_phrase, + &mut wallet_proxy, + false + ); + let mask3 = (&mask3_i).as_ref(); + // also restore into here + create_wallet_and_add!( + client4, + wallet4, + mask4_i, + test_dir, + "wallet4", + seed_phrase, + &mut wallet_proxy, + false + ); + let mask4 = (&mask4_i).as_ref(); + // Simulate a recover from seed without restore into here + create_wallet_and_add!( + client5, + wallet5, + mask5_i, + test_dir, + "wallet5", + seed_phrase, + &mut wallet_proxy, + false + ); + //simulate a recover from seed without restore into here + let mask5 = (&mask5_i).as_ref(); + create_wallet_and_add!( + client6, + wallet6, + mask6_i, + test_dir, + "wallet6", + seed_phrase, + &mut wallet_proxy, + false + ); + let mask6 = (&mask6_i).as_ref(); + + create_wallet_and_add!( + client7, + wallet7, + mask7_i, + test_dir, + "wallet7", + seed_phrase, + &mut wallet_proxy, + false + ); + let mask7 = (&mask7_i).as_ref(); + create_wallet_and_add!( + client8, + wallet8, + mask8_i, + test_dir, + "wallet8", + seed_phrase, + &mut wallet_proxy, + false + ); + let mask8 = (&mask8_i).as_ref(); + create_wallet_and_add!( + client9, + wallet9, + mask9_i, + test_dir, + "wallet9", + seed_phrase, + &mut wallet_proxy, + false + ); + let mask9 = (&mask9_i).as_ref(); + create_wallet_and_add!( + client10, + wallet10, + mask10_i, + test_dir, + "wallet10", + seed_phrase, + &mut wallet_proxy, + false + ); + let mask10 = (&mask10_i).as_ref(); + + // Set the wallet proxy listener running + thread::spawn(move || { + if let Err(e) = wallet_proxy.run() { + error!("Wallet Proxy error: {}", e); + } + }); + + // few values to keep things shorter + let _reward = core::consensus::REWARD; + let cm = global::coinbase_maturity() as usize; // assume all testing precedes soft fork height + + // Do some mining + let mut bh = 20u64; + let base_amount = consensus::GRIN_BASE; + let _ = test_framework::award_blocks_to_wallet( + &chain, + miner.clone(), + miner_mask, + bh as usize, + false, + ); + + // send some funds to wallets 1 + send_to_dest!( + miner.clone(), + miner_mask, + m_client.clone(), + "wallet1", + base_amount * 1 + )?; + send_to_dest!( + miner.clone(), + miner_mask, + m_client.clone(), + "wallet1", + base_amount * 2 + )?; + send_to_dest!( + miner.clone(), + miner_mask, + m_client.clone(), + "wallet1", + base_amount * 3 + )?; + bh += 3; + + // 0) Check repair when all is okay should leave wallet contents alone + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + api.scan(m, None, true)?; + let info = wallet_info!(wallet1.clone(), m)?; + assert_eq!(info.amount_currently_spendable, base_amount * 6); + assert_eq!(info.total, base_amount * 6); + Ok(()) + })?; + + // send some funds to wallet 2 + send_to_dest!( + miner.clone(), + miner_mask, + m_client.clone(), + "wallet2", + base_amount * 4 + )?; + send_to_dest!( + miner.clone(), + miner_mask, + m_client.clone(), + "wallet2", + base_amount * 5 + )?; + send_to_dest!( + miner.clone(), + miner_mask, + m_client.clone(), + "wallet2", + base_amount * 6 + )?; + bh += 3; + + let _ = test_framework::award_blocks_to_wallet(&chain, miner.clone(), miner_mask, cm, false); + bh += cm as u64; + + // confirm balances + // since info is now performing a partial scan, these should confirm + // as containing all outputs + let info = wallet_info!(wallet1.clone(), mask1)?; + assert_eq!(info.amount_currently_spendable, base_amount * 21); + assert_eq!(info.total, base_amount * 21); + + let info = wallet_info!(wallet2.clone(), mask2)?; + assert_eq!(info.amount_currently_spendable, base_amount * 21); + assert_eq!(info.total, base_amount * 21); + + // Now there should be outputs on the chain using the same + // seed + BIP32 path. + + // 1) a full restore should recover all of them: + wallet::controller::owner_single_use(Some(wallet3.clone()), mask3, None, |api, m| { + api.scan(m, None, false)?; + Ok(()) + })?; + + wallet::controller::owner_single_use(Some(wallet3.clone()), mask3, None, |api, m| { + let info = wallet_info!(wallet3.clone(), m)?; + let outputs = api.retrieve_outputs(m, true, false, None)?.1; + assert_eq!(outputs.len(), 6); + assert_eq!(info.amount_currently_spendable, base_amount * 21); + assert_eq!(info.total, base_amount * 21); + Ok(()) + })?; + + // 2) scan should recover them into a single wallet + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + api.scan(m, None, true)?; + Ok(()) + })?; + + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let info = wallet_info!(wallet1.clone(), m)?; + let outputs = api.retrieve_outputs(m, true, false, None)?.1; + assert_eq!(outputs.len(), 6); + assert_eq!(info.amount_currently_spendable, base_amount * 21); + Ok(()) + })?; + + // 3) If I recover from seed and start using the wallet without restoring, + // scan should restore the older outputs + // update, again, since scan is run automatically, balances on both + // wallets should turn out the same + send_to_dest!( + miner.clone(), + miner_mask, + m_client.clone(), + "wallet4", + base_amount * 7 + )?; + send_to_dest!( + miner.clone(), + miner_mask, + m_client.clone(), + "wallet4", + base_amount * 8 + )?; + send_to_dest!( + miner.clone(), + miner_mask, + m_client.clone(), + "wallet4", + base_amount * 9 + )?; + bh += 3; + + let _ = test_framework::award_blocks_to_wallet(&chain, miner.clone(), miner_mask, cm, false); + bh += cm as u64; + + wallet::controller::owner_single_use(Some(wallet4.clone()), mask4, None, |api, m| { + let info = wallet_info!(wallet4.clone(), m)?; + let outputs = api.retrieve_outputs(m, true, false, None)?.1; + assert_eq!(outputs.len(), 9); + assert_eq!(info.amount_currently_spendable, base_amount * 45); + Ok(()) + })?; + + wallet::controller::owner_single_use(Some(wallet5.clone()), mask5, None, |api, m| { + api.scan(m, None, false)?; + Ok(()) + })?; + + wallet::controller::owner_single_use(Some(wallet5.clone()), mask5, None, |api, m| { + let info = wallet_info!(wallet5.clone(), m)?; + let outputs = api.retrieve_outputs(m, true, false, None)?.1; + assert_eq!(outputs.len(), 9); + assert_eq!(info.amount_currently_spendable, base_amount * (45)); + Ok(()) + })?; + + // 4) If I recover from seed and start using the wallet without restoring, + // scan should restore the older outputs + send_to_dest!( + miner.clone(), + miner_mask, + m_client.clone(), + "wallet6", + base_amount * 10 + )?; + send_to_dest!( + miner.clone(), + miner_mask, + m_client.clone(), + "wallet6", + base_amount * 11 + )?; + send_to_dest!( + miner.clone(), + miner_mask, + m_client.clone(), + "wallet6", + base_amount * 12 + )?; + bh += 3; + + let _ = test_framework::award_blocks_to_wallet( + &chain, + miner.clone(), + miner_mask, + cm as usize, + false, + ); + bh += cm as u64; + + wallet::controller::owner_single_use(Some(wallet6.clone()), mask6, None, |api, m| { + let info = wallet_info!(wallet6.clone(), m)?; + let outputs = api.retrieve_outputs(m, true, false, None)?.1; + assert_eq!(outputs.len(), 12); + assert_eq!(info.amount_currently_spendable, base_amount * 78); + Ok(()) + })?; + + wallet::controller::owner_single_use(Some(wallet6.clone()), mask6, None, |api, m| { + api.scan(m, None, true)?; + Ok(()) + })?; + + wallet::controller::owner_single_use(Some(wallet6.clone()), mask6, None, |api, m| { + let info = wallet_info!(wallet6.clone(), m)?; + let outputs = api.retrieve_outputs(m, true, false, None)?.1; + assert_eq!(outputs.len(), 12); + assert_eq!(info.amount_currently_spendable, base_amount * (78)); + Ok(()) + })?; + + // 5) Start using same seed with a different account, amounts should + // be distinct and restore should return funds from other account + + send_to_dest!( + miner.clone(), + miner_mask, + m_client.clone(), + "wallet7", + base_amount * 13 + )?; + send_to_dest!( + miner.clone(), + miner_mask, + m_client.clone(), + "wallet7", + base_amount * 14 + )?; + send_to_dest!( + miner.clone(), + miner_mask, + m_client.clone(), + "wallet7", + base_amount * 15 + )?; + bh += 3; + + // mix it up a bit + wallet::controller::owner_single_use(Some(wallet7.clone()), mask7, None, |api, m| { + api.create_account_path(m, "account_1")?; + api.set_active_account(m, "account_1")?; + Ok(()) + })?; + + send_to_dest!( + miner.clone(), + miner_mask, + m_client.clone(), + "wallet7", + base_amount * 1 + )?; + send_to_dest!( + miner.clone(), + miner_mask, + m_client.clone(), + "wallet7", + base_amount * 2 + )?; + send_to_dest!( + miner.clone(), + miner_mask, + m_client.clone(), + "wallet7", + base_amount * 3 + )?; + bh += 3; + + // check balances + let _ = test_framework::award_blocks_to_wallet(&chain, miner.clone(), miner_mask, cm, false); + bh += cm as u64; + + wallet::controller::owner_single_use(Some(wallet7.clone()), mask7, None, |api, m| { + let info = wallet_info!(wallet7.clone(), m)?; + let outputs = api.retrieve_outputs(m, true, false, None)?.1; + assert_eq!(outputs.len(), 3); + assert_eq!(info.amount_currently_spendable, base_amount * 6); + api.set_active_account(m, "default")?; + let info = wallet_info!(wallet7.clone(), m)?; + let outputs = api.retrieve_outputs(m, true, false, None)?.1; + assert_eq!(outputs.len(), 15); + assert_eq!(info.amount_currently_spendable, base_amount * 120); + Ok(()) + })?; + + wallet::controller::owner_single_use(Some(wallet8.clone()), mask8, None, |api, m| { + api.scan(m, None, false)?; + let info = wallet_info!(wallet8.clone(), m)?; + let outputs = api.retrieve_outputs(m, true, false, None)?.1; + assert_eq!(outputs.len(), 15); + assert_eq!(info.amount_currently_spendable, base_amount * 120); + api.set_active_account(m, "account_1")?; + let info = wallet_info!(wallet8.clone(), m)?; + let outputs = api.retrieve_outputs(m, true, false, None)?.1; + assert_eq!(outputs.len(), 3); + assert_eq!(info.amount_currently_spendable, base_amount * 6); + Ok(()) + })?; + + // 6) Start using same seed with a different account, now overwriting + // ids on account 2 as well, scan should get all outputs created + // to now into 2 accounts + + wallet::controller::owner_single_use(Some(wallet9.clone()), mask9, None, |api, m| { + api.create_account_path(m, "account_1")?; + api.set_active_account(m, "account_1")?; + Ok(()) + })?; + + send_to_dest!( + miner.clone(), + miner_mask, + m_client.clone(), + "wallet9", + base_amount * 4 + )?; + send_to_dest!( + miner.clone(), + miner_mask, + m_client.clone(), + "wallet9", + base_amount * 5 + )?; + send_to_dest!( + miner.clone(), + miner_mask, + m_client.clone(), + "wallet9", + base_amount * 6 + )?; + bh += 3; + let _bh = bh; + + wallet::controller::owner_single_use(Some(wallet9.clone()), mask9, None, |api, m| { + let info = wallet_info!(wallet9.clone(), m)?; + let outputs = api.retrieve_outputs(m, true, false, None)?.1; + assert_eq!(outputs.len(), 6); + assert_eq!(info.amount_currently_spendable, base_amount * 21); + api.scan(m, None, true)?; + let info = wallet_info!(wallet9.clone(), m)?; + let outputs = api.retrieve_outputs(m, true, false, None)?.1; + assert_eq!(outputs.len(), 6); + assert_eq!(info.amount_currently_spendable, base_amount * 21); + + api.set_active_account(m, "default")?; + let info = wallet_info!(wallet9.clone(), m)?; + let outputs = api.retrieve_outputs(m, true, false, None)?.1; + assert_eq!(outputs.len(), 15); + assert_eq!(info.amount_currently_spendable, base_amount * 120); + Ok(()) + })?; + + let _ = test_framework::award_blocks_to_wallet(&chain, miner.clone(), miner_mask, cm, false); + + // 7) Ensure scan creates missing accounts + wallet::controller::owner_single_use(Some(wallet10.clone()), mask10, None, |api, m| { + api.scan(m, None, true)?; + api.set_active_account(m, "account_1")?; + let info = wallet_info!(wallet10.clone(), m)?; + let outputs = api.retrieve_outputs(m, true, false, None)?.1; + assert_eq!(outputs.len(), 6); + assert_eq!(info.amount_currently_spendable, base_amount * 21); + + api.set_active_account(m, "default")?; + let info = wallet_info!(wallet10.clone(), m)?; + let outputs = api.retrieve_outputs(m, true, false, None)?.1; + assert_eq!(outputs.len(), 15); + assert_eq!(info.amount_currently_spendable, base_amount * 120); + Ok(()) + })?; + + // let logging finish + stopper.store(false, Ordering::Relaxed); + thread::sleep(Duration::from_millis(200)); + Ok(()) +} + +// Testing output scanning functionality, easier here as the testing framework +// is all here +fn output_scanning_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { + let mut wallet_proxy = create_wallet_proxy(test_dir); + let chain = wallet_proxy.chain.clone(); + let stopper = wallet_proxy.running.clone(); + // Create a new wallet test client, and set its queues to communicate with the + // proxy + create_wallet_and_add!( + client1, + wallet1, + mask1_i, + test_dir, + "wallet1", + None, + &mut wallet_proxy, + false + ); + let mask1 = (&mask1_i).as_ref(); + thread::spawn(move || { + if let Err(e) = wallet_proxy.run() { + error!("Wallet Proxy error: {}", e); + } + }); + + // Do some mining + let bh = 20u64; + let _ = + test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, bh as usize, false); + + // Now some chain scanning + { + // Entire range should be correct + let ranges = client1.height_range_to_pmmr_indices(1, None)?; + assert_eq!(ranges, (1, 38)); + let outputs = client1.get_outputs_by_pmmr_index(ranges.0, Some(ranges.1), 1000)?; + assert_eq!(outputs.2.len(), 20); + + // Basic range should be correct + let ranges = client1.height_range_to_pmmr_indices(1, Some(14))?; + assert_eq!(ranges, (1, 25)); + let outputs = client1.get_outputs_by_pmmr_index(ranges.0, Some(ranges.1), 1000)?; + println!( + "Last Index: {}, Max: {}, Outputs.len: {}", + outputs.0, + outputs.1, + outputs.2.len() + ); + assert_eq!(outputs.2.len(), 14); + + // mid range + let ranges = client1.height_range_to_pmmr_indices(5, Some(14))?; + assert_eq!(ranges, (8, 25)); + let outputs = client1.get_outputs_by_pmmr_index(ranges.0, Some(ranges.1), 1000)?; + println!( + "Last Index: {}, Max: {}, Outputs.len: {}", + outputs.0, + outputs.1, + outputs.2.len() + ); + for o in outputs.2.clone() { + println!("height: {}, mmr_index: {}", o.3, o.4); + } + assert_eq!(outputs.2.len(), 10); + + // end + let ranges = client1.height_range_to_pmmr_indices(5, None)?; + assert_eq!(ranges, (8, 38)); + let outputs = client1.get_outputs_by_pmmr_index(ranges.0, Some(ranges.1), 1000)?; + println!( + "Last Index: {}, Max: {}, Outputs.len: {}", + outputs.0, + outputs.1, + outputs.2.len() + ); + for o in outputs.2.clone() { + println!("height: {}, mmr_index: {}", o.3, o.4); + } + assert_eq!(outputs.2.len(), 16); + } + + // let logging finish + stopper.store(false, Ordering::Relaxed); + thread::sleep(Duration::from_millis(200)); + Ok(()) +} + +#[test] +fn scan() { + let test_dir = "test_output/scan"; + setup(test_dir); + if let Err(e) = scan_impl(test_dir) { + panic!("Libwallet Error: {}", e); + } + clean_output_dir(test_dir); +} + +#[test] +fn two_wallets_one_seed() { + let test_dir = "test_output/two_wallets_one_seed"; + setup(test_dir); + if let Err(e) = two_wallets_one_seed_impl(test_dir) { + panic!("Libwallet Error: {}", e); + } + clean_output_dir(test_dir); +} + +#[test] +fn output_scanning() { + let test_dir = "test_output/output_scanning"; + setup(test_dir); + if let Err(e) = output_scanning_impl(test_dir) { + panic!("Libwallet Error: {}", e); + } + clean_output_dir(test_dir); +} diff --git a/controller/tests/common/mod.rs b/controller/tests/common/mod.rs new file mode 100644 index 0000000..114667a --- /dev/null +++ b/controller/tests/common/mod.rs @@ -0,0 +1,178 @@ +// Copyright 2021 The Grin Developers +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! common functions for tests (instantiating wallet and proxy, mostly) +extern crate grin_wallet_controller as wallet; +extern crate grin_wallet_impls as impls; +extern crate grin_wallet_libwallet as libwallet; + +use grin_core as core; +use grin_keychain as keychain; +use grin_util as util; + +use self::core::global; +use self::core::global::ChainTypes; +use self::keychain::ExtKeychain; +use self::libwallet::WalletInst; +use impls::test_framework::{LocalWalletClient, WalletProxy}; +use impls::{DefaultLCProvider, DefaultWalletImpl}; +use std::sync::Arc; +use util::secp::key::SecretKey; +use util::{Mutex, ZeroingString}; + +#[macro_export] +macro_rules! wallet_inst { + ($wallet:ident, $w: ident) => { + let mut w_lock = $wallet.lock(); + let lc = w_lock.lc_provider()?; + let $w = lc.wallet_inst()?; + }; +} + +#[macro_export] +macro_rules! create_wallet_and_add { + ($client:ident, $wallet: ident, $mask: ident, $test_dir: expr, $name: expr, $seed_phrase: expr, $proxy: expr, $create_mask: expr) => { + let $client = LocalWalletClient::new($name, $proxy.tx.clone()); + let ($wallet, $mask) = common::create_local_wallet( + $test_dir, + $name, + $seed_phrase.clone(), + $client.clone(), + $create_mask, + ); + $proxy.add_wallet( + $name, + $client.get_send_instance(), + $wallet.clone(), + $mask.clone(), + ); + }; +} + +#[macro_export] +macro_rules! open_wallet_and_add { + ($client:ident, $wallet: ident, $mask: ident, $test_dir: expr, $name: expr, $proxy: expr, $create_mask: expr) => { + let $client = LocalWalletClient::new($name, $proxy.tx.clone()); + let ($wallet, $mask) = + common::open_local_wallet($test_dir, $name, $client.clone(), $create_mask); + $proxy.add_wallet( + $name, + $client.get_send_instance(), + $wallet.clone(), + $mask.clone(), + ); + }; +} +pub fn clean_output_dir(test_dir: &str) { + let path = std::path::Path::new(test_dir); + if path.is_dir() { + remove_dir_all::remove_dir_all(test_dir).unwrap(); + } +} + +pub fn setup(test_dir: &str) { + util::init_test_logger(); + clean_output_dir(test_dir); + global::set_local_chain_type(ChainTypes::AutomatedTesting); +} + +/// Some tests require the global chain_type to be configured due to threads being spawned internally. +/// It is recommended to avoid relying on this if at all possible as global chain_type +/// leaks across multiple tests and will likely have unintended consequences. +#[allow(dead_code)] +pub fn setup_global_chain_type() { + global::init_global_chain_type(global::ChainTypes::AutomatedTesting); +} + +pub fn create_wallet_proxy( + test_dir: &str, +) -> WalletProxy, LocalWalletClient, ExtKeychain> +{ + WalletProxy::new(test_dir) +} + +pub fn create_local_wallet( + test_dir: &str, + name: &str, + mnemonic: Option, + client: LocalWalletClient, + create_mask: bool, +) -> ( + Arc< + Mutex< + Box< + dyn WalletInst< + 'static, + DefaultLCProvider<'static, LocalWalletClient, ExtKeychain>, + LocalWalletClient, + ExtKeychain, + >, + >, + >, + >, + Option, +) { + let mut wallet = Box::new(DefaultWalletImpl::::new(client).unwrap()) + as Box< + dyn WalletInst< + DefaultLCProvider<'static, LocalWalletClient, ExtKeychain>, + LocalWalletClient, + ExtKeychain, + >, + >; + let lc = wallet.lc_provider().unwrap(); + let _ = lc.set_top_level_directory(&format!("{}/{}", test_dir, name)); + lc.create_wallet(None, mnemonic, 32, ZeroingString::from(""), false) + .unwrap(); + let mask = lc + .open_wallet(None, ZeroingString::from(""), create_mask, false) + .unwrap(); + (Arc::new(Mutex::new(wallet)), mask) +} + +#[allow(dead_code)] +pub fn open_local_wallet( + test_dir: &str, + name: &str, + client: LocalWalletClient, + create_mask: bool, +) -> ( + Arc< + Mutex< + Box< + dyn WalletInst< + 'static, + DefaultLCProvider<'static, LocalWalletClient, ExtKeychain>, + LocalWalletClient, + ExtKeychain, + >, + >, + >, + >, + Option, +) { + let mut wallet = Box::new(DefaultWalletImpl::::new(client).unwrap()) + as Box< + dyn WalletInst< + DefaultLCProvider<'static, LocalWalletClient, ExtKeychain>, + LocalWalletClient, + ExtKeychain, + >, + >; + let lc = wallet.lc_provider().unwrap(); + let _ = lc.set_top_level_directory(&format!("{}/{}", test_dir, name)); + let mask = lc + .open_wallet(None, ZeroingString::from(""), create_mask, false) + .unwrap(); + (Arc::new(Mutex::new(wallet)), mask) +} diff --git a/controller/tests/file.rs b/controller/tests/file.rs new file mode 100644 index 0000000..c6c4e1a --- /dev/null +++ b/controller/tests/file.rs @@ -0,0 +1,320 @@ +// Copyright 2021 The Grin Developers +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Test a wallet file send/recieve +#[macro_use] +extern crate log; +extern crate grin_wallet_controller as wallet; +extern crate grin_wallet_impls as impls; + +use grin_core as core; +use grin_wallet_libwallet as libwallet; + +use impls::test_framework::{self, LocalWalletClient}; +use impls::{PathToSlate, SlateGetter as _, SlatePutter as _}; +use std::sync::atomic::Ordering; +use std::thread; +use std::time::Duration; + +use grin_wallet_libwallet::{InitTxArgs, IssueInvoiceTxArgs, Slate}; + +#[macro_use] +mod common; +use common::{clean_output_dir, create_wallet_proxy, setup}; + +/// self send impl +fn file_exchange_test_impl(test_dir: &'static str, use_bin: bool) -> Result<(), libwallet::Error> { + // Create a new proxy to simulate server and wallet responses + let mut wallet_proxy = create_wallet_proxy(test_dir); + let chain = wallet_proxy.chain.clone(); + let stopper = wallet_proxy.running.clone(); + + // Create a new wallet test client, and set its queues to communicate with the + // proxy + create_wallet_and_add!( + client1, + wallet1, + mask1_i, + test_dir, + "wallet1", + None, + &mut wallet_proxy, + false + ); + let mask1 = (&mask1_i).as_ref(); + create_wallet_and_add!( + client2, + wallet2, + mask2_i, + test_dir, + "wallet2", + None, + &mut wallet_proxy, + false + ); + let mask2 = (&mask2_i).as_ref(); + + // Set the wallet proxy listener running + thread::spawn(move || { + if let Err(e) = wallet_proxy.run() { + error!("Wallet Proxy error: {}", e); + } + }); + + // few values to keep things shorter + let reward = core::consensus::REWARD; + + // add some accounts + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + api.create_account_path(m, "mining")?; + api.create_account_path(m, "listener")?; + Ok(()) + })?; + + // add some accounts + wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| { + api.create_account_path(m, "account1")?; + api.create_account_path(m, "account2")?; + Ok(()) + })?; + + // Get some mining done + { + wallet_inst!(wallet1, w); + w.set_parent_key_id_by_name("mining")?; + } + let mut bh = 10u64; + let _ = + test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, bh as usize, false); + + let (send_file, receive_file, final_file) = match use_bin { + false => ( + format!("{}/standard_S1.tx", test_dir), + format!("{}/standard_S2.tx", test_dir), + format!("{}/standard_S3.tx", test_dir), + ), + true => ( + format!("{}/standard_S1.txbin", test_dir), + format!("{}/standard_S2.txbin", test_dir), + format!("{}/standard_S3.txbin", test_dir), + ), + }; + + // Should have 5 in account1 (5 spendable), 5 in account (2 spendable) + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(wallet1_refreshed); + assert_eq!(wallet1_info.last_confirmed_height, bh); + assert_eq!(wallet1_info.total, bh * reward); + // send to send + let args = InitTxArgs { + src_acct_name: Some("mining".to_owned()), + amount: reward * 2, + minimum_confirmations: 2, + max_outputs: 500, + num_change_outputs: 1, + selection_strategy_is_use_all: true, + ..Default::default() + }; + let slate = api.init_send_tx(m, args)?; + // output tx file + PathToSlate((&send_file).into()).put_tx(&slate, use_bin)?; + api.tx_lock_outputs(m, &slate)?; + Ok(()) + })?; + + // Get some mining done + { + wallet_inst!(wallet2, w); + w.set_parent_key_id_by_name("account1")?; + } + + let mut slate = PathToSlate((&send_file).into()).get_tx()?.0; + + // wallet 2 receives file, completes, sends file back + wallet::controller::foreign_single_use(wallet2.clone(), mask2_i.clone(), |api| { + slate = api.receive_tx(&slate, None, None)?; + PathToSlate((&receive_file).into()).put_tx(&slate, use_bin)?; + Ok(()) + })?; + + // wallet 1 finalises and posts + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let mut slate = PathToSlate(receive_file.into()).get_tx()?.0; + slate = api.finalize_tx(m, &slate)?; + // Output final file for reference + PathToSlate((&final_file).into()).put_tx(&slate, use_bin)?; + api.post_tx(m, &slate, false)?; + bh += 1; + Ok(()) + })?; + + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 3, false); + bh += 3; + + // Check total in mining account + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(wallet1_refreshed); + assert_eq!(wallet1_info.last_confirmed_height, bh); + assert_eq!(wallet1_info.total, bh * reward - reward * 2); + Ok(()) + })?; + + // Check total in 'wallet 2' account + wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| { + let (wallet2_refreshed, wallet2_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(wallet2_refreshed); + assert_eq!(wallet2_info.last_confirmed_height, bh); + assert_eq!(wallet2_info.total, 2 * reward); + Ok(()) + })?; + + // Now other types of exchange, for reference + // Invoice transaction + let (send_file, receive_file, final_file) = match use_bin { + false => ( + format!("{}/invoice_I1.tx", test_dir), + format!("{}/invoice_I2.tx", test_dir), + format!("{}/invoice_I3.tx", test_dir), + ), + true => ( + format!("{}/invoice_I1.txbin", test_dir), + format!("{}/invoice_I2.txbin", test_dir), + format!("{}/invoice_I3.txbin", test_dir), + ), + }; + + let mut slate = Slate::blank(2, true); + + wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| { + let args = IssueInvoiceTxArgs { + amount: 1000000000, + ..Default::default() + }; + slate = api.issue_invoice_tx(m, args)?; + PathToSlate((&send_file).into()).put_tx(&slate, use_bin)?; + Ok(()) + })?; + + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let args = InitTxArgs { + src_acct_name: None, + amount: slate.amount, + minimum_confirmations: 2, + max_outputs: 500, + num_change_outputs: 1, + selection_strategy_is_use_all: true, + ..Default::default() + }; + slate = PathToSlate((&send_file).into()).get_tx()?.0; + slate = api.process_invoice_tx(m, &slate, args)?; + api.tx_lock_outputs(m, &slate)?; + PathToSlate((&receive_file).into()).put_tx(&slate, use_bin)?; + Ok(()) + })?; + wallet::controller::foreign_single_use(wallet2.clone(), mask2_i.clone(), |api| { + // Wallet 2 receives the invoice transaction + slate = PathToSlate((&receive_file).into()).get_tx()?.0; + slate = api.finalize_tx(&slate, false)?; + PathToSlate((&final_file).into()).put_tx(&slate, use_bin)?; + Ok(()) + })?; + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + api.post_tx(m, &slate, false)?; + Ok(()) + })?; + + // Standard, with payment proof + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 3, false); + let (send_file, receive_file, final_file) = match use_bin { + false => ( + format!("{}/standard_pp_S1.tx", test_dir), + format!("{}/standard_pp_S2.tx", test_dir), + format!("{}/standard_pp_S3.tx", test_dir), + ), + true => ( + format!("{}/standard_pp_S1.txbin", test_dir), + format!("{}/standard_pp_S2.txbin", test_dir), + format!("{}/standard_pp_S3.txbin", test_dir), + ), + }; + let mut slate = Slate::blank(2, true); + let mut address = None; + wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| { + address = Some(api.get_slatepack_address(m, 0)?); + Ok(()) + })?; + + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + // send to send + let args = InitTxArgs { + src_acct_name: Some("mining".to_owned()), + amount: reward, + minimum_confirmations: 2, + max_outputs: 500, + num_change_outputs: 1, + selection_strategy_is_use_all: true, + payment_proof_recipient_address: address.clone(), + ..Default::default() + }; + let slate = api.init_send_tx(m, args)?; + PathToSlate((&send_file).into()).put_tx(&slate, use_bin)?; + api.tx_lock_outputs(m, &slate)?; + Ok(()) + })?; + + wallet::controller::foreign_single_use(wallet2.clone(), mask2_i.clone(), |api| { + slate = PathToSlate((&send_file).into()).get_tx()?.0; + slate = api.receive_tx(&slate, None, None)?; + PathToSlate((&receive_file).into()).put_tx(&slate, use_bin)?; + Ok(()) + })?; + + // wallet 1 finalises and posts + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + slate = PathToSlate(receive_file.into()).get_tx()?.0; + slate = api.finalize_tx(m, &slate)?; + // Output final file for reference + PathToSlate((&final_file).into()).put_tx(&slate, use_bin)?; + api.post_tx(m, &slate, false)?; + bh += 1; + Ok(()) + })?; + + // let logging finish + stopper.store(false, Ordering::Relaxed); + thread::sleep(Duration::from_millis(200)); + Ok(()) +} + +#[test] +fn wallet_file_exchange_json() { + let test_dir = "test_output/file_exchange_json"; + setup(test_dir); + // Json output + if let Err(e) = file_exchange_test_impl(test_dir, false) { + panic!("Libwallet Error: {}", e); + } + clean_output_dir(test_dir); +} + +#[test] +fn wallet_file_exchange_bin() { + let test_dir = "test_output/file_exchange_bin"; + setup(test_dir); + if let Err(e) = file_exchange_test_impl(test_dir, true) { + panic!("Libwallet Error: {}", e); + } + clean_output_dir(test_dir); +} diff --git a/controller/tests/invoice.rs b/controller/tests/invoice.rs new file mode 100644 index 0000000..2fe7d64 --- /dev/null +++ b/controller/tests/invoice.rs @@ -0,0 +1,305 @@ +// Copyright 2021 The Grin Developers +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Test a wallet sending to self +#[macro_use] +extern crate log; +extern crate grin_wallet_controller as wallet; +extern crate grin_wallet_impls as impls; + +use grin_core as core; +use grin_wallet_libwallet as libwallet; + +use impls::test_framework::{self, LocalWalletClient}; +use libwallet::{InitTxArgs, IssueInvoiceTxArgs, Slate, SlateState}; +use std::sync::atomic::Ordering; +use std::thread; +use std::time::Duration; + +#[macro_use] +mod common; +use common::{clean_output_dir, create_wallet_proxy, setup}; + +/// self send impl +fn invoice_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { + // Create a new proxy to simulate server and wallet responses + let mut wallet_proxy = create_wallet_proxy(test_dir); + let chain = wallet_proxy.chain.clone(); + let stopper = wallet_proxy.running.clone(); + + create_wallet_and_add!( + client1, + wallet1, + mask1_i, + test_dir, + "wallet1", + None, + &mut wallet_proxy, + true + ); + let mask1 = (&mask1_i).as_ref(); + create_wallet_and_add!( + client2, + wallet2, + mask2_i, + test_dir, + "wallet2", + None, + &mut wallet_proxy, + true + ); + let mask2 = (&mask2_i).as_ref(); + + // Set the wallet proxy listener running + thread::spawn(move || { + if let Err(e) = wallet_proxy.run() { + error!("Wallet Proxy error: {}", e); + } + }); + + // few values to keep things shorter + let reward = core::consensus::REWARD; + + // add some accounts + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + api.create_account_path(m, "mining")?; + api.create_account_path(m, "listener")?; + Ok(()) + })?; + + // Get some mining done + { + wallet_inst!(wallet1, w); + w.set_parent_key_id_by_name("mining")?; + } + let mut _bh = 10u64; + let _ = + test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, _bh as usize, false); + + // Sanity check wallet 1 contents + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(wallet1_refreshed); + assert_eq!(wallet1_info.last_confirmed_height, _bh); + assert_eq!(wallet1_info.total, _bh * reward); + Ok(()) + })?; + + let mut slate = Slate::blank(2, true); + + wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| { + // Wallet 2 inititates an invoice transaction, requesting payment + let args = IssueInvoiceTxArgs { + amount: reward * 2, + ..Default::default() + }; + slate = api.issue_invoice_tx(m, args)?; + Ok(()) + })?; + assert_eq!(slate.state, SlateState::Invoice1); + + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + // Wallet 1 receives the invoice transaction + let args = InitTxArgs { + src_acct_name: None, + amount: slate.amount, + minimum_confirmations: 2, + max_outputs: 500, + num_change_outputs: 1, + selection_strategy_is_use_all: true, + ..Default::default() + }; + slate = api.process_invoice_tx(m, &slate, args)?; + api.tx_lock_outputs(m, &slate)?; + Ok(()) + })?; + assert_eq!(slate.state, SlateState::Invoice2); + + // wallet 2 finalizes and posts + wallet::controller::foreign_single_use(wallet2.clone(), mask2_i.clone(), |api| { + // Wallet 2 receives the invoice transaction + slate = api.finalize_tx(&slate, false)?; + Ok(()) + })?; + assert_eq!(slate.state, SlateState::Invoice3); + + // wallet 1 posts so wallet 2 doesn't get the mined amount + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + api.post_tx(m, &slate, false)?; + Ok(()) + })?; + _bh += 1; + + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 3, false); + _bh += 3; + + // Check transaction log for wallet 2 + wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| { + let (_, wallet2_info) = api.retrieve_summary_info(m, true, 1)?; + let (refreshed, txs) = api.retrieve_txs(m, true, None, None, None)?; + assert!(refreshed); + assert!(txs.len() == 1); + println!( + "last confirmed height: {}, bh: {}", + wallet2_info.last_confirmed_height, _bh + ); + assert!(refreshed); + Ok(()) + })?; + + // Check transaction log for wallet 1, ensure only 1 entry + // exists + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let (_, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + let (refreshed, txs) = api.retrieve_txs(m, true, None, None, None)?; + assert!(refreshed); + assert_eq!(txs.len() as u64, _bh + 1); + println!( + "Wallet 1: last confirmed height: {}, bh: {}", + wallet1_info.last_confirmed_height, _bh + ); + Ok(()) + })?; + + // Test self-sending + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + // Wallet 1 inititates an invoice transaction, requesting payment + let args = IssueInvoiceTxArgs { + amount: reward * 2, + ..Default::default() + }; + slate = api.issue_invoice_tx(m, args)?; + // Wallet 1 receives the invoice transaction + let args = InitTxArgs { + src_acct_name: None, + amount: slate.amount, + minimum_confirmations: 2, + max_outputs: 500, + num_change_outputs: 1, + selection_strategy_is_use_all: true, + ..Default::default() + }; + println!("Self invoice slate init: {}", slate); + slate = api.process_invoice_tx(m, &slate, args)?; + api.tx_lock_outputs(m, &slate)?; + Ok(()) + })?; + + println!("Self invoice slate after process: {}", slate); + + // wallet 1 finalizes and posts + wallet::controller::foreign_single_use(wallet1.clone(), mask1_i.clone(), |api| { + // Wallet 2 receives the invoice transaction + slate = api.finalize_tx(&slate, false)?; + Ok(()) + })?; + + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 3, false); + //bh += 3; + + // As above, but use owner API to finalize + let mut slate = Slate::blank(2, true); + + wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| { + // Wallet 2 inititates an invoice transaction, requesting payment + let args = IssueInvoiceTxArgs { + amount: reward * 2, + ..Default::default() + }; + slate = api.issue_invoice_tx(m, args)?; + Ok(()) + })?; + assert_eq!(slate.state, SlateState::Invoice1); + + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + // Wallet 1 receives the invoice transaction + let args = InitTxArgs { + src_acct_name: None, + amount: slate.amount, + minimum_confirmations: 2, + max_outputs: 500, + num_change_outputs: 1, + selection_strategy_is_use_all: true, + ..Default::default() + }; + slate = api.process_invoice_tx(m, &slate, args)?; + api.tx_lock_outputs(m, &slate)?; + Ok(()) + })?; + assert_eq!(slate.state, SlateState::Invoice2); + + // wallet 2 finalizes via owner API + wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| { + // Wallet 2 receives the invoice transaction + slate = api.finalize_tx(m, &slate)?; + Ok(()) + })?; + assert_eq!(slate.state, SlateState::Invoice3); + + // test that payee can only cancel once + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 3, false); + _bh += 3; + + wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| { + // Wallet 2 inititates an invoice transaction, requesting payment + let args = IssueInvoiceTxArgs { + amount: reward * 2, + ..Default::default() + }; + slate = api.issue_invoice_tx(m, args)?; + Ok(()) + })?; + assert_eq!(slate.state, SlateState::Invoice1); + + let orig_slate = slate.clone(); + + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + // Wallet 1 receives the invoice transaction + let args = InitTxArgs { + src_acct_name: None, + amount: slate.amount, + minimum_confirmations: 2, + max_outputs: 500, + num_change_outputs: 1, + selection_strategy_is_use_all: true, + ..Default::default() + }; + slate = api.process_invoice_tx(m, &slate, args.clone())?; + api.tx_lock_outputs(m, &slate)?; + + // Wallet 1 cancels the invoice transaction + api.cancel_tx(m, None, Some(slate.id))?; + + // Wallet 1 attempts to repay again + let res = api.process_invoice_tx(m, &orig_slate, args); + assert!(res.is_err()); + + Ok(()) + })?; + assert_eq!(slate.state, SlateState::Invoice2); + + // let logging finish + stopper.store(false, Ordering::Relaxed); + thread::sleep(Duration::from_millis(200)); + + Ok(()) +} + +#[test] +fn wallet_invoice_tx() -> Result<(), libwallet::Error> { + let test_dir = "test_output/invoice_tx"; + setup(test_dir); + invoice_tx_impl(test_dir)?; + clean_output_dir(test_dir); + Ok(()) +} diff --git a/controller/tests/late_lock.rs b/controller/tests/late_lock.rs new file mode 100644 index 0000000..b80cc8c --- /dev/null +++ b/controller/tests/late_lock.rs @@ -0,0 +1,158 @@ +// Copyright 2021 The Grin Developers +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Tests and experimentations with late locking +#[macro_use] +extern crate log; +extern crate grin_wallet_controller as wallet; +extern crate grin_wallet_impls as impls; +extern crate grin_wallet_libwallet as libwallet; + +use self::libwallet::{InitTxArgs, Slate}; +use impls::test_framework::{self, LocalWalletClient}; +use std::sync::atomic::Ordering; +use std::thread; +use std::time::Duration; + +#[macro_use] +mod common; +use common::{clean_output_dir, create_wallet_proxy, setup}; + +/// self send impl +fn late_lock_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { + // Create a new proxy to simulate server and wallet responses + let mut wallet_proxy = create_wallet_proxy(test_dir); + let chain = wallet_proxy.chain.clone(); + let stopper = wallet_proxy.running.clone(); + + // Create a new wallet test client, and set its queues to communicate with the + // proxy + create_wallet_and_add!( + client1, + wallet1, + mask1_i, + test_dir, + "wallet1", + None, + &mut wallet_proxy, + false + ); + let mask1 = (&mask1_i).as_ref(); + create_wallet_and_add!( + client2, + wallet2, + mask2_i, + test_dir, + "wallet2", + None, + &mut wallet_proxy, + false + ); + let mask2 = (&mask2_i).as_ref(); + + // Set the wallet proxy listener running + thread::spawn(move || { + if let Err(e) = wallet_proxy.run() { + error!("Wallet Proxy error: {}", e); + } + }); + + // add some accounts + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + api.create_account_path(m, "mining")?; + Ok(()) + })?; + + // add some accounts + wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| { + api.create_account_path(m, "account1")?; + Ok(()) + })?; + + // Get some mining done + { + wallet_inst!(wallet1, w); + w.set_parent_key_id_by_name("mining")?; + } + { + wallet_inst!(wallet2, w); + w.set_parent_key_id_by_name("account1")?; + } + + test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 10, false)?; + + let mut slate = Slate::blank(2, false); + let amount = 100_000_000_000; + + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |sender_api, m| { + let args = InitTxArgs { + src_acct_name: Some("mining".to_owned()), + amount, + minimum_confirmations: 2, + max_outputs: 500, + num_change_outputs: 1, + selection_strategy_is_use_all: false, + late_lock: Some(true), + ..Default::default() + }; + let slate_i = sender_api.init_send_tx(m, args)?; + println!("S1 SLATE: {}", slate_i); + slate = client1.send_tx_slate_direct("wallet2", &slate_i)?; + println!("S2 SLATE: {}", slate); + + // Note we don't call `tx_lock_outputs` on the sender side here, + // as the outputs will only be locked during finalization + + slate = sender_api.finalize_tx(m, &slate)?; + println!("S3 SLATE: {}", slate); + + // Now post tx to our node for inclusion in the next block. + sender_api.post_tx(m, &slate, true)?; + + Ok(()) + })?; + + test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 3, false)?; + + // update/test contents of both accounts + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let (wallet1_refreshed, wallet_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(wallet1_refreshed); + // Reward from mining 11 blocks, minus the amount sent. + // Note: We mined the block containing the tx, so fees are effectively refunded. + assert_eq!(560_000_000_000, wallet_info.amount_currently_spendable); + Ok(()) + })?; + + wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| { + let (wallet2_refreshed, wallet_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(wallet2_refreshed); + assert_eq!(amount, wallet_info.amount_currently_spendable); + Ok(()) + })?; + + // let logging finish + stopper.store(false, Ordering::Relaxed); + thread::sleep(Duration::from_millis(200)); + Ok(()) +} + +#[test] +fn late_lock() { + let test_dir = "test_output/late_lock"; + setup(test_dir); + if let Err(e) = late_lock_test_impl(test_dir) { + panic!("Libwallet Error: {}", e); + } + clean_output_dir(test_dir); +} diff --git a/controller/tests/mwixnet.rs b/controller/tests/mwixnet.rs new file mode 100644 index 0000000..0db537d --- /dev/null +++ b/controller/tests/mwixnet.rs @@ -0,0 +1,190 @@ +// Copyright 2024 The Grin Developers +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Test a wallet sending to self, then creation of comsig request +#[macro_use] +extern crate log; +extern crate grin_wallet_controller as wallet; +extern crate grin_wallet_impls as impls; + +use grin_core as core; +use grin_util as util; +use grin_util::secp::key::SecretKey; + +use grin_wallet_libwallet as libwallet; +use impls::test_framework::{self, LocalWalletClient}; +use libwallet::{mwixnet::MixnetReqCreationParams, InitTxArgs}; +use std::sync::atomic::Ordering; +use std::thread; +use std::time::Duration; + +#[macro_use] +mod common; +use common::{clean_output_dir, create_wallet_proxy, setup}; + +/// self send impl +fn mwixnet_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { + // Create a new proxy to simulate server and wallet responses + let mut wallet_proxy = create_wallet_proxy(test_dir); + let chain = wallet_proxy.chain.clone(); + let stopper = wallet_proxy.running.clone(); + + // Create a new wallet test client, and set its queues to communicate with the + // proxy + create_wallet_and_add!( + client1, + wallet1, + mask1_i, + test_dir, + "wallet1", + None, + &mut wallet_proxy, + true + ); + let mask1 = (&mask1_i).as_ref(); + + // Set the wallet proxy listener running + thread::spawn(move || { + if let Err(e) = wallet_proxy.run() { + error!("Wallet Proxy error: {}", e); + } + }); + + // few values to keep things shorter + let reward = core::consensus::REWARD; + + // add some accounts + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + api.create_account_path(m, "mining")?; + api.create_account_path(m, "listener")?; + Ok(()) + })?; + + // Get some mining done + { + wallet_inst!(wallet1, w); + w.set_parent_key_id_by_name("mining")?; + } + let mut bh = 10u64; + let _ = + test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, bh as usize, false); + + // Should have 5 in account1 (5 spendable), 5 in account (2 spendable) + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(wallet1_refreshed); + assert_eq!(wallet1_info.last_confirmed_height, bh); + assert_eq!(wallet1_info.total, bh * reward); + // send to send + let args = InitTxArgs { + src_acct_name: Some("mining".to_owned()), + amount: reward * 2, + minimum_confirmations: 2, + max_outputs: 500, + num_change_outputs: 1, + selection_strategy_is_use_all: true, + ..Default::default() + }; + let mut slate = api.init_send_tx(m, args)?; + api.tx_lock_outputs(m, &slate)?; + // Send directly to self + wallet::controller::foreign_single_use(wallet1.clone(), mask1_i.clone(), |api| { + slate = api.receive_tx(&slate, Some("listener"), None)?; + Ok(()) + })?; + slate = api.finalize_tx(m, &slate)?; + api.post_tx(m, &slate, false)?; // mines a block + bh += 1; + Ok(()) + })?; + + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 3, false); + bh += 3; + + // Check total in mining account + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(wallet1_refreshed); + assert_eq!(wallet1_info.last_confirmed_height, bh); + assert_eq!(wallet1_info.total, bh * reward - reward * 2); + Ok(()) + })?; + + // Check total in 'listener' account + { + wallet_inst!(wallet1, w); + w.set_parent_key_id_by_name("listener")?; + } + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(wallet1_refreshed); + assert_eq!(wallet1_info.last_confirmed_height, bh); + assert_eq!(wallet1_info.total, 2 * reward); + Ok(()) + })?; + + // Recipient wallet creates a mwixnet request from the last output + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let secp_locked = util::static_secp_instance(); + let secp = secp_locked.lock(); + let server_pubkey_str_1 = + "97444ae673bb92c713c1a2f7b8882ffbfc1c67401a280a775dce1a8651584332"; + let server_pubkey_str_2 = + "0c9414341f2140ed34a5a12a6479bf5a6404820d001ab81d9d3e8cc38f049b4e"; + let server_pubkey_str_3 = + "b58ece97d60e71bb7e53218400b0d67bfe6a3cb7d3b4a67a44f8fb7c525cbca5"; + let server_key_1 = + SecretKey::from_slice(&secp, &grin_util::from_hex(&server_pubkey_str_1).unwrap()) + .unwrap(); + let server_key_2 = + SecretKey::from_slice(&secp, &grin_util::from_hex(&server_pubkey_str_2).unwrap()) + .unwrap(); + let server_key_3 = + SecretKey::from_slice(&secp, &grin_util::from_hex(&server_pubkey_str_3).unwrap()) + .unwrap(); + let params = MixnetReqCreationParams { + server_keys: vec![server_key_1, server_key_2, server_key_3], + fee_per_hop: 50_000_000, + }; + let outputs = api.retrieve_outputs(mask1, false, false, None)?; + // get last output + let last_output = outputs.1[outputs.1.len() - 1].clone(); + + let mwixnet_req = api.create_mwixnet_req(m, ¶ms, &last_output.commit, true)?; + + println!("MWIXNET REQ: {:?}", mwixnet_req); + + // check output we created comsig for is indeed locked + let outputs = api.retrieve_outputs(mask1, false, false, None)?; + // get last output + let last_output = outputs.1[outputs.1.len() - 1].clone(); + assert!(last_output.output.status == libwallet::OutputStatus::Locked); + + Ok(()) + })?; + + // let logging finish + stopper.store(false, Ordering::Relaxed); + thread::sleep(Duration::from_millis(1000)); + Ok(()) +} + +#[test] +fn mwixnet_comsig_test() { + let test_dir = "test_output/mwixnet"; + setup(test_dir); + if let Err(e) = mwixnet_test_impl(test_dir) { + panic!("Libwallet Error: {}", e); + } + clean_output_dir(test_dir); +} diff --git a/controller/tests/no_change.rs b/controller/tests/no_change.rs new file mode 100644 index 0000000..cefba91 --- /dev/null +++ b/controller/tests/no_change.rs @@ -0,0 +1,212 @@ +// Copyright 2021 The Grin Developers +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Test sender transaction with no change output +#[macro_use] +extern crate log; +extern crate grin_wallet_controller as wallet; +extern crate grin_wallet_impls as impls; + +use grin_core as core; + +use grin_wallet_libwallet as libwallet; +use impls::test_framework::{self, LocalWalletClient}; +use libwallet::{InitTxArgs, IssueInvoiceTxArgs, Slate}; +use std::sync::atomic::Ordering; +use std::thread; +use std::time::Duration; + +#[macro_use] +mod common; +use common::{clean_output_dir, create_wallet_proxy, setup}; + +fn no_change_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { + let mut wallet_proxy = create_wallet_proxy(test_dir); + let chain = wallet_proxy.chain.clone(); + let stopper = wallet_proxy.running.clone(); + + create_wallet_and_add!( + client1, + wallet1, + mask1_i, + test_dir, + "wallet1", + None, + &mut wallet_proxy, + false + ); + + let mask1 = (&mask1_i).as_ref(); + + create_wallet_and_add!( + client2, + wallet2, + mask2_i, + test_dir, + "wallet2", + None, + &mut wallet_proxy, + false + ); + + let mask2 = (&mask2_i).as_ref(); + + // Set the wallet proxy listener running + thread::spawn(move || { + if let Err(e) = wallet_proxy.run() { + error!("Wallet Proxy error: {}", e); + } + }); + + // few values to keep things shorter + let reward = core::consensus::REWARD; + + // Mine into wallet 1 + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 4, false); + let fee = core::libtx::tx_fee(1, 1, 1); + + // send a single block's worth of transactions with minimal strategy + let mut slate = Slate::blank(2, false); + let mut stored_excess = None; + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let args = InitTxArgs { + src_acct_name: None, + amount: reward - fee, + minimum_confirmations: 2, + max_outputs: 500, + num_change_outputs: 1, + selection_strategy_is_use_all: false, + ..Default::default() + }; + slate = api.init_send_tx(m, args)?; + slate = client1.send_tx_slate_direct("wallet2", &slate)?; + api.tx_lock_outputs(m, &slate)?; + slate = api.finalize_tx(m, &slate)?; + println!("Posted Slate: {:?}", slate); + stored_excess = Some(slate.tx.as_ref().unwrap().body.kernels[0].excess); + api.post_tx(m, &slate, false)?; + Ok(()) + })?; + + // ensure stored excess is correct in both wallets + // Wallet 1 calculated the excess with the full slate // Wallet 2 only had the excess provided by + // wallet 1 + + // Refresh and check transaction log for wallet 1 + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let (refreshed, txs) = api.retrieve_txs(m, true, None, Some(slate.id), None)?; + assert!(refreshed); + let tx = txs[0].clone(); + println!("SIMPLE SEND - SENDING WALLET"); + println!("{:?}", tx); + println!(); + assert!(tx.confirmed); + assert_eq!(stored_excess, tx.kernel_excess); + Ok(()) + })?; + + // Refresh and check transaction log for wallet 2 + wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| { + let (refreshed, txs) = api.retrieve_txs(m, true, None, Some(slate.id), None)?; + assert!(refreshed); + let tx = txs[0].clone(); + println!("SIMPLE SEND - RECEIVING WALLET"); + println!("{:?}", tx); + println!(); + assert!(tx.confirmed); + assert_eq!(stored_excess, tx.kernel_excess); + Ok(()) + })?; + + // ensure invoice TX works as well with no change + wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| { + // Wallet 2 inititates an invoice transaction, requesting payment + let args = IssueInvoiceTxArgs { + amount: reward - fee, + ..Default::default() + }; + slate = api.issue_invoice_tx(m, args)?; + Ok(()) + })?; + + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + // Wallet 1 receives the invoice transaction + let args = InitTxArgs { + src_acct_name: None, + amount: slate.amount, + minimum_confirmations: 2, + max_outputs: 500, + num_change_outputs: 1, + selection_strategy_is_use_all: false, + ..Default::default() + }; + slate = api.process_invoice_tx(m, &slate, args)?; + api.tx_lock_outputs(m, &slate)?; + Ok(()) + })?; + + // wallet 2 finalizes and posts + wallet::controller::foreign_single_use(wallet2.clone(), mask2_i.clone(), |api| { + // Wallet 2 receives the invoice transaction + slate = api.finalize_tx(&slate, false)?; + Ok(()) + })?; + wallet::controller::owner_single_use(Some(wallet2.clone()), mask1, None, |api, m| { + println!("Invoice Posted TX: {}", slate); + stored_excess = Some(slate.tx.as_ref().unwrap().body.kernels[0].excess); + api.post_tx(m, &slate, false)?; + Ok(()) + })?; + + // check wallet 2's version + wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| { + let (refreshed, txs) = api.retrieve_txs(m, true, None, Some(slate.id), None)?; + assert!(refreshed); + for tx in txs { + stored_excess = tx.kernel_excess; + println!("Wallet 2: {:?}", tx); + println!(); + assert!(tx.confirmed); + assert_eq!(stored_excess, tx.kernel_excess); + } + Ok(()) + })?; + + // Refresh and check transaction log for wallet 1 + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let (refreshed, txs) = api.retrieve_txs(m, true, None, Some(slate.id), None)?; + assert!(refreshed); + for tx in txs { + println!("Wallet 1: {:?}", tx); + println!(); + assert_eq!(stored_excess, tx.kernel_excess); + assert!(tx.confirmed); + } + Ok(()) + })?; + + // let logging finish + stopper.store(false, Ordering::Relaxed); + thread::sleep(Duration::from_millis(200)); + Ok(()) +} + +#[test] +fn no_change() { + let test_dir = "test_output/no_change"; + setup(test_dir); + if let Err(e) = no_change_test_impl(test_dir) { + panic!("Libwallet Error: {}", e); + } + clean_output_dir(test_dir); +} diff --git a/controller/tests/payment_proofs.rs b/controller/tests/payment_proofs.rs new file mode 100644 index 0000000..edb416f --- /dev/null +++ b/controller/tests/payment_proofs.rs @@ -0,0 +1,172 @@ +// Copyright 2021 The Grin Developers +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! tests differing accounts in the same wallet +#[macro_use] +extern crate log; +extern crate grin_wallet_controller as wallet; +extern crate grin_wallet_impls as impls; +extern crate grin_wallet_util; + +use grin_wallet_libwallet as libwallet; +use impls::test_framework::{self, LocalWalletClient}; +use libwallet::{InitTxArgs, Slate}; +use std::sync::atomic::Ordering; +use std::thread; +use std::time::Duration; + +#[macro_use] +mod common; +use common::{clean_output_dir, create_wallet_proxy, setup}; + +/// Various tests on accounts within the same wallet +fn payment_proofs_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { + // Create a new proxy to simulate server and wallet responses + let mut wallet_proxy = create_wallet_proxy(test_dir); + let chain = wallet_proxy.chain.clone(); + let stopper = wallet_proxy.running.clone(); + + create_wallet_and_add!( + client1, + wallet1, + mask1_i, + test_dir, + "wallet1", + None, + &mut wallet_proxy, + false + ); + + let mask1 = (&mask1_i).as_ref(); + + create_wallet_and_add!( + client2, + wallet2, + mask2_i, + test_dir, + "wallet2", + None, + &mut wallet_proxy, + false + ); + + let mask2 = (&mask2_i).as_ref(); + + // Set the wallet proxy listener running + thread::spawn(move || { + if let Err(e) = wallet_proxy.run() { + error!("Wallet Proxy error: {}", e); + } + }); + + // few values to keep things shorter + + // Do some mining + let bh = 10u64; + let _ = + test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, bh as usize, false); + + let mut address = None; + wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| { + address = Some(api.get_slatepack_address(m, 0)?); + Ok(()) + })?; + + println!("Public address is: {:?}", address); + let amount = 60_000_000_000; + let mut slate = Slate::blank(1, false); + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |sender_api, m| { + // note this will increment the block count as part of the transaction "Posting" + let args = InitTxArgs { + src_acct_name: None, + amount: amount, + minimum_confirmations: 2, + max_outputs: 500, + num_change_outputs: 1, + selection_strategy_is_use_all: true, + payment_proof_recipient_address: address.clone(), + ..Default::default() + }; + let slate_i = sender_api.init_send_tx(m, args)?; + + assert_eq!( + slate_i.payment_proof.as_ref().unwrap().receiver_address, + address.as_ref().unwrap().pub_key, + ); + println!( + "Sender addr: {:?}", + slate_i.payment_proof.as_ref().unwrap().sender_address + ); + + // Check we are creating a tx with kernel features 0 + // We will check this produces a Plain kernel later. + assert_eq!(0, slate.kernel_features); + + slate = client1.send_tx_slate_direct("wallet2", &slate_i)?; + sender_api.tx_lock_outputs(m, &slate)?; + + // Ensure what's stored in TX log for payment proof is correct + let (_, txs) = sender_api.retrieve_txs(m, true, None, Some(slate.id), None)?; + assert!(txs[0].payment_proof.is_some()); + let pp = txs[0].clone().payment_proof.unwrap(); + assert_eq!( + pp.receiver_address, + slate_i.payment_proof.as_ref().unwrap().receiver_address + ); + assert!(pp.receiver_signature.is_some()); + assert_eq!(pp.sender_address_path, 0); + assert_eq!(pp.sender_signature, None); + + // check we should get an error at this point since proof is not complete + let pp = sender_api.retrieve_payment_proof(m, true, None, Some(slate.id)); + assert!(pp.is_err()); + + slate = sender_api.finalize_tx(m, &slate)?; + sender_api.post_tx(m, &slate, true)?; + Ok(()) + })?; + + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 2, false); + + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |sender_api, m| { + // Check payment proof here + let mut pp = sender_api.retrieve_payment_proof(m, true, None, Some(slate.id))?; + + println!("Payment proof: {:?}", pp); + + // verify, should be good + let res = sender_api.verify_payment_proof(m, &pp)?; + assert_eq!(res, (true, false)); + + // Modify values, should not be good + pp.amount = 20; + let res = sender_api.verify_payment_proof(m, &pp); + assert!(res.is_err()); + Ok(()) + })?; + + // let logging finish + stopper.store(false, Ordering::Relaxed); + thread::sleep(Duration::from_millis(200)); + Ok(()) +} + +#[test] +fn payment_proofs() { + let test_dir = "test_output/payment_proofs"; + setup(test_dir); + if let Err(e) = payment_proofs_test_impl(test_dir) { + panic!("Libwallet Error: {}", e); + } + clean_output_dir(test_dir); +} diff --git a/controller/tests/repost.rs b/controller/tests/repost.rs new file mode 100644 index 0000000..283164c --- /dev/null +++ b/controller/tests/repost.rs @@ -0,0 +1,268 @@ +// Copyright 2021 The Grin Developers +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Test a wallet repost command +#[macro_use] +extern crate log; +extern crate grin_wallet_controller as wallet; +extern crate grin_wallet_impls as impls; +extern crate grin_wallet_libwallet as libwallet; + +use grin_core as core; + +use self::libwallet::{InitTxArgs, Slate}; +use impls::test_framework::{self, LocalWalletClient}; +use impls::{PathToSlate, SlateGetter as _, SlatePutter as _}; +use std::sync::atomic::Ordering; +use std::thread; +use std::time::Duration; + +#[macro_use] +mod common; +use common::{clean_output_dir, create_wallet_proxy, setup}; + +/// self send impl +fn file_repost_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { + // Create a new proxy to simulate server and wallet responses + let mut wallet_proxy = create_wallet_proxy(test_dir); + let chain = wallet_proxy.chain.clone(); + let stopper = wallet_proxy.running.clone(); + + // Create a new wallet test client, and set its queues to communicate with the + // proxy + create_wallet_and_add!( + client1, + wallet1, + mask1_i, + test_dir, + "wallet1", + None, + &mut wallet_proxy, + false + ); + let mask1 = (&mask1_i).as_ref(); + create_wallet_and_add!( + client2, + wallet2, + mask2_i, + test_dir, + "wallet2", + None, + &mut wallet_proxy, + false + ); + let mask2 = (&mask2_i).as_ref(); + + // Set the wallet proxy listener running + thread::spawn(move || { + if let Err(e) = wallet_proxy.run() { + error!("Wallet Proxy error: {}", e); + } + }); + + // few values to keep things shorter + let reward = core::consensus::REWARD; + + // add some accounts + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + api.create_account_path(m, "mining")?; + api.create_account_path(m, "listener")?; + Ok(()) + })?; + + // add some accounts + wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| { + api.create_account_path(m, "account1")?; + api.create_account_path(m, "account2")?; + Ok(()) + })?; + + // Get some mining done + { + wallet_inst!(wallet1, w); + w.set_parent_key_id_by_name("mining")?; + } + let mut bh = 10u64; + let _ = + test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, bh as usize, false); + + let send_file = format!("{}/part_tx_1.tx", test_dir); + let receive_file = format!("{}/part_tx_2.tx", test_dir); + + let mut slate = Slate::blank(2, false); + + // Should have 5 in account1 (5 spendable), 5 in account (2 spendable) + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(wallet1_refreshed); + assert_eq!(wallet1_info.last_confirmed_height, bh); + assert_eq!(wallet1_info.total, bh * reward); + // send to send + let args = InitTxArgs { + src_acct_name: Some("mining".to_owned()), + amount: reward * 2, + minimum_confirmations: 2, + max_outputs: 500, + num_change_outputs: 1, + selection_strategy_is_use_all: true, + ..Default::default() + }; + let slate = api.init_send_tx(m, args)?; + PathToSlate((&send_file).into()).put_tx(&slate, false)?; + api.tx_lock_outputs(m, &slate)?; + Ok(()) + })?; + + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 3, false); + bh += 3; + + // wallet 1 receives file to different account, completes + { + wallet_inst!(wallet1, w); + w.set_parent_key_id_by_name("listener")?; + } + + wallet::controller::foreign_single_use(wallet1.clone(), mask1_i.clone(), |api| { + slate = PathToSlate((&send_file).into()).get_tx()?.0; + slate = api.receive_tx(&slate, None, None)?; + PathToSlate((&receive_file).into()).put_tx(&slate, false)?; + Ok(()) + })?; + + // wallet 1 receives file to different account, completes + { + wallet_inst!(wallet1, w); + w.set_parent_key_id_by_name("mining")?; + } + + // wallet 1 finalize + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + slate = PathToSlate((&receive_file).into()).get_tx()?.0; + slate = api.finalize_tx(m, &slate)?; + Ok(()) + })?; + + // Now repost from cached + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let (_, txs) = api.retrieve_txs(m, true, None, Some(slate.id), None)?; + println!("TXS[0]: {:?}", txs[0]); + let stored_tx = api.get_stored_tx(m, None, Some(&txs[0].tx_slate_id.unwrap()))?; + println!("Stored tx: {:?}", stored_tx); + api.post_tx(m, &slate, false)?; + bh += 1; + Ok(()) + })?; + + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 3, false); + bh += 3; + + // update/test contents of both accounts + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(wallet1_refreshed); + assert_eq!(wallet1_info.last_confirmed_height, bh); + assert_eq!(wallet1_info.total, bh * reward - reward * 2); + Ok(()) + })?; + + { + wallet_inst!(wallet1, w); + w.set_parent_key_id_by_name("listener")?; + } + + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let (wallet2_refreshed, wallet2_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(wallet2_refreshed); + assert_eq!(wallet2_info.last_confirmed_height, bh); + assert_eq!(wallet2_info.total, 2 * reward); + Ok(()) + })?; + + // as above, but syncronously + { + wallet_inst!(wallet1, w); + w.set_parent_key_id_by_name("mining")?; + } + { + wallet_inst!(wallet2, w); + w.set_parent_key_id_by_name("account1")?; + } + + let mut slate = Slate::blank(2, false); + let amount = 60_000_000_000; + + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |sender_api, m| { + // note this will increment the block count as part of the transaction "Posting" + let args = InitTxArgs { + src_acct_name: None, + amount: reward * 2, + minimum_confirmations: 2, + max_outputs: 500, + num_change_outputs: 1, + selection_strategy_is_use_all: true, + ..Default::default() + }; + let slate_i = sender_api.init_send_tx(m, args)?; + slate = client1.send_tx_slate_direct("wallet2", &slate_i)?; + sender_api.tx_lock_outputs(m, &slate)?; + slate = sender_api.finalize_tx(m, &slate)?; + Ok(()) + })?; + + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 3, false); + bh += 3; + + // Now repost from cached + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let (_, txs) = api.retrieve_txs(m, true, None, Some(slate.id), None)?; + let stored_tx_slate = api.get_stored_tx(m, Some(txs[0].id), None)?.unwrap(); + api.post_tx(m, &stored_tx_slate, false)?; + bh += 1; + Ok(()) + })?; + + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 3, false); + bh += 3; + // + // update/test contents of both accounts + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(wallet1_refreshed); + assert_eq!(wallet1_info.last_confirmed_height, bh); + assert_eq!(wallet1_info.total, bh * reward - reward * 4); + Ok(()) + })?; + + wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| { + let (wallet2_refreshed, wallet2_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(wallet2_refreshed); + assert_eq!(wallet2_info.last_confirmed_height, bh); + assert_eq!(wallet2_info.total, 2 * amount); + Ok(()) + })?; + + // let logging finish + stopper.store(false, Ordering::Relaxed); + thread::sleep(Duration::from_millis(200)); + Ok(()) +} + +#[test] +fn wallet_file_repost() { + let test_dir = "test_output/file_repost"; + setup(test_dir); + if let Err(e) = file_repost_test_impl(test_dir) { + panic!("Libwallet Error: {}", e); + } + clean_output_dir(test_dir); +} diff --git a/controller/tests/revert.rs b/controller/tests/revert.rs new file mode 100644 index 0000000..ea6c0b2 --- /dev/null +++ b/controller/tests/revert.rs @@ -0,0 +1,379 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#[macro_use] +mod common; + +use common::{clean_output_dir, create_wallet_proxy, setup}; +use grin_chain as chain; +use grin_core as core; +use grin_core::core::hash::Hashed; +use grin_core::core::Transaction; +use grin_core::global; +use grin_keychain::ExtKeychain; +use grin_util::secp::key::SecretKey; +use grin_util::Mutex; +use grin_wallet_controller::controller::owner_single_use as owner; +use grin_wallet_impls::test_framework::*; +use grin_wallet_impls::{DefaultLCProvider, PathToSlate, SlatePutter}; +use grin_wallet_libwallet as libwallet; +use grin_wallet_libwallet::api_impl::types::InitTxArgs; +use grin_wallet_libwallet::WalletInst; +use log::error; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::thread; +use std::time::Duration; + +type Wallet = Arc< + Mutex< + Box< + dyn WalletInst< + 'static, + DefaultLCProvider<'static, LocalWalletClient, ExtKeychain>, + LocalWalletClient, + ExtKeychain, + >, + >, + >, +>; + +fn revert( + test_dir: &'static str, +) -> Result< + ( + Arc, + Arc, + u64, + u64, + Transaction, + Wallet, + Option, + Wallet, + Option, + ), + libwallet::Error, +> { + let mut wallet_proxy = create_wallet_proxy(test_dir); + let stopper = wallet_proxy.running.clone(); + let chain = wallet_proxy.chain.clone(); + let test_dir2 = format!("{}/chain2", test_dir); + let wallet_proxy2 = create_wallet_proxy(&test_dir2); + let chain2 = wallet_proxy2.chain.clone(); + let stopper2 = wallet_proxy2.running.clone(); + + create_wallet_and_add!( + client1, + wallet1, + mask1_i, + test_dir, + "wallet1", + None, + &mut wallet_proxy, + false + ); + let mask1 = mask1_i.as_ref(); + + create_wallet_and_add!( + client2, + wallet2, + mask2_i, + test_dir, + "wallet2", + None, + &mut wallet_proxy, + false + ); + let mask2 = mask2_i.as_ref(); + + // Set the wallet proxy listener running + std::thread::spawn(move || { + if let Err(e) = wallet_proxy.run() { + error!("Wallet Proxy error: {}", e); + } + }); + + owner(Some(wallet1.clone()), mask1, None, |api, m| { + api.create_account_path(m, "a")?; + api.set_active_account(m, "a")?; + Ok(()) + })?; + + owner(Some(wallet2.clone()), mask2, None, |api, m| { + api.create_account_path(m, "b")?; + api.set_active_account(m, "b")?; + Ok(()) + })?; + + let reward = core::consensus::REWARD; + let cm = global::coinbase_maturity() as u64; + let sent = reward * 2; + + // Mine some blocks + let bh = 10u64; + award_blocks_to_wallet(&chain, wallet1.clone(), mask1, bh as usize, false)?; + + // Sanity check contents + owner(Some(wallet1.clone()), mask1, None, |api, m| { + let (refreshed, info) = api.retrieve_summary_info(m, true, 1)?; + assert!(refreshed); + assert_eq!(info.last_confirmed_height, bh); + assert_eq!(info.total, bh * reward); + assert_eq!(info.amount_currently_spendable, (bh - cm) * reward); + assert_eq!(info.amount_reverted, 0); + // check tx log as well + let (_, txs) = api.retrieve_txs(m, true, None, None, None)?; + let (c, _) = libwallet::TxLogEntry::sum_confirmed(&txs); + assert_eq!(info.total, c); + assert_eq!(txs.len(), bh as usize); + Ok(()) + })?; + + owner(Some(wallet2.clone()), mask2, None, |api, m| { + let (refreshed, info) = api.retrieve_summary_info(m, true, 1)?; + assert!(refreshed); + assert_eq!(info.last_confirmed_height, bh); + assert_eq!(info.total, 0); + assert_eq!(info.amount_currently_spendable, 0); + assert_eq!(info.amount_reverted, 0); + // check tx log as well + let (_, txs) = api.retrieve_txs(m, true, None, None, None)?; + assert_eq!(txs.len(), 0); + Ok(()) + })?; + + // Send some funds + let mut tx = None; + owner(Some(wallet1.clone()), mask1, None, |api, m| { + // send to send + let args = InitTxArgs { + src_acct_name: None, + amount: sent, + minimum_confirmations: cm, + max_outputs: 500, + num_change_outputs: 1, + selection_strategy_is_use_all: false, + ..Default::default() + }; + let slate = api.init_send_tx(m, args)?; + // output tx file + let send_file = format!("{}/part_tx_1.tx", test_dir); + PathToSlate(send_file.into()).put_tx(&slate, false)?; + api.tx_lock_outputs(m, &slate)?; + let slate = client1.send_tx_slate_direct("wallet2", &slate)?; + let slate = api.finalize_tx(m, &slate)?; + tx = slate.tx; + + Ok(()) + })?; + let tx = tx.expect("tx from slate"); + + // Check funds have been received + owner(Some(wallet2.clone()), mask2, None, |api, m| { + let (refreshed, info) = api.retrieve_summary_info(m, true, 1)?; + assert!(refreshed); + assert_eq!(info.last_confirmed_height, bh); + assert_eq!(info.total, 0); + assert_eq!(info.amount_currently_spendable, 0); + assert_eq!(info.amount_reverted, 0); + // check tx log as well + let (_, txs) = api.retrieve_txs(m, true, None, None, None)?; + assert_eq!(txs.len(), 1); + let tx = &txs[0]; + assert_eq!(tx.tx_type, libwallet::TxLogEntryType::TxReceived); + assert!(!tx.confirmed); + Ok(()) + })?; + + // Update parallel chain + assert_eq!(chain2.head_header().unwrap().height, 0); + for i in 0..bh { + let hash = chain.get_header_by_height(i + 1).unwrap().hash(); + let block = chain.get_block(&hash).unwrap(); + process_block(&chain2, block); + } + assert_eq!(chain2.head_header().unwrap().height, bh); + + // Build 2 blocks at same height: 1 with the tx, 1 without + let head = chain.head_header().unwrap(); + let block_with = + create_block_for_wallet(&chain, head.clone(), &[tx.clone()], wallet1.clone(), mask1)?; + let block_without = create_block_for_wallet(&chain, head, &[], wallet1.clone(), mask1)?; + + // Add block with tx to the chain + process_block(&chain, block_with.clone()); + assert_eq!(chain.head_header().unwrap(), block_with.header); + + // Add block without tx to the parallel chain + process_block(&chain2, block_without.clone()); + assert_eq!(chain2.head_header().unwrap(), block_without.header); + + let bh = bh + 1; + + // Check funds have been confirmed + owner(Some(wallet2.clone()), mask2, None, |api, m| { + let (refreshed, info) = api.retrieve_summary_info(m, true, 1)?; + assert!(refreshed); + assert_eq!(info.last_confirmed_height, bh); + assert_eq!(info.total, sent); + assert_eq!(info.amount_currently_spendable, sent); + assert_eq!(info.amount_reverted, 0); + // check tx log as well + let (_, txs) = api.retrieve_txs(m, true, None, None, None)?; + assert_eq!(txs.len(), 1); + let tx = &txs[0]; + assert_eq!(tx.tx_type, libwallet::TxLogEntryType::TxReceived); + assert!(tx.confirmed); + assert!(tx.kernel_excess.is_some()); + assert!(tx.reverted_after.is_none()); + Ok(()) + })?; + + // Attach more blocks to the parallel chain, making it the longest one + award_block_to_wallet(&chain2, &[], wallet1.clone(), mask1)?; + assert_eq!(chain2.head_header().unwrap().height, bh + 1); + let new_head = chain2 + .get_block(&chain2.head_header().unwrap().hash()) + .unwrap(); + + // Input blocks from parallel chain to original chain, updating it as well + // and effectively reverting the transaction + process_block(&chain, block_without.clone()); // This shouldn't update the head + assert_eq!(chain.head_header().unwrap(), block_with.header); + process_block(&chain, new_head.clone()); // But this should! + assert_eq!(chain.head_header().unwrap(), new_head.header); + + let bh = bh + 1; + + // Check funds have been reverted + owner(Some(wallet2.clone()), mask2, None, |api, m| { + api.scan(m, None, false)?; + let (refreshed, info) = api.retrieve_summary_info(m, true, 1)?; + assert!(refreshed); + assert_eq!(info.last_confirmed_height, bh); + assert_eq!(info.total, 0); + assert_eq!(info.amount_currently_spendable, 0); + assert_eq!(info.amount_reverted, sent); + // check tx log as well + let (_, txs) = api.retrieve_txs(m, true, None, None, None)?; + assert_eq!(txs.len(), 1); + let tx = &txs[0]; + assert_eq!(tx.tx_type, libwallet::TxLogEntryType::TxReverted); + assert!(!tx.confirmed); + assert!(tx.reverted_after.is_some()); + Ok(()) + })?; + + stopper2.store(false, Ordering::Relaxed); + Ok(( + chain, stopper, sent, bh, tx, wallet1, mask1_i, wallet2, mask2_i, + )) +} + +fn revert_reconfirm_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { + let (chain, stopper, sent, bh, tx, wallet1, mask1_i, wallet2, mask2_i) = revert(test_dir)?; + let mask1 = mask1_i.as_ref(); + let mask2 = mask2_i.as_ref(); + + // Include the tx into the chain again, the tx should no longer be reverted + award_block_to_wallet(&chain, &[tx], wallet1.clone(), mask1)?; + + let bh = bh + 1; + + // Check funds have been confirmed again + owner(Some(wallet2.clone()), mask2, None, |api, m| { + let (refreshed, info) = api.retrieve_summary_info(m, true, 1)?; + assert!(refreshed); + assert_eq!(info.last_confirmed_height, bh); + assert_eq!(info.total, sent); + assert_eq!(info.amount_currently_spendable, sent); + assert_eq!(info.amount_reverted, 0); + // check tx log as well + let (_, txs) = api.retrieve_txs(m, true, None, None, None)?; + assert_eq!(txs.len(), 1); + let tx = &txs[0]; + assert_eq!(tx.tx_type, libwallet::TxLogEntryType::TxReceived); + assert!(tx.confirmed); + assert!(tx.reverted_after.is_none()); + Ok(()) + })?; + + // let logging finish + stopper.store(false, Ordering::Relaxed); + thread::sleep(Duration::from_millis(1000)); + Ok(()) +} + +fn revert_cancel_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { + let (_, stopper, sent, bh, _, _, _, wallet2, mask2_i) = revert(test_dir)?; + let mask2 = mask2_i.as_ref(); + + // Cancelling tx + owner(Some(wallet2.clone()), mask2, None, |api, m| { + // Sanity check + let (refreshed, info) = api.retrieve_summary_info(m, true, 1)?; + assert!(refreshed); + assert_eq!(info.last_confirmed_height, bh); + assert_eq!(info.total, 0); + assert_eq!(info.amount_currently_spendable, 0); + assert_eq!(info.amount_reverted, sent); + + let (_, txs) = api.retrieve_txs(m, true, None, None, None)?; + assert_eq!(txs.len(), 1); + let tx = &txs[0]; + + // Cancel + api.cancel_tx(m, Some(tx.id), None)?; + + // Check updated summary info + let (refreshed, info) = api.retrieve_summary_info(m, true, 1)?; + assert!(refreshed); + assert_eq!(info.last_confirmed_height, bh); + assert_eq!(info.total, 0); + assert_eq!(info.amount_currently_spendable, 0); + assert_eq!(info.amount_reverted, 0); + + // Check updated tx log + let (_, txs) = api.retrieve_txs(m, true, None, None, None)?; + assert_eq!(txs.len(), 1); + let tx = &txs[0]; + assert_eq!(tx.tx_type, libwallet::TxLogEntryType::TxReceivedCancelled); + Ok(()) + })?; + + // let logging finish + stopper.store(false, Ordering::Relaxed); + thread::sleep(Duration::from_millis(1000)); + Ok(()) +} + +#[test] +fn tx_revert_reconfirm() { + let test_dir = "test_output/revert_tx"; + setup(test_dir); + if let Err(e) = revert_reconfirm_impl(test_dir) { + panic!("Libwallet Error: {}", e); + } + clean_output_dir(test_dir); +} + +#[test] +fn tx_revert_cancel() { + let test_dir = "test_output/revert_tx_cancel"; + setup(test_dir); + if let Err(e) = revert_cancel_impl(test_dir) { + panic!("Libwallet Error: {}", e); + } + clean_output_dir(test_dir); +} diff --git a/controller/tests/self_send.rs b/controller/tests/self_send.rs new file mode 100644 index 0000000..5fc6058 --- /dev/null +++ b/controller/tests/self_send.rs @@ -0,0 +1,148 @@ +// Copyright 2021 The Grin Developers +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Test a wallet sending to self +#[macro_use] +extern crate log; +extern crate grin_wallet_controller as wallet; +extern crate grin_wallet_impls as impls; + +use grin_core as core; + +use grin_wallet_libwallet as libwallet; +use impls::test_framework::{self, LocalWalletClient}; +use libwallet::InitTxArgs; +use std::sync::atomic::Ordering; +use std::thread; +use std::time::Duration; + +#[macro_use] +mod common; +use common::{clean_output_dir, create_wallet_proxy, setup}; + +/// self send impl +fn self_send_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { + // Create a new proxy to simulate server and wallet responses + let mut wallet_proxy = create_wallet_proxy(test_dir); + let chain = wallet_proxy.chain.clone(); + let stopper = wallet_proxy.running.clone(); + + // Create a new wallet test client, and set its queues to communicate with the + // proxy + create_wallet_and_add!( + client1, + wallet1, + mask1_i, + test_dir, + "wallet1", + None, + &mut wallet_proxy, + true + ); + let mask1 = (&mask1_i).as_ref(); + + // Set the wallet proxy listener running + thread::spawn(move || { + if let Err(e) = wallet_proxy.run() { + error!("Wallet Proxy error: {}", e); + } + }); + + // few values to keep things shorter + let reward = core::consensus::REWARD; + + // add some accounts + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + api.create_account_path(m, "mining")?; + api.create_account_path(m, "listener")?; + Ok(()) + })?; + + // Get some mining done + { + wallet_inst!(wallet1, w); + w.set_parent_key_id_by_name("mining")?; + } + let mut bh = 10u64; + let _ = + test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, bh as usize, false); + + // Should have 5 in account1 (5 spendable), 5 in account (2 spendable) + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(wallet1_refreshed); + assert_eq!(wallet1_info.last_confirmed_height, bh); + assert_eq!(wallet1_info.total, bh * reward); + // send to send + let args = InitTxArgs { + src_acct_name: Some("mining".to_owned()), + amount: reward * 2, + minimum_confirmations: 2, + max_outputs: 500, + num_change_outputs: 1, + selection_strategy_is_use_all: true, + ..Default::default() + }; + let mut slate = api.init_send_tx(m, args)?; + api.tx_lock_outputs(m, &slate)?; + // Send directly to self + wallet::controller::foreign_single_use(wallet1.clone(), mask1_i.clone(), |api| { + slate = api.receive_tx(&slate, Some("listener"), None)?; + Ok(()) + })?; + slate = api.finalize_tx(m, &slate)?; + api.post_tx(m, &slate, false)?; // mines a block + bh += 1; + Ok(()) + })?; + + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 3, false); + bh += 3; + + // Check total in mining account + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(wallet1_refreshed); + assert_eq!(wallet1_info.last_confirmed_height, bh); + assert_eq!(wallet1_info.total, bh * reward - reward * 2); + Ok(()) + })?; + + // Check total in 'listener' account + { + wallet_inst!(wallet1, w); + w.set_parent_key_id_by_name("listener")?; + } + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(wallet1_refreshed); + assert_eq!(wallet1_info.last_confirmed_height, bh); + assert_eq!(wallet1_info.total, 2 * reward); + Ok(()) + })?; + + // let logging finish + stopper.store(false, Ordering::Relaxed); + thread::sleep(Duration::from_millis(1000)); + Ok(()) +} + +#[test] +fn wallet_self_send() { + let test_dir = "test_output/self_send"; + setup(test_dir); + if let Err(e) = self_send_test_impl(test_dir) { + panic!("Libwallet Error: {}", e); + } + clean_output_dir(test_dir); +} diff --git a/controller/tests/slatepack.rs b/controller/tests/slatepack.rs new file mode 100644 index 0000000..d65ec53 --- /dev/null +++ b/controller/tests/slatepack.rs @@ -0,0 +1,587 @@ +// Copyright 2021 The Grin Developers +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Test a wallet file send/recieve +#[macro_use] +extern crate log; +extern crate grin_wallet_controller as wallet; +extern crate grin_wallet_impls as impls; + +use grin_core as core; +use grin_wallet_libwallet as libwallet; + +use impls::test_framework::{self, LocalWalletClient}; +use impls::{PathToSlatepack, SlatePutter as _}; +use std::sync::atomic::Ordering; +use std::thread; +use std::time::Duration; + +use grin_wallet_libwallet::{ + InitTxArgs, IssueInvoiceTxArgs, Slate, Slatepack, SlatepackAddress, Slatepacker, + SlatepackerArgs, +}; + +use ed25519_dalek::PublicKey as edDalekPublicKey; +use ed25519_dalek::SecretKey as edDalekSecretKey; + +#[macro_use] +mod common; +use common::{clean_output_dir, create_wallet_proxy, setup}; + +fn output_slatepack( + slate: &Slate, + file: &str, + armored: bool, + use_bin: bool, + sender: Option, + recipients: Vec, +) -> Result<(), libwallet::Error> { + let packer = Slatepacker::new(SlatepackerArgs { + sender, + recipients, + dec_key: None, + }); + let mut file = file.into(); + if armored { + file = format!("{}.armored", file); + } + PathToSlatepack::new(file.into(), &packer, armored).put_tx(&slate, use_bin) +} + +fn slate_from_packed( + file: &str, + armored: bool, + dec_key: Option<&edDalekSecretKey>, +) -> Result<(Slatepack, Slate), libwallet::Error> { + let packer = Slatepacker::new(SlatepackerArgs { + sender: None, + recipients: vec![], + dec_key, + }); + let mut file = file.into(); + if armored { + file = format!("{}.armored", file); + } + let slatepack = PathToSlatepack::new(file.into(), &packer, armored).get_slatepack(true)?; + Ok((slatepack.clone(), packer.get_slate(&slatepack)?)) +} + +/// self send impl +fn slatepack_exchange_test_impl( + test_dir: &'static str, + use_bin: bool, + use_armored: bool, + use_encryption: bool, +) -> Result<(), libwallet::Error> { + // Create a new proxy to simulate server and wallet responses + let mut wallet_proxy = create_wallet_proxy(test_dir); + let chain = wallet_proxy.chain.clone(); + let stopper = wallet_proxy.running.clone(); + + // Create a new wallet test client, and set its queues to communicate with the + // proxy + create_wallet_and_add!( + client1, + wallet1, + mask1_i, + test_dir, + "wallet1", + None, + &mut wallet_proxy, + false + ); + let mask1 = (&mask1_i).as_ref(); + create_wallet_and_add!( + client2, + wallet2, + mask2_i, + test_dir, + "wallet2", + None, + &mut wallet_proxy, + false + ); + let mask2 = (&mask2_i).as_ref(); + + // Set the wallet proxy listener running + thread::spawn(move || { + if let Err(e) = wallet_proxy.run() { + error!("Wallet Proxy error: {}", e); + } + }); + + // few values to keep things shorter + let reward = core::consensus::REWARD; + + // add some accounts + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + api.create_account_path(m, "mining")?; + api.create_account_path(m, "listener")?; + Ok(()) + })?; + + // add some accounts + wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| { + api.create_account_path(m, "account1")?; + api.create_account_path(m, "account2")?; + Ok(()) + })?; + + // Get some mining done + { + wallet_inst!(wallet1, w); + w.set_parent_key_id_by_name("mining")?; + } + let mut bh = 10u64; + let _ = + test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, bh as usize, false); + + let (recipients_1, dec_key_1, sender_1) = match use_encryption { + true => { + let mut rec_address = SlatepackAddress::random(); + let mut sec_key = edDalekSecretKey::from_bytes(&[0u8; 32]).unwrap(); + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + sec_key = api.get_slatepack_secret_key(m, 0)?; + let pub_key = edDalekPublicKey::from(&sec_key); + rec_address = SlatepackAddress::new(&pub_key); + Ok(()) + })?; + ( + vec![rec_address.clone()], + Some(sec_key), + Some(rec_address.clone()), + ) + } + false => (vec![], None, None), + }; + + let (recipients_2, dec_key_2, sender_2) = match use_encryption { + true => { + let mut rec_address = SlatepackAddress::random(); + let mut sec_key = edDalekSecretKey::from_bytes(&[0u8; 32]).unwrap(); + wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| { + sec_key = api.get_slatepack_secret_key(m, 0)?; + let pub_key = edDalekPublicKey::from(&sec_key); + rec_address = SlatepackAddress::new(&pub_key); + Ok(()) + })?; + ( + vec![rec_address.clone()], + Some(sec_key), + Some(rec_address.clone()), + ) + } + false => (vec![], None, None), + }; + + let (send_file, receive_file, final_file) = match use_bin { + false => ( + format!("{}/standard_S1.slatepack", test_dir), + format!("{}/standard_S2.slatepack", test_dir), + format!("{}/standard_S3.slatepack", test_dir), + ), + true => ( + format!("{}/standard_S1.slatepackbin", test_dir), + format!("{}/standard_S2.slatepackbin", test_dir), + format!("{}/standard_S3.slatepackbin", test_dir), + ), + }; + + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(wallet1_refreshed); + assert_eq!(wallet1_info.last_confirmed_height, bh); + assert_eq!(wallet1_info.total, bh * reward); + // send to send + let args = InitTxArgs { + src_acct_name: Some("mining".to_owned()), + amount: reward * 2, + minimum_confirmations: 2, + max_outputs: 500, + num_change_outputs: 1, + selection_strategy_is_use_all: true, + ..Default::default() + }; + let slate = api.init_send_tx(m, args)?; + // output tx file + output_slatepack( + &slate, + &send_file, + use_armored, + use_bin, + sender_1.clone(), + recipients_2.clone(), + )?; + api.tx_lock_outputs(m, &slate)?; + Ok(()) + })?; + + // Get some mining done + { + wallet_inst!(wallet2, w); + w.set_parent_key_id_by_name("account1")?; + } + + let (mut slatepack, mut slate) = + slate_from_packed(&send_file, use_armored, (&dec_key_2).as_ref())?; + + // wallet 2 receives file, completes, sends file back + wallet::controller::foreign_single_use(wallet2.clone(), mask2_i.clone(), |api| { + slate = api.receive_tx(&slate, None, None)?; + output_slatepack( + &slate, + &receive_file, + use_armored, + use_bin, + // re-encrypt for sender! + sender_2.clone(), + match slatepack.sender.clone() { + Some(s) => vec![s.clone()], + None => vec![], + }, + )?; + Ok(()) + })?; + + // wallet 1 finalises and posts + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let (_, mut slate) = slate_from_packed(&receive_file, use_armored, (&dec_key_1).as_ref())?; + slate = api.finalize_tx(m, &slate)?; + // Output final file for reference + output_slatepack(&slate, &final_file, use_armored, use_bin, None, vec![])?; + api.post_tx(m, &slate, false)?; + bh += 1; + Ok(()) + })?; + + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 3, false); + bh += 3; + + // Check total in mining account + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(wallet1_refreshed); + assert_eq!(wallet1_info.last_confirmed_height, bh); + assert_eq!(wallet1_info.total, bh * reward - reward * 2); + Ok(()) + })?; + + // Check total in 'wallet 2' account + wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| { + let (wallet2_refreshed, wallet2_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(wallet2_refreshed); + assert_eq!(wallet2_info.last_confirmed_height, bh); + assert_eq!(wallet2_info.total, 2 * reward); + Ok(()) + })?; + + // Now other types of exchange, for reference + // Invoice transaction + let (send_file, receive_file, final_file) = match use_bin { + false => ( + format!("{}/invoice_I1.slatepack", test_dir), + format!("{}/invoice_I2.slatepack", test_dir), + format!("{}/invoice_I3.slatepack", test_dir), + ), + true => ( + format!("{}/invoice_I1.slatepackbin", test_dir), + format!("{}/invoice_I2.slatepackbin", test_dir), + format!("{}/invoice_I3.slatepackbin", test_dir), + ), + }; + + let mut slate = Slate::blank(2, true); + + wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| { + let args = IssueInvoiceTxArgs { + amount: 1000000000, + ..Default::default() + }; + slate = api.issue_invoice_tx(m, args)?; + output_slatepack( + &slate, + &send_file, + use_armored, + use_bin, + sender_2.clone(), + recipients_1.clone(), + )?; + Ok(()) + })?; + + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let args = InitTxArgs { + src_acct_name: None, + amount: slate.amount, + minimum_confirmations: 2, + max_outputs: 500, + num_change_outputs: 1, + selection_strategy_is_use_all: true, + ..Default::default() + }; + let res = slate_from_packed(&send_file, use_armored, (&dec_key_1).as_ref())?; + slatepack = res.0; + slate = res.1; + slate = api.process_invoice_tx(m, &slate, args)?; + api.tx_lock_outputs(m, &slate)?; + output_slatepack( + &slate, + &receive_file, + use_armored, + use_bin, + sender_1.clone(), + match slatepack.sender.clone() { + Some(s) => vec![s.clone()], + None => vec![], + }, + )?; + Ok(()) + })?; + wallet::controller::foreign_single_use(wallet2.clone(), mask2_i.clone(), |api| { + // Wallet 2 receives the invoice transaction + let res = slate_from_packed(&receive_file, use_armored, (&dec_key_2).as_ref())?; + slate = res.1; + slate = api.finalize_tx(&slate, false)?; + output_slatepack(&slate, &final_file, use_armored, use_bin, None, vec![])?; + Ok(()) + })?; + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + api.post_tx(m, &slate, false)?; + Ok(()) + })?; + + // Standard, with payment proof + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 3, false); + let (send_file, receive_file, final_file) = match use_bin { + false => ( + format!("{}/standard_pp_S1.slatepack", test_dir), + format!("{}/standard_pp_S2.slatepack", test_dir), + format!("{}/standard_pp_S3.slatepack", test_dir), + ), + true => ( + format!("{}/standard_pp_S1.slatepackbin", test_dir), + format!("{}/standard_pp_S2.slatepackbin", test_dir), + format!("{}/standard_pp_S3.slatepackbin", test_dir), + ), + }; + + let mut slate = Slate::blank(2, true); + let mut address = None; + wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| { + address = Some(api.get_slatepack_address(m, 0)?); + Ok(()) + })?; + + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + // send to send + let args = InitTxArgs { + src_acct_name: Some("mining".to_owned()), + amount: reward, + minimum_confirmations: 2, + max_outputs: 500, + num_change_outputs: 1, + selection_strategy_is_use_all: true, + payment_proof_recipient_address: address.clone(), + ..Default::default() + }; + let slate = api.init_send_tx(m, args)?; + output_slatepack( + &slate, + &send_file, + use_armored, + use_bin, + sender_1, + recipients_2.clone(), + )?; + api.tx_lock_outputs(m, &slate)?; + Ok(()) + })?; + + wallet::controller::foreign_single_use(wallet2.clone(), mask2_i.clone(), |api| { + let res = slate_from_packed(&send_file, use_armored, (&dec_key_2).as_ref())?; + let slatepack = res.0; + slate = res.1; + slate = api.receive_tx(&slate, None, None)?; + output_slatepack( + &slate, + &receive_file, + use_armored, + use_bin, + sender_2, + match slatepack.sender { + Some(s) => vec![s.clone()], + None => vec![], + }, + )?; + Ok(()) + })?; + + // wallet 1 finalises and posts + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let res = slate_from_packed(&receive_file, use_armored, (&dec_key_1).as_ref())?; + slate = res.1; + slate = api.finalize_tx(m, &slate)?; + // Output final file for reference + output_slatepack(&slate, &final_file, use_armored, use_bin, None, vec![])?; + api.post_tx(m, &slate, false)?; + bh += 1; + Ok(()) + })?; + + // let logging finish + stopper.store(false, Ordering::Relaxed); + thread::sleep(Duration::from_millis(200)); + Ok(()) +} + +/// Exercise slate encryption/decryption via the API, +/// Since doctests don't cover encryption +fn slatepack_api_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { + // Create a new proxy to simulate server and wallet responses + let mut wallet_proxy = create_wallet_proxy(test_dir); + let chain = wallet_proxy.chain.clone(); + let stopper = wallet_proxy.running.clone(); + + // Create a new wallet test client, and set its queues to communicate with the + // proxy + create_wallet_and_add!( + client1, + wallet1, + mask1_i, + test_dir, + "wallet1", + None, + &mut wallet_proxy, + false + ); + let mask1 = (&mask1_i).as_ref(); + + // Set the wallet proxy listener running + thread::spawn(move || { + if let Err(e) = wallet_proxy.run() { + error!("Wallet Proxy error: {}", e); + } + }); + + // few values to keep things shorter + let reward = core::consensus::REWARD; + + // Get some mining done + let bh = 6u64; + let _ = + test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, bh as usize, false); + + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let args = InitTxArgs { + src_acct_name: Some("mining".to_owned()), + amount: reward * 2, + minimum_confirmations: 2, + max_outputs: 500, + num_change_outputs: 1, + selection_strategy_is_use_all: true, + ..Default::default() + }; + let slate = api.init_send_tx(m, args)?; + // create an encrypted slatepack (just encrypted for self) + let enc_addr = api.get_slatepack_address(m, 0)?; + let slatepack = api.create_slatepack_message(m, &slate, Some(0), vec![enc_addr])?; + println!("{}", slatepack); + let slatepack_raw = api.decode_slatepack_message(m, slatepack.clone(), vec![0])?; + println!("{}", slatepack_raw); + let decoded_slate = api.slate_from_slatepack_message(m, slatepack, vec![0])?; + println!("{}", decoded_slate); + Ok(()) + })?; + + // let logging finish + stopper.store(false, Ordering::Relaxed); + thread::sleep(Duration::from_millis(200)); + Ok(()) +} + +#[test] +fn slatepack_exchange_json() { + let test_dir = "test_output/slatepack_exchange_json"; + setup(test_dir); + // Json output + if let Err(e) = slatepack_exchange_test_impl(test_dir, false, false, false) { + panic!("Libwallet Error: {}", e); + } + clean_output_dir(test_dir); +} + +#[test] +fn slatepack_exchange_bin() { + let test_dir = "test_output/slatepack_exchange_bin"; + setup(test_dir); + // Bin output + if let Err(e) = slatepack_exchange_test_impl(test_dir, true, false, false) { + panic!("Libwallet Error: {}", e); + } + clean_output_dir(test_dir); +} + +#[test] +fn slatepack_exchange_armored() { + let test_dir = "test_output/slatepack_exchange_armored"; + setup(test_dir); + // Bin output + if let Err(e) = slatepack_exchange_test_impl(test_dir, true, true, true) { + panic!("Libwallet Error: {}", e); + } + clean_output_dir(test_dir); +} + +#[test] +fn slatepack_exchange_json_enc() { + let test_dir = "test_output/slatepack_exchange_json_enc"; + setup(test_dir); + // Json output + if let Err(e) = slatepack_exchange_test_impl(test_dir, false, false, true) { + panic!("Libwallet Error: {}", e); + } + clean_output_dir(test_dir); +} + +#[test] +fn slatepack_exchange_bin_enc() { + let test_dir = "test_output/slatepack_exchange_bin_enc"; + setup(test_dir); + // Bin output + if let Err(e) = slatepack_exchange_test_impl(test_dir, true, false, true) { + panic!("Libwallet Error: {}", e); + } + clean_output_dir(test_dir); +} + +#[test] +fn slatepack_exchange_armored_enc() { + let test_dir = "test_output/slatepack_exchange_armored_enc"; + setup(test_dir); + // Bin output + if let Err(e) = slatepack_exchange_test_impl(test_dir, true, true, true) { + panic!("Libwallet Error: {}", e); + } + clean_output_dir(test_dir); +} + +#[test] +fn slatepack_api() { + let test_dir = "test_output/slatepack_api"; + setup(test_dir); + // Json output + if let Err(e) = slatepack_api_impl(test_dir) { + panic!("Libwallet Error: {}", e); + } + clean_output_dir(test_dir); +} diff --git a/controller/tests/transaction.rs b/controller/tests/transaction.rs new file mode 100644 index 0000000..33db4c9 --- /dev/null +++ b/controller/tests/transaction.rs @@ -0,0 +1,601 @@ +// Copyright 2021 The Grin Developers +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! tests for transactions building within core::libtx +#[macro_use] +extern crate log; +extern crate grin_wallet_controller as wallet; +extern crate grin_wallet_impls as impls; +extern crate grin_wallet_libwallet as libwallet; + +use grin_core as core; + +use self::core::core::transaction; +use self::core::global; +use self::libwallet::{InitTxArgs, OutputStatus, Slate, SlateState}; +use impls::test_framework::{self, LocalWalletClient}; +use std::convert::TryInto; +use std::sync::atomic::Ordering; +use std::thread; +use std::time::Duration; + +mod common; +use common::{clean_output_dir, create_wallet_proxy, setup}; + +/// Exercises the Transaction API fully with a test NodeClient operating +/// directly on a chain instance +/// Callable with any type of wallet +fn basic_transaction_api(test_dir: &'static str) -> Result<(), libwallet::Error> { + // Create a new proxy to simulate server and wallet responses + let mut wallet_proxy = create_wallet_proxy(test_dir); + let chain = wallet_proxy.chain.clone(); + let stopper = wallet_proxy.running.clone(); + + create_wallet_and_add!( + client1, + wallet1, + mask1_i, + test_dir, + "wallet1", + None, + &mut wallet_proxy, + true + ); + let mask1 = (&mask1_i).as_ref(); + println!("Mask1: {:?}", mask1); + create_wallet_and_add!( + client2, + wallet2, + mask2_i, + test_dir, + "wallet2", + None, + &mut wallet_proxy, + false + ); + let mask2 = (&mask2_i).as_ref(); + println!("Mask2: {:?}", mask2); + + // Set the wallet proxy listener running + thread::spawn(move || { + if let Err(e) = wallet_proxy.run() { + error!("Wallet Proxy error: {}", e); + } + }); + + // few values to keep things shorter + let reward = core::consensus::REWARD; + let cm = global::coinbase_maturity(); + // mine a few blocks + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 10, false); + + // Check wallet 1 contents are as expected + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + debug!( + "Wallet 1 Info Pre-Transaction, after {} blocks: {:?}", + wallet1_info.last_confirmed_height, wallet1_info + ); + assert!(wallet1_refreshed); + assert_eq!( + wallet1_info.amount_currently_spendable, + (wallet1_info.last_confirmed_height - cm) * reward + ); + assert_eq!(wallet1_info.amount_immature, cm * reward); + Ok(()) + })?; + + // assert wallet contents + // and a single use api for a send command + let amount = 60_000_000_000; + let mut slate = Slate::blank(1, false); + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |sender_api, m| { + // note this will increment the block count as part of the transaction "Posting" + let args = InitTxArgs { + src_acct_name: None, + amount: amount, + minimum_confirmations: 2, + max_outputs: 500, + num_change_outputs: 1, + selection_strategy_is_use_all: true, + ..Default::default() + }; + let slate_i = sender_api.init_send_tx(m, args)?; + + assert_eq!(slate_i.state, SlateState::Standard1); + + // Check we are creating a tx with the expected lock_height of 0. + // We will check this produces a Plain kernel later. + assert_eq!(0, slate.kernel_features); + + slate = client1.send_tx_slate_direct("wallet2", &slate_i)?; + assert_eq!(slate.state, SlateState::Standard2); + sender_api.tx_lock_outputs(m, &slate)?; + slate = sender_api.finalize_tx(m, &slate)?; + assert_eq!(slate.state, SlateState::Standard3); + + // Check we have a single kernel and that it is a Plain kernel (no lock_height). + // fees for 7 inputs, 2 outputs, 1 kernel (weight 52) + assert_eq!(slate.tx_or_err()?.kernels().len(), 1); + assert_eq!( + slate + .tx_or_err()? + .kernels() + .first() + .map(|k| k.features) + .unwrap(), + transaction::KernelFeatures::Plain { + fee: 26_000_000.into() + } + ); + + Ok(()) + })?; + + // Check transaction log for wallet 1 + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let (_, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + let (refreshed, txs) = api.retrieve_txs(m, true, None, None, None)?; + assert!(refreshed); + let fee = core::libtx::tx_fee( + wallet1_info.last_confirmed_height as usize - cm as usize, + 2, + 1, + ); + // we should have a transaction entry for this slate + let tx = txs.iter().find(|t| t.tx_slate_id == Some(slate.id)); + assert!(tx.is_some()); + let tx = tx.unwrap(); + assert!(!tx.confirmed); + assert!(tx.confirmation_ts.is_none()); + assert_eq!(tx.amount_debited - tx.amount_credited, fee + amount); + println!("tx: {:?}", tx); + assert_eq!(Some(fee.try_into().unwrap()), tx.fee); + Ok(()) + })?; + + // Check transaction log for wallet 2 + wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| { + let (refreshed, txs) = api.retrieve_txs(m, true, None, None, None)?; + assert!(refreshed); + // we should have a transaction entry for this slate + let tx = txs.iter().find(|t| t.tx_slate_id == Some(slate.id)); + assert!(tx.is_some()); + let tx = tx.unwrap(); + assert!(!tx.confirmed); + assert!(tx.confirmation_ts.is_none()); + assert_eq!(amount, tx.amount_credited); + assert_eq!(0, tx.amount_debited); + assert_eq!(None, tx.fee); + Ok(()) + })?; + + // post transaction + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + api.post_tx(m, &slate, false)?; + Ok(()) + })?; + + // Check wallet 1 contents are as expected + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + debug!( + "Wallet 1 Info Post Transaction, after {} blocks: {:?}", + wallet1_info.last_confirmed_height, wallet1_info + ); + let fee = core::libtx::tx_fee( + wallet1_info.last_confirmed_height as usize - 1 - cm as usize, + 2, + 1, + ); + assert!(wallet1_refreshed); + // wallet 1 received fees, so amount should be the same + assert_eq!( + wallet1_info.total, + amount * wallet1_info.last_confirmed_height - amount + ); + assert_eq!( + wallet1_info.amount_currently_spendable, + (wallet1_info.last_confirmed_height - cm) * reward - amount - fee + ); + assert_eq!(wallet1_info.amount_immature, cm * reward + fee); + + // check tx log entry is confirmed + let (refreshed, txs) = api.retrieve_txs(m, true, None, None, None)?; + assert!(refreshed); + let tx = txs.iter().find(|t| t.tx_slate_id == Some(slate.id)); + assert!(tx.is_some()); + let tx = tx.unwrap(); + assert!(tx.confirmed); + assert!(tx.confirmation_ts.is_some()); + + Ok(()) + })?; + + // mine a few more blocks + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 3, false); + + // refresh wallets and retrieve info/tests for each wallet after maturity + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + debug!("Wallet 1 Info: {:?}", wallet1_info); + assert!(wallet1_refreshed); + assert_eq!( + wallet1_info.total, + amount * wallet1_info.last_confirmed_height - amount + ); + assert_eq!( + wallet1_info.amount_currently_spendable, + (wallet1_info.last_confirmed_height - cm - 1) * reward + ); + Ok(()) + })?; + + wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| { + let (wallet2_refreshed, wallet2_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(wallet2_refreshed); + assert_eq!(wallet2_info.amount_currently_spendable, amount); + + // check tx log entry is confirmed + let (refreshed, txs) = api.retrieve_txs(m, true, None, None, None)?; + assert!(refreshed); + let tx = txs.iter().find(|t| t.tx_slate_id == Some(slate.id)); + assert!(tx.is_some()); + let tx = tx.unwrap(); + assert!(tx.confirmed); + assert!(tx.confirmation_ts.is_some()); + Ok(()) + })?; + + // Estimate fee and locked amount for a transaction + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |sender_api, m| { + let init_args = InitTxArgs { + src_acct_name: None, + amount: amount * 2, + minimum_confirmations: 2, + max_outputs: 500, + num_change_outputs: 1, + selection_strategy_is_use_all: true, + estimate_only: Some(true), + ..Default::default() + }; + let est = sender_api.init_send_tx(m, init_args)?; + assert_eq!(est.amount, 600_000_000_000); + // fees for 5 inputs, 2 outputs, 1 kernel (weight 50) + assert_eq!(est.fee_fields.fee(), 25_000_000); + + let init_args = InitTxArgs { + src_acct_name: None, + amount: amount * 2, + minimum_confirmations: 2, + max_outputs: 500, + num_change_outputs: 1, + selection_strategy_is_use_all: false, //select smallest number + estimate_only: Some(true), + ..Default::default() + }; + let est = sender_api.init_send_tx(m, init_args)?; + assert_eq!(est.amount, 180_000_000_000); + // fees for 3 inputs, 2 outputs, 1 kernel (weight 48) + assert_eq!(est.fee_fields.fee(), 24_000_000); + + Ok(()) + })?; + + // Send another transaction, but don't post to chain immediately and use + // the stored transaction instead + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |sender_api, m| { + // note this will increment the block count as part of the transaction "Posting" + let args = InitTxArgs { + src_acct_name: None, + amount: amount * 2, + minimum_confirmations: 2, + max_outputs: 500, + num_change_outputs: 1, + selection_strategy_is_use_all: true, + ..Default::default() + }; + let slate_i = sender_api.init_send_tx(m, args)?; + slate = client1.send_tx_slate_direct("wallet2", &slate_i)?; + sender_api.tx_lock_outputs(m, &slate)?; + slate = sender_api.finalize_tx(m, &slate)?; + Ok(()) + })?; + + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |sender_api, m| { + let (refreshed, _wallet1_info) = sender_api.retrieve_summary_info(m, true, 1)?; + assert!(refreshed); + let (_, txs) = sender_api.retrieve_txs(m, true, None, None, None)?; + // find the transaction + let tx = txs + .iter() + .find(|t| t.tx_slate_id == Some(slate.id)) + .unwrap(); + let stored_tx_slate = sender_api + .get_stored_tx(m, None, Some(&tx.tx_slate_id.unwrap()))? + .unwrap(); + sender_api.post_tx(m, &stored_tx_slate, false)?; + let (_, wallet1_info) = sender_api.retrieve_summary_info(m, true, 1)?; + // should be mined now + assert_eq!( + wallet1_info.total, + amount * wallet1_info.last_confirmed_height - amount * 3 + ); + Ok(()) + })?; + + // mine a few more blocks + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 3, false); + + // check wallet2 has stored transaction + wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| { + let (wallet2_refreshed, wallet2_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(wallet2_refreshed); + assert_eq!(wallet2_info.amount_currently_spendable, amount * 3); + + // check tx log entry is confirmed + let (refreshed, txs) = api.retrieve_txs(m, true, None, None, None)?; + assert!(refreshed); + let tx = txs.iter().find(|t| t.tx_slate_id == Some(slate.id)); + assert!(tx.is_some()); + let tx = tx.unwrap(); + assert!(tx.confirmed); + assert!(tx.confirmation_ts.is_some()); + Ok(()) + })?; + + // try to send a transaction with amount inclusive of fees, but amount too + // small to cover fees. Should fail. + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |sender_api, m| { + // note this will increment the block count as part of the transaction "Posting" + let args = InitTxArgs { + src_acct_name: None, + amount: 1, + amount_includes_fee: Some(true), + minimum_confirmations: 2, + max_outputs: 500, + num_change_outputs: 1, + selection_strategy_is_use_all: true, + ..Default::default() + }; + let res = sender_api.init_send_tx(m, args); + assert!(res.is_err()); + Ok(()) + })?; + + // try to build a transaction with amount inclusive of fees. Confirm that tx + // amount + fee is equal to the originally specified amount + let amount = 60_000_000_000; + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |sender_api, m| { + // note this will increment the block count as part of the transaction "Posting" + let args = InitTxArgs { + src_acct_name: None, + amount: amount, + amount_includes_fee: Some(true), + minimum_confirmations: 2, + max_outputs: 500, + num_change_outputs: 1, + selection_strategy_is_use_all: true, + ..Default::default() + }; + let slate_i = sender_api.init_send_tx(m, args)?; + assert_eq!(slate_i.state, SlateState::Standard1); + let total_spend: u64 = slate_i.amount + slate_i.fee_fields.fee(); + assert_eq!(amount, total_spend); + Ok(()) + })?; + + // let logging finish + stopper.store(false, Ordering::Relaxed); + thread::sleep(Duration::from_millis(200)); + Ok(()) +} + +/// Test rolling back transactions and outputs when a transaction is never +/// posted to a chain +fn tx_rollback(test_dir: &'static str) -> Result<(), libwallet::Error> { + // Create a new proxy to simulate server and wallet responses + let mut wallet_proxy = create_wallet_proxy(test_dir); + let chain = wallet_proxy.chain.clone(); + let stopper = wallet_proxy.running.clone(); + + create_wallet_and_add!( + client1, + wallet1, + mask1_i, + test_dir, + "wallet1", + None, + &mut wallet_proxy, + false + ); + let mask1 = (&mask1_i).as_ref(); + create_wallet_and_add!( + client2, + wallet2, + mask2_i, + test_dir, + "wallet2", + None, + &mut wallet_proxy, + false + ); + let mask2 = (&mask2_i).as_ref(); + + // Set the wallet proxy listener running + thread::spawn(move || { + if let Err(e) = wallet_proxy.run() { + error!("Wallet Proxy error: {}", e); + } + }); + + // few values to keep things shorter + let reward = core::consensus::REWARD; + let cm = global::coinbase_maturity(); // assume all testing precedes soft fork height + // mine a few blocks + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 5, false); + + let amount = 30_000_000_000; + let mut slate = Slate::blank(1, false); + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |sender_api, m| { + // note this will increment the block count as part of the transaction "Posting" + let args = InitTxArgs { + src_acct_name: None, + amount: amount, + minimum_confirmations: 2, + max_outputs: 500, + num_change_outputs: 1, + selection_strategy_is_use_all: true, + ..Default::default() + }; + + let slate_i = sender_api.init_send_tx(m, args)?; + slate = client1.send_tx_slate_direct("wallet2", &slate_i)?; + sender_api.tx_lock_outputs(m, &slate)?; + slate = sender_api.finalize_tx(m, &slate)?; + Ok(()) + })?; + + // Check transaction log for wallet 1 + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let (refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + println!( + "last confirmed height: {}", + wallet1_info.last_confirmed_height + ); + assert!(refreshed); + let (_, txs) = api.retrieve_txs(m, true, None, None, None)?; + // we should have a transaction entry for this slate + let tx = txs.iter().find(|t| t.tx_slate_id == Some(slate.id)); + assert!(tx.is_some()); + let mut locked_count = 0; + let mut unconfirmed_count = 0; + // get the tx entry, check outputs are as expected + let (_, output_mappings) = api.retrieve_outputs(m, true, false, Some(tx.unwrap().id))?; + for m in output_mappings.clone() { + if m.output.status == OutputStatus::Locked { + locked_count = locked_count + 1; + } + if m.output.status == OutputStatus::Unconfirmed { + unconfirmed_count = unconfirmed_count + 1; + } + } + assert_eq!(output_mappings.len(), 3); + assert_eq!(locked_count, 2); + assert_eq!(unconfirmed_count, 1); + + Ok(()) + })?; + + // Check transaction log for wallet 2 + wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| { + let (refreshed, txs) = api.retrieve_txs(m, true, None, None, None)?; + assert!(refreshed); + let mut unconfirmed_count = 0; + let tx = txs.iter().find(|t| t.tx_slate_id == Some(slate.id)); + assert!(tx.is_some()); + // get the tx entry, check outputs are as expected + let (_, outputs) = api.retrieve_outputs(m, true, false, Some(tx.unwrap().id))?; + for m in outputs.clone() { + if m.output.status == OutputStatus::Unconfirmed { + unconfirmed_count = unconfirmed_count + 1; + } + } + assert_eq!(outputs.len(), 1); + assert_eq!(unconfirmed_count, 1); + let (refreshed, wallet2_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(refreshed); + assert_eq!(wallet2_info.amount_currently_spendable, 0,); + assert_eq!(wallet2_info.amount_awaiting_finalization, amount); + Ok(()) + })?; + + // wallet 1 is bold and doesn't ever post the transaction + // mine a few more blocks + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 5, false); + + // Wallet 1 decides to roll back instead + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + // can't roll back coinbase + let res = api.cancel_tx(m, Some(1), None); + assert!(res.is_err()); + let (_, txs) = api.retrieve_txs(m, true, None, None, None)?; + let tx = txs + .iter() + .find(|t| t.tx_slate_id == Some(slate.id)) + .unwrap(); + api.cancel_tx(m, Some(tx.id), None)?; + let (refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(refreshed); + println!( + "last confirmed height: {}", + wallet1_info.last_confirmed_height + ); + // check all eligible inputs should be now be spendable + println!("cm: {}", cm); + assert_eq!( + wallet1_info.amount_currently_spendable, + (wallet1_info.last_confirmed_height - cm) * reward + ); + // can't roll back again + let res = api.cancel_tx(m, Some(tx.id), None); + assert!(res.is_err()); + + Ok(()) + })?; + + // Wallet 2 rolls back + wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| { + let (_, txs) = api.retrieve_txs(m, true, None, None, None)?; + let tx = txs + .iter() + .find(|t| t.tx_slate_id == Some(slate.id)) + .unwrap(); + api.cancel_tx(m, Some(tx.id), None)?; + let (refreshed, wallet2_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(refreshed); + // check all eligible inputs should be now be spendable + assert_eq!(wallet2_info.amount_currently_spendable, 0,); + assert_eq!(wallet2_info.total, 0,); + // can't roll back again + let res = api.cancel_tx(m, Some(tx.id), None); + assert!(res.is_err()); + + Ok(()) + })?; + + // let logging finish + stopper.store(false, Ordering::Relaxed); + thread::sleep(Duration::from_millis(200)); + Ok(()) +} + +#[test] +fn db_wallet_basic_transaction_api() { + let test_dir = "test_output/basic_transaction_api"; + setup(test_dir); + if let Err(e) = basic_transaction_api(test_dir) { + panic!("Libwallet Error: {}", e); + } + clean_output_dir(test_dir); +} + +#[test] +fn db_wallet_tx_rollback() { + let test_dir = "test_output/tx_rollback"; + setup(test_dir); + if let Err(e) = tx_rollback(test_dir) { + panic!("Libwallet Error: {}", e); + } + clean_output_dir(test_dir); +} diff --git a/controller/tests/ttl_cutoff.rs b/controller/tests/ttl_cutoff.rs new file mode 100644 index 0000000..ec4340b --- /dev/null +++ b/controller/tests/ttl_cutoff.rs @@ -0,0 +1,185 @@ +// Copyright 2021 The Grin Developers +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! tests ttl_cutoff blocks +#[macro_use] +extern crate log; +extern crate grin_wallet_controller as wallet; +extern crate grin_wallet_impls as impls; +extern crate grin_wallet_util; + +use grin_wallet_libwallet as libwallet; +use impls::test_framework::{self, LocalWalletClient}; +use libwallet::{InitTxArgs, Slate, TxLogEntryType}; +use std::sync::atomic::Ordering; +use std::thread; +use std::time::Duration; + +#[macro_use] +mod common; +use common::{clean_output_dir, create_wallet_proxy, setup}; + +/// Test cutoff block times +fn ttl_cutoff_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { + // Create a new proxy to simulate server and wallet responses + let mut wallet_proxy = create_wallet_proxy(test_dir); + let chain = wallet_proxy.chain.clone(); + let stopper = wallet_proxy.running.clone(); + + create_wallet_and_add!( + client1, + wallet1, + mask1_i, + test_dir, + "wallet1", + None, + &mut wallet_proxy, + false + ); + + let mask1 = (&mask1_i).as_ref(); + + create_wallet_and_add!( + client2, + wallet2, + mask2_i, + test_dir, + "wallet2", + None, + &mut wallet_proxy, + false + ); + + let mask2 = (&mask2_i).as_ref(); + + // Set the wallet proxy listener running + thread::spawn(move || { + if let Err(e) = wallet_proxy.run() { + error!("Wallet Proxy error: {}", e); + } + }); + + // few values to keep things shorter + + // Do some mining + let bh = 10u64; + let _ = + test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, bh as usize, false); + + let amount = 60_000_000_000; + let mut slate = Slate::blank(1, false); + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |sender_api, m| { + // note this will increment the block count as part of the transaction "Posting" + let args = InitTxArgs { + src_acct_name: None, + amount: amount, + minimum_confirmations: 2, + max_outputs: 500, + num_change_outputs: 1, + selection_strategy_is_use_all: true, + ttl_blocks: Some(2), + ..Default::default() + }; + let slate_i = sender_api.init_send_tx(m, args)?; + + slate = client1.send_tx_slate_direct("wallet2", &slate_i)?; + sender_api.tx_lock_outputs(m, &slate)?; + + let (_, txs) = sender_api.retrieve_txs(m, true, None, Some(slate.id), None)?; + let tx = txs[0].clone(); + + assert_eq!(tx.ttl_cutoff_height, Some(12)); + Ok(()) + })?; + + // Now mine past the block, and check again. Transaction should be gone. + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 2, false); + + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |sender_api, m| { + let (_, txs) = sender_api.retrieve_txs(m, true, None, Some(slate.id), None)?; + let tx = txs[0].clone(); + + assert_eq!(tx.ttl_cutoff_height, Some(12)); + assert!(tx.tx_type == TxLogEntryType::TxSentCancelled); + Ok(()) + })?; + + // Should also be gone in wallet 2, and output gone + wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |sender_api, m| { + let (_, txs) = sender_api.retrieve_txs(m, true, None, Some(slate.id), None)?; + let tx = txs[0].clone(); + let outputs = sender_api.retrieve_outputs(m, false, true, None)?.1; + assert_eq!(outputs.len(), 0); + + assert_eq!(tx.ttl_cutoff_height, Some(12)); + assert!(tx.tx_type == TxLogEntryType::TxReceivedCancelled); + Ok(()) + })?; + + // try again, except try and send off the transaction for completion beyond the expiry + let mut slate = Slate::blank(1, false); + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |sender_api, m| { + // note this will increment the block count as part of the transaction "Posting" + let args = InitTxArgs { + src_acct_name: None, + amount: amount, + minimum_confirmations: 2, + max_outputs: 500, + num_change_outputs: 1, + selection_strategy_is_use_all: true, + ttl_blocks: Some(2), + ..Default::default() + }; + let slate_i = sender_api.init_send_tx(m, args)?; + sender_api.tx_lock_outputs(m, &slate_i)?; + slate = slate_i; + + let (_, txs) = sender_api.retrieve_txs(m, true, None, Some(slate.id), None)?; + let tx = txs[0].clone(); + + assert_eq!(tx.ttl_cutoff_height, Some(14)); + Ok(()) + })?; + + // Mine past the ttl block and try to send + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 2, false); + + // Wallet 2 will need to have updated past the TTL + wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |sender_api, m| { + let (_, _) = sender_api.retrieve_txs(m, true, None, Some(slate.id), None)?; + Ok(()) + })?; + + // And when wallet 1 sends, should be rejected + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |_sender_api, _m| { + let res = client1.send_tx_slate_direct("wallet2", &slate); + println!("Send after TTL result is: {:?}", res); + assert!(res.is_err()); + Ok(()) + })?; + + // let logging finish + stopper.store(false, Ordering::Relaxed); + thread::sleep(Duration::from_millis(200)); + Ok(()) +} + +#[test] +fn ttl_cutoff() { + let test_dir = "test_output/ttl_cutoff"; + setup(test_dir); + if let Err(e) = ttl_cutoff_test_impl(test_dir) { + panic!("Libwallet Error: {}", e); + } + clean_output_dir(test_dir); +} diff --git a/controller/tests/tx_list_filter.rs b/controller/tests/tx_list_filter.rs new file mode 100644 index 0000000..7a8dab0 --- /dev/null +++ b/controller/tests/tx_list_filter.rs @@ -0,0 +1,360 @@ +// Copyright 2021 The Grin Developers +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! tests of advanced TX filtering + +#[macro_use] +extern crate log; +extern crate grin_wallet_controller as wallet; +extern crate grin_wallet_impls as impls; +extern crate grin_wallet_libwallet as libwallet; + +use grin_core as core; +use grin_keychain as keychain; +use grin_util as util; +use libwallet::{RetrieveTxQueryArgs, RetrieveTxQuerySortField}; + +use self::libwallet::{InitTxArgs, Slate}; +use impls::test_framework::{self, LocalWalletClient}; +use std::sync::{atomic::Ordering, Arc}; +use std::thread; +use std::time::Duration; +use util::secp::key::SecretKey; +use util::Mutex; + +use self::keychain::ExtKeychain; +use self::libwallet::WalletInst; +use impls::DefaultLCProvider; + +mod common; +use common::{clean_output_dir, create_wallet_proxy, setup}; + +fn test_wallet_tx_filtering( + wallet: Arc< + Mutex< + Box< + dyn WalletInst< + 'static, + DefaultLCProvider<'static, LocalWalletClient, ExtKeychain>, + LocalWalletClient, + ExtKeychain, + >, + >, + >, + >, + mask: Option<&SecretKey>, +) -> Result<(), libwallet::Error> { + wallet::controller::owner_single_use(Some(wallet.clone()), mask, None, |api, _m| { + let mut tx_query_args = RetrieveTxQueryArgs::default(); + tx_query_args.min_id = Some(5); + + // Min ID + let tx_results = api + .retrieve_txs(mask, true, None, None, Some(tx_query_args))? + .1; + assert_eq!(tx_results[0].id, 5); + assert_eq!(tx_results[tx_results.len() - 1].id, 33); + + // Max ID + let mut tx_query_args = RetrieveTxQueryArgs::default(); + tx_query_args.min_id = Some(5); + tx_query_args.max_id = Some(20); + let tx_results = api + .retrieve_txs(mask, true, None, None, Some(tx_query_args))? + .1; + assert_eq!(tx_results[0].id, 5); + assert_eq!(tx_results[tx_results.len() - 1].id, 20); + + // Exclude 1 cancelled + let mut tx_query_args = RetrieveTxQueryArgs::default(); + tx_query_args.exclude_cancelled = Some(true); + tx_query_args.min_id = Some(5); + tx_query_args.max_id = Some(50); + let tx_results = api + .retrieve_txs(mask, true, None, None, Some(tx_query_args))? + .1; + assert_eq!(tx_results.len(), 28); + + // Exclude 1 cancelled, show confirmed only + let mut tx_query_args = RetrieveTxQueryArgs::default(); + tx_query_args.exclude_cancelled = Some(true); + tx_query_args.include_confirmed_only = Some(true); + tx_query_args.min_id = Some(5); + tx_query_args.max_id = Some(50); + let tx_results = api + .retrieve_txs(mask, true, None, None, Some(tx_query_args))? + .1; + assert_eq!(tx_results.len(), 14); + + // show outstanding only (including cancelled) + let mut tx_query_args = RetrieveTxQueryArgs::default(); + tx_query_args.exclude_cancelled = Some(false); + tx_query_args.include_outstanding_only = Some(true); + tx_query_args.min_id = Some(5); + tx_query_args.max_id = Some(50); + let tx_results = api + .retrieve_txs(mask, true, None, None, Some(tx_query_args))? + .1; + assert_eq!(tx_results.len(), 15); + + // outstanding only and confirmed only should give empty set + let mut tx_query_args = RetrieveTxQueryArgs::default(); + tx_query_args.exclude_cancelled = Some(false); + tx_query_args.include_outstanding_only = Some(true); + tx_query_args.include_confirmed_only = Some(true); + tx_query_args.min_id = Some(5); + tx_query_args.max_id = Some(50); + let tx_results = api + .retrieve_txs(mask, true, None, None, Some(tx_query_args))? + .1; + assert_eq!(tx_results.len(), 0); + + // include sent only + let mut tx_query_args = RetrieveTxQueryArgs::default(); + tx_query_args.include_sent_only = Some(true); + let tx_results = api + .retrieve_txs(mask, true, None, None, Some(tx_query_args))? + .1; + assert_eq!(tx_results.len(), 15); + + // include received only (none in this set) + let mut tx_query_args = RetrieveTxQueryArgs::default(); + tx_query_args.include_received_only = Some(true); + let tx_results = api + .retrieve_txs(mask, true, None, None, Some(tx_query_args))? + .1; + assert_eq!(tx_results.len(), 0); + + // include reverted only (none in this set) + let mut tx_query_args = RetrieveTxQueryArgs::default(); + tx_query_args.include_reverted_only = Some(true); + let tx_results = api + .retrieve_txs(mask, true, None, None, Some(tx_query_args))? + .1; + assert_eq!(tx_results.len(), 0); + + // include coinbase only + let mut tx_query_args = RetrieveTxQueryArgs::default(); + tx_query_args.include_coinbase_only = Some(true); + let tx_results = api + .retrieve_txs(mask, true, None, None, Some(tx_query_args))? + .1; + assert_eq!(tx_results.len(), 19); + + // Amounts + let mut tx_query_args = RetrieveTxQueryArgs::default(); + tx_query_args.min_amount = Some(60_000_000_000 - 59_963_300_000); + let tx_results = api + .retrieve_txs(mask, true, None, None, Some(tx_query_args))? + .1; + assert_eq!(tx_results.len(), 27); + + // amount, should see as above with coinbases excluded + let mut tx_query_args = RetrieveTxQueryArgs::default(); + tx_query_args.min_amount = Some(60_000_000_000 - 59_963_300_000); + tx_query_args.max_amount = Some(60_000_000_000 - 1); + let tx_results = api + .retrieve_txs(mask, true, None, None, Some(tx_query_args))? + .1; + assert_eq!(tx_results.len(), 8); + + // Amount - should only see coinbase (incoming) + let mut tx_query_args = RetrieveTxQueryArgs::default(); + tx_query_args.min_amount = Some(60_000_000_000); + let tx_results = api + .retrieve_txs(mask, true, None, None, Some(tx_query_args))? + .1; + assert_eq!(tx_results.len(), 19); + + // sort order + let mut tx_query_args = RetrieveTxQueryArgs::default(); + tx_query_args.sort_order = Some(libwallet::RetrieveTxQuerySortOrder::Desc); + let tx_results = api + .retrieve_txs(mask, true, None, None, Some(tx_query_args))? + .1; + + assert_eq!(tx_results[0].id, 33); + assert_eq!(tx_results[tx_results.len() - 1].id, 0); + + // change sort field to amount desc, should have coinbases first + let mut tx_query_args = RetrieveTxQueryArgs::default(); + tx_query_args.sort_order = Some(libwallet::RetrieveTxQuerySortOrder::Desc); + tx_query_args.sort_field = Some(RetrieveTxQuerySortField::TotalAmount); + let tx_results = api + .retrieve_txs(mask, true, None, None, Some(tx_query_args))? + .1; + assert_eq!(tx_results[0].amount_credited, 60_000_000_000); + + /*for entry in tx_results.iter() { + println!("{:?}", entry); + }*/ + + Ok(()) + })?; + Ok(()) +} + +/// Builds a wallet + chain with a few transactions, and return wallet for further testing +fn build_chain_for_tx_filtering( + test_dir: &'static str, + block_height: usize, +) -> Result<(), libwallet::Error> { + // Create a new proxy to simulate server and wallet responses + let mut wallet_proxy = create_wallet_proxy(test_dir); + let chain = wallet_proxy.chain.clone(); + let stopper = wallet_proxy.running.clone(); + + create_wallet_and_add!( + client1, + wallet1, + mask1_i, + test_dir, + "wallet1", + None, + &mut wallet_proxy, + true + ); + let mask1 = (&mask1_i).as_ref(); + debug!("Mask1: {:?}", mask1); + create_wallet_and_add!( + client2, + wallet2, + mask2_i, + test_dir, + "wallet2", + None, + &mut wallet_proxy, + false + ); + let mask2 = (&mask2_i).as_ref(); + debug!("Mask2: {:?}", mask2); + + // Set the wallet proxy listener running + thread::spawn(move || { + if let Err(e) = wallet_proxy.run() { + error!("Wallet Proxy error: {}", e); + } + }); + + // Stop the scanning updater threads because it extends the time needed to build the chain + // exponentially + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, _m| { + api.stop_updater()?; + Ok(()) + })?; + + wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, _m| { + api.stop_updater()?; + Ok(()) + })?; + + // few values to keep things shorter + let reward = core::consensus::REWARD; + + // Start off with a few blocks + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 3, false); + + for i in 0..block_height { + let mut wallet_1_has_funds = false; + + // Check wallet 1 contents + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let (_, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + debug!( + "Wallet 1 spendable - {}", + wallet1_info.amount_currently_spendable + ); + if wallet1_info.amount_currently_spendable > reward { + wallet_1_has_funds = true; + } + Ok(()) + })?; + + if !wallet_1_has_funds { + let _ = + test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 1, false); + continue; + } + + // send a random tx + let num_txs = 1; + for _ in 0..num_txs { + let amount: u64 = i as u64 * 1_000_000; + let mut slate = Slate::blank(1, false); + debug!("Creating TX for {}", amount); + wallet::controller::owner_single_use( + Some(wallet1.clone()), + mask1, + None, + |sender_api, m| { + // note this will increment the block count as part of the transaction "Posting" + let args = InitTxArgs { + src_acct_name: None, + amount: amount, + minimum_confirmations: 1, + max_outputs: 500, + num_change_outputs: 1, + selection_strategy_is_use_all: false, + ..Default::default() + }; + let slate_i = sender_api.init_send_tx(m, args)?; + slate = client1.send_tx_slate_direct("wallet2", &slate_i)?; + sender_api.tx_lock_outputs(m, &slate)?; + slate = sender_api.finalize_tx(m, &slate)?; + Ok(()) + }, + )?; + } + } + + // Cancel a tx for filtering testing + let amount: u64 = 1_000_000; + let mut slate = Slate::blank(1, false); + debug!("Creating TX for {}", amount); + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |sender_api, m| { + // note this will increment the block count as part of the transaction "Posting" + let args = InitTxArgs { + src_acct_name: None, + amount: amount, + minimum_confirmations: 1, + max_outputs: 500, + num_change_outputs: 1, + selection_strategy_is_use_all: false, + ..Default::default() + }; + let slate_i = sender_api.init_send_tx(m, args)?; + slate = client1.send_tx_slate_direct("wallet2", &slate_i)?; + sender_api.tx_lock_outputs(m, &slate)?; + sender_api.cancel_tx(m, Some(33), None)?; + Ok(()) + })?; + + // Perform actual testing + test_wallet_tx_filtering(wallet1, mask1)?; + + // let logging finish + stopper.store(false, Ordering::Relaxed); + thread::sleep(Duration::from_millis(200)); + Ok(()) +} + +#[test] +fn wallet_tx_filtering() { + let test_dir = "test_output/advanced_tx_filtering"; + clean_output_dir(test_dir); + setup(test_dir); + if let Err(e) = build_chain_for_tx_filtering(test_dir, 30) { + panic!("Libwallet Error: {}", e); + } + clean_output_dir(test_dir); +} diff --git a/controller/tests/updater_thread.rs b/controller/tests/updater_thread.rs new file mode 100644 index 0000000..c137b4e --- /dev/null +++ b/controller/tests/updater_thread.rs @@ -0,0 +1,123 @@ +// Copyright 2021 The Grin Developers +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Test a wallet repost command +#[macro_use] +extern crate log; +extern crate grin_wallet_api as api; +extern crate grin_wallet_controller as wallet; +extern crate grin_wallet_impls as impls; +extern crate grin_wallet_libwallet as libwallet; + +// use crate::libwallet::api_impl::owner_updater::{start_updater_log_thread, StatusMessage}; +// use grin_wallet_util::grin_core as core; + +use impls::test_framework::{self, LocalWalletClient}; +use std::sync::atomic::Ordering; +use std::thread; +use std::time::Duration; + +#[macro_use] +mod common; +use common::{clean_output_dir, create_wallet_proxy, setup, setup_global_chain_type}; + +/// updater thread test impl +fn updater_thread_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { + // Create a new proxy to simulate server and wallet responses + let mut wallet_proxy = create_wallet_proxy(test_dir); + let chain = wallet_proxy.chain.clone(); + let stopper = wallet_proxy.running.clone(); + + // Create a new wallet test client, and set its queues to communicate with the + // proxy + create_wallet_and_add!( + client1, + wallet1, + mask1_i, + test_dir, + "wallet1", + None, + &mut wallet_proxy, + false + ); + let mask1 = (&mask1_i).as_ref(); + create_wallet_and_add!( + client2, + wallet2, + mask2_i, + test_dir, + "wallet2", + None, + &mut wallet_proxy, + false + ); + let mask2 = (&mask2_i).as_ref(); + + // Set the wallet proxy listener running + thread::spawn(move || { + if let Err(e) = wallet_proxy.run() { + error!("Wallet Proxy error: {}", e); + } + }); + + // add some accounts + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + api.create_account_path(m, "mining")?; + api.create_account_path(m, "listener")?; + Ok(()) + })?; + + // add some accounts + wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| { + api.create_account_path(m, "account1")?; + api.create_account_path(m, "account2")?; + Ok(()) + })?; + + // Get some mining done + { + wallet_inst!(wallet1, w); + w.set_parent_key_id_by_name("mining")?; + } + let bh = 10u64; + let _ = + test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, bh as usize, false); + + let owner_api = api::Owner::new(wallet1, None); + owner_api.start_updater(mask1, Duration::from_secs(5))?; + + // let updater thread run a bit + thread::sleep(Duration::from_secs(10)); + + let messages = owner_api.get_updater_messages(1000)?; + assert_eq!(messages.len(), 32); + + owner_api.stop_updater()?; + stopper.store(false, Ordering::Relaxed); + thread::sleep(Duration::from_secs(2)); + Ok(()) +} + +#[test] +fn updater_thread() { + // The "updater" kicks off a new thread so we need to ensure the global chain_type + // is set for this to work correctly. + setup_global_chain_type(); + + let test_dir = "test_output/updater_thread"; + setup(test_dir); + if let Err(e) = updater_thread_test_impl(test_dir) { + panic!("Libwallet Error: {}", e); + } + clean_output_dir(test_dir); +} diff --git a/doc/design/design.md b/doc/design/design.md new file mode 100644 index 0000000..d113504 --- /dev/null +++ b/doc/design/design.md @@ -0,0 +1,89 @@ +# Grin Wallet + Library Design + +![wallet design](wallet-arch.png) + +## High Level Wallet Design Overview + +The current Grin `wallet` crate provides several layers of libraries, services, and traits that can be mixed, matched and reimplemented to support +various needs within the default Grin wallet as well as provide a set of useful library functions for 3rd-party implementors. At a very high level, +the code is organized into the following components (from highest-level to lowest): + +* **Command Line Client** - The command line client invoked by `grin-wallet [command]`, simply instantiates the other components below + and parses command line arguments as needed. +* **Web Wallet Client** - [Work In Progress] A web wallet client accessible from the local machine only. Current code can be viewed here: + https://github.com/mimblewimble/grin-web-wallet +* **Static File Server** - [TBD] A means of serving up the web wallet client above to the user (still under consideration) +* **libWallet** - A high level wallet library that provides functions for the default grin wallet. The functions in here can be somewhat + specific to how the grin wallet does things, but could still be reused by 3rd party implementors following the same basic principles as grin + does. Major functionality is split into: + * **Owner API** - An API that provides information that should only be viewable by the wallet owner + * **Foreign API** - An API to communicate with other wallets and external grin nodes + * **Service Controller** - A Controller that instantiates the above APIs (either locally or via web services) + * **Internal Functions** Helper functions to perform needed wallet tasks, such as selecting coins, updating wallet outputs with + results from a Grin node, etc. +* **libTx** - Library that provides lower-level transaction building, rangeproof and signing functions, highly-reusable by wallet implementors. +* **Wallet Traits** - A set of generic traits defined within libWallet and the `keychain` crate . A wallet implementation such as Grin's current + default only needs to implement these traits in order to provide a wallet: + * **NodeClient** - Defines communication between the wallet, a running grin node and/or other wallets + * **WalletBackend** - Defines the storage implementation of the wallet + * **KeyChain** - Defines key derivation operations + +## Module-Specific Notes + +A full API-Description for each of these parts is still TBD (and should be generated by rustdoc rather than repeated here). However a few design +notes on each module are worth mentioning here. + +### Web Wallet Client / Static File Server + +This component is not a 3rd-party hosted 'Web Wallet' , but a client meant to be run on the local machine only by the wallet owner. It should provide +a usable browser interface into the wallet, that should be functionally equivalent to using the command line but (hopefully) far easier to use. +It is currently not being included by a default grin build, although the required listener is currently being run by default. To build and test this +component, see instructions on the [project page](https://github.com/mimblewimble/grin-web-wallet). The 'Static File Server' is still under +discussion, and concerns how to provide the web-wallet to the user in a default Grin build. + +### Owner API / Foreign API + +The high-level wallet API has been split into two, to allow for different requirements on each. For instance, the Foreign API would listen on +an external-facing port, and therefore potentially has different security requirements from the Owner API, which can simply be bound to localhost +only. + +### libTX + +Transactions are built using the concept of a 'Slate', which is a data structure that gets passed around to all participants in a transaction, +with each appending their Inputs, Outputs or Signatures to it to build a completed wallet transaction. Although the current mode of operation in +the default client only supports single-user - single recipient, an arbitrary number of participants to a transaction is supported within libTX. + +### Wallet Traits + +In the current code, a Wallet implementation is just a combination of these three traits. The vast majority of functions within libwallet +and libTX have a signature similar to the following: + +```rust +pub fn retrieve_outputs( +!·wallet: &mut T, +!·show_spent: bool, +!·tx_id: Option, +) -> Result, Error> +where +!·T: WalletBackend, +!·C: NodeClient, +!·K: Keychain, +{ +``` + +With `T` in this instance being a class that implements the `WalletBackend` trait, which is further parameterized with implementations of +`NodeClient` and `Keychain`. + +There is currently only a single implementation of the Keychain trait within the Grin code, in the `keychain` crate exported as `ExtKeyChain`. +The `Keychain` trait makes several assumptions about the underlying implementation, particularly that it will adhere to a +[BIP-38 style](https://github.com/bitcoin/bips/blob/master/bip-0038.mediawiki) 'master key -> child key' model. + +There are two implementations of `NodeClient` within the code, the main version being the `HTTPNodeClient` found within `wallet/src/client.rs` and +the seconds a test client that communicates with an in-process instance of a chain. The NodeClient isolates all network calls, so upgrading wallet +communication from the current simple http interaction to a more secure protocol (or allowing for many options) should be a simple +matter of dropping in different `NodeClient` implementations. + +There are also two implementations of `WalletBackend` within the code at the base of the `wallet` crate. `LMDBBackend` found within +`wallet/src/lmdb_wallet.rs` is the main implementation, and is now used by all grin wallet commands. The earlier `FileWallet` still exists +within the code, however it is not invoked, and given there are no real advantages to running it over a DB implementation, development on it +has been dropped in favour of the LMDB implementation. \ No newline at end of file diff --git a/doc/design/goals.md b/doc/design/goals.md new file mode 100644 index 0000000..a035c8c --- /dev/null +++ b/doc/design/goals.md @@ -0,0 +1,82 @@ + +Mode of Interactions +==================== + +There's a variety of ways wallet software can be integrated with, from hardware +to automated bots to the more classic desktop wallets. No single implementation +can hope to accommodate all possible interactions, especially if it wants to +remain user friendly (who or whatever the user may be). With that in mind, Grin +needs to provide a healthy base for a more complete wallet ecosystem to +develop. + +We propose to achieve this by implementing, as part of the "standard" wallet: + +* A good set of APIs that are flexible enough for most cases. +* One or two default main mode of interaction. + +While not being exhaustive, the different ways we can imagine wallet software +working with Grin are the following: + +1. A receive-only online wallet server. This should have some well-known network + address that can be reached by a client. There should be a spending key kept + offline. +1. A fully offline interaction. The sender uses her wallet to dump a file that's + sent to the receiver in any practical way. The receiver builds upon that file, + sending it back to the sender. The sender finalizes the transaction and sends it + to a Grin node. +1. Fully online interaction through a non-trusted 3rd party. In this mode + receiver and sender both connect to a web server that facilitates the + interaction. Exchanges can be all be encrypted. +1. Hardware wallet. Similar to offline but the hardware wallet interacts with + a computer to produce required public keys and signatures. +1. Web wallet. A 3rd party runs the required software behind the scenes and + handles some of the key generation. This could be done in a custodial, + non-custodial and multisig fashion. +1. Fully programmatic. Similar to the online server, but both for receiving and + sending, most likely by an automated bot of some sorts. + +As part of the Grin project, we will only consider the first 2 modes of +interaction. We hope that other projects and businesses will tackle other modes +and perhaps even create new ones we haven't considered. + +Design Considerations +===================== + +Lower-level APIs +---------------- + +Rust can easily be [reused by other languages](https://doc.rust-lang.org/1.2.0/book/rust-inside-other-languages.html) +like Ruby, Python or node.js, using standard FFI libraries. By providing APIs +to build and manipulate commitments, related bulletproofs and aggregate +signatures we can kill many birds with one stone: + +* Make the job of wallet implementers easier. The underlying cryptographic + concepts can be quite complex. +* Make wallet implementations more secure. As we provide a higher level API, + there is less risk in misusing lower-level constructs. +* Provide some standardization in the way aggregations are done. There are + sometimes multiple ways to build a commitment or aggregate signatures or proofs + in a multiparty output. +* Provide more eyeballs and more security to the standard library. We need to + have the wallet APIs thoroughly reviewed regardless. + +Receive-only Online Wallet +-------------------------- + +To be receive only we need an aggregation between a "hot" receiving key and an +offline spending key. To receive, only the receiving key should be required, to +spend both keys are needed. + +This can work by forming a multi-party output (multisig) where only the public +part of the spending key is known to the receiving server. Practically a master +public key that can be derived similarly to Hierarchical Deterministic wallets +would provide the best security and privacy. + +TODO figure out what's needed for the bulletproof. Maybe pre-compute multiple +of them for ranges of receiving amounts (i.e. 1-10 grins, 10-100 grins, etc). + +Offline Wallet +-------------- + +This is likely the simplest to implement, with each interaction dumping its +intermediate values to a file and building off each other. diff --git a/doc/design/wallet-arch.png b/doc/design/wallet-arch.png new file mode 100644 index 0000000000000000000000000000000000000000..5a50a1b447db2739d0636be5bf5f4c0afff4bdae GIT binary patch literal 115352 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 Sel>g2&!$s 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 traits +libwallet <--> traits + +note right of traits + **Default Wallet simply a struct that provides** + **implementations for these 3 traits** +end note + +' Client Side +'package "Provided as reference implementation" { + [Pure JS Wallet Client Implementation] as js_client + [Command Line Wallet Client] as cl_client + component web_server [ + V. Light Rust Web Server - Serve static files (TBD) + (Provided by default - localhost only) + (Serve up pure JS client) + ] +'} + +[External Wallets] as external_wallets +[External Wallets] as external_wallets_2 + +wallet_client <--> grin_node +wallet_client <--> external_wallets_2 + +web_server <--> owner_http +js_client <-- web_server +cl_client <--> owner_single +cl_client <--> foreign_single + +owner_single <--> owner_api +foreign_single <--> foreign_api + +libwallet <--> libtx + +foreign_api --> libwallet +owner_api --> libwallet + +js_client <--> owner_http +owner_http <--> owner_api +external_wallets <--> foreign_http +foreign_http <--> foreign_api + +'layout fix +'grin_node -[hidden]- wallet_backend + +@enduml \ No newline at end of file diff --git a/doc/samples/v3_api_node/package-lock.json b/doc/samples/v3_api_node/package-lock.json new file mode 100644 index 0000000..4ccbc35 --- /dev/null +++ b/doc/samples/v3_api_node/package-lock.json @@ -0,0 +1,115 @@ +{ + "name": "node-sample", + "version": "0.0.1", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@types/connect": { + "version": "3.4.33", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.33.tgz", + "integrity": "sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A==", + "requires": { + "@types/node": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.2.tgz", + "integrity": "sha512-El9yMpctM6tORDAiBwZVLMcxoTMcqqRO9dVyYcn7ycLWbvR8klrDn8CAOwRfZujZtWD7yS/mshTdz43jMOejbg==", + "requires": { + "@types/node": "*", + "@types/range-parser": "*" + } + }, + "@types/lodash": { + "version": "4.14.149", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.149.tgz", + "integrity": "sha512-ijGqzZt/b7BfzcK9vTrS6MFljQRPn5BFWOx8oE0GYxribu6uV+aA9zZuXI1zc/etK9E8nrgdoF2+LgUw7+9tJQ==" + }, + "@types/node": { + "version": "12.12.27", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.27.tgz", + "integrity": "sha512-odQFl/+B9idbdS0e8IxDl2ia/LP8KZLXhV3BUeI98TrZp0uoIzQPhGd+5EtzHmT0SMOIaPd7jfz6pOHLWTtl7A==" + }, + "@types/range-parser": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", + "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==" + }, + "JSONStream": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "requires": { + "jsonparse": "^1.2.0", + "through": ">=2.2.7 <3" + } + }, + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, + "es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" + }, + "es6-promisify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", + "requires": { + "es6-promise": "^4.0.3" + } + }, + "eyes": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", + "integrity": "sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=" + }, + "jayson": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jayson/-/jayson-3.2.0.tgz", + "integrity": "sha512-DZQnwA57GcStw4soSYB2VntWXFfoWvmSarlaWePDYOWhjxT72PBM4atEBomaTaS1uqk3jFC9UO9AyWjlujo3xw==", + "requires": { + "@types/connect": "^3.4.32", + "@types/express-serve-static-core": "^4.16.9", + "@types/lodash": "^4.14.139", + "@types/node": "^12.7.7", + "JSONStream": "^1.3.1", + "commander": "^2.12.2", + "es6-promisify": "^5.0.0", + "eyes": "^0.1.8", + "json-stringify-safe": "^5.0.1", + "lodash": "^4.17.15", + "uuid": "^3.2.1" + } + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=" + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" + } + } +} diff --git a/doc/samples/v3_api_node/package.json b/doc/samples/v3_api_node/package.json new file mode 100644 index 0000000..1126955 --- /dev/null +++ b/doc/samples/v3_api_node/package.json @@ -0,0 +1,14 @@ +{ + "name": "node-sample", + "version": "0.0.1", + "description": "Sample of connecting to the secure OwnerAPI via node", + "main": "src/index.js", + "scripts": { + "test": "npm test" + }, + "author": "", + "license": "ISC", + "dependencies": { + "jayson": "^3.2.0" + } +} diff --git a/doc/samples/v3_api_node/readme.md b/doc/samples/v3_api_node/readme.md new file mode 100644 index 0000000..e08ccbf --- /dev/null +++ b/doc/samples/v3_api_node/readme.md @@ -0,0 +1,28 @@ +# Connecting to the wallet's V3 Owner API from Node + +This is a small sample with code that demonstrates how to initialize the Wallet V3's Secure API and call API functions through it. + +To run this sample: + +First run the Owner API: + +```.sh +grin-wallet owner_api +``` + +This sample doesn't use the authentication specified in the wallet's `.api_secret`, so before running the owner_api please ensure api authentication is commented out in `grin-wallet.toml`. Including the authentication token as part of the request is a function of your json-rpc client library of choice, so it's not included in the sample to make setup a bit simpler. + +ensure the client url in `src\index.js` is set correctly: + +```.sh +const client = jayson.client.http('http://localhost:3420/v3/owner'); +``` + +Then (assuming node.js and npm are installed on the system): + +```.sh +npm install +node src/index.json +``` + +Feel free to play around with the sample, modifying it to call whatever functions you'd like to see in operation! diff --git a/doc/samples/v3_api_node/src/index.js b/doc/samples/v3_api_node/src/index.js new file mode 100644 index 0000000..6de1c5b --- /dev/null +++ b/doc/samples/v3_api_node/src/index.js @@ -0,0 +1,134 @@ +/* Sample Code for connecting to the V3 Secure API via Node + * + * With thanks to xiaojay of Niffler Wallet: + * https://github.com/grinfans/Niffler/blob/gw3/src/shared/walletv3.js + * + */ + +const jayson = require('jayson/promise'); +const crypto = require('crypto'); + +const client = jayson.client.http('http://localhost:3420/v3/owner'); + +// Demo implementation of using `aes-256-gcm` with node.js's `crypto` lib. +const aes256gcm = (shared_secret) => { + const ALGO = 'aes-256-gcm'; + + // encrypt returns base64-encoded ciphertext + const encrypt = (str, nonce) => { + let key = Buffer.from(shared_secret, 'hex') + const cipher = crypto.createCipheriv(ALGO, key, nonce) + const enc = Buffer.concat([cipher.update(str, 'utf8'), cipher.final()]) + const tag = cipher.getAuthTag() + return Buffer.concat([enc, tag]).toString('base64') + }; + + // decrypt decodes base64-encoded ciphertext into a utf8-encoded string + const decrypt = (enc, nonce) => { + //key,nonce is all buffer type; data is base64-encoded string + let key = Buffer.from(shared_secret, 'hex') + const data_ = Buffer.from(enc, 'base64') + const decipher = crypto.createDecipheriv(ALGO, key, nonce) + const len = data_.length + const tag = data_.slice(len-16, len) + const text = data_.slice(0, len-16) + decipher.setAuthTag(tag) + const dec = decipher.update(text, 'binary', 'utf8') + decipher.final('utf8'); + return dec + }; + + return { + encrypt, + decrypt, + }; +}; + +class JSONRequestEncrypted { + constructor(id, method, params) { + this.jsonrpc = '2.0' + this.method = method + this.id = id + this.params = params + } + + async send(key){ + const aesCipher = aes256gcm(key); + const nonce = new Buffer.from(crypto.randomBytes(12)); + let enc = aesCipher.encrypt(JSON.stringify(this), nonce); + console.log("Encrypted: " + enc) + let params = { + 'nonce': nonce.toString('hex'), + 'body_enc': enc, + } + let response = await client.request('encrypted_request_v3', params); + + if (response.err) { + throw response.err + } + + const nonce2 = Buffer.from(response.result.Ok.nonce, 'hex'); + const data = Buffer.from(response.result.Ok.body_enc, 'base64'); + + let dec = aesCipher.decrypt(data, nonce2) + return dec + } +} + +async function initSecure() { + let ecdh = crypto.createECDH('secp256k1') + ecdh.generateKeys() + let publicKey = ecdh.getPublicKey('hex', 'compressed') + const params = { + 'ecdh_pubkey': publicKey + } + let response = await client.request('init_secure_api', params); + if (response.err) { + throw response.err + } + + return ecdh.computeSecret(response.result.Ok, 'hex', 'hex') +} + +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +async function main() { + let shared_key = await initSecure(); + + let response = await new JSONRequestEncrypted(1, 'open_wallet', { + "name": null, + "password": "", + }).send(shared_key); + + let token = JSON.parse(response).result.Ok; + + let iterations = 1; + + for (i=0; i `3.3.3.3`) + +If you don't have a static IP you may want to consider using services like DynDNS which support dynamic IP resolving, this case is not covered by this guide, but all the next steps are equally applicable. + +If you don't have a domain name there is a possibility to get a TLS certificate for your IP, but you have to pay for that (so perhaps it's cheaper to buy a domain name) and it's rarely supported by certificate providers. + +## I have a TLS certificate already +Uncomment and update the following lines in wallet config (by default `~/.grin/grin-wallet.toml`): + +```toml +tls_certificate_file = "/path/to/my/cerificate/fullchain.pem" +tls_certificate_key = "/path/to/my/cerificate/privkey.pem" +``` + +If you have Stratum server enabled (you run a miner) make sure that wallet listener URL starts with `https` in node config (by default `~/.grin/grin-server.toml`): + +```toml +wallet_listener_url = "https://grin1.example.com:13415" +``` + +Make sure your user has read access to the files (see below for how to do it). Restart wallet. If you changed your node configuration restart `grin` too. When you (or someone else) send grins to this wallet the destination (`-d` option) must start with `https://`, not with `http://`. + +## I don't have a TLS certificate +You can get it for free from [Let's Encrypt](https://letsencrypt.org/). To simplify the process we need `certbot`. + +### Install certbot +Go to [Certbot home page](https://certbot.eff.org/), choose I'm using `None of the above` and your OS (eg `Ubuntu 18.04` which will be used as an example). You will be redirected to a page with instructions like [steps for Ubuntu](https://certbot.eff.org/lets-encrypt/ubuntubionic-other). Follow instructions from `Install` section. As result you should have `certbot` installed. + +### Obtain certificate +If you have experince with `certboot` feel free to use any type of challenge. This guide covers the simplest case of HTTP challenge. For this you need to have a web server listening on port `80`, which requires running it as root in the simplest case. We will use the server provided by certbot. **Make sure you have port 80 open** + +```sh +sudo certbot certonly --standalone -d grin1.example.com +``` + +It will ask you some questions, as result you should see something like: + +``` +Congratulations! Your certificate and chain have been saved at: + /etc/letsencrypt/live/grin1.example.com/fullchain.pem + Your key file has been saved at: + /etc/letsencrypt/live/grin1.example.com/privkey.pem + Your cert will expire on 2019-01-16. To obtain a new or tweaked + version of this certificate in the future, simply run certbot + again. To non-interactively renew *all* of your certificates, run +"certbot renew" +``` + +### Change permissions +Now you have the certificate files but only root user can read it. We run grin as `ubuntu` user. There are different scenarios how to fix it, the simplest one is to create a group which will have access to `/etc/letsencrypt` directory and add our user to this group. + +```sh +sudo groupadd tls-cert +sudo usermod -a -G tls-cert ubuntu +chgrp -R tls-cert /etc/letsencrypt +chmod -R g=rX /etc/letsencrypt +sudo chmod 2755 /etc/letsencrypt +``` + +The last step is needed for renewal, it makes sure that all new files will have the same group ownership. + +### Update wallet config +Refer to `I have a TLS certificate already` because you have it now. Use the folowing values: + +```toml +tls_certificate_file = "/etc/letsencrypt/live/grin1.example.com/fullchain.pem" +tls_certificate_key = "/etc/letsencrypt/live/grin1.example.com/privkey.pem" +``` + diff --git a/doc/transaction/basic-transaction-wf.png b/doc/transaction/basic-transaction-wf.png new file mode 100644 index 0000000000000000000000000000000000000000..a3508d5f1bec19496637a607763c9312d7dd36f6 GIT binary patch literal 157285 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

    % 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 recipient: **Slate** +== Round 2 == +note right of recipient + 1: Check fee against number of **inputs**, **change_outputs** +1 * **receiver_output**) + 2: Create **receiver_output** + 3: Choose random blinding factor for **receiver_output** **xR** (private scalar) +end note +note right of recipient + 4: Calculate message **M** = **fee | lock_height ** + 5: Choose random nonce **kR** (private scalar) + 6: Multiply **xR** and **kR** by generator G to create public curve points **xRG** and **kRG** + 7: Compute Schnorr challenge **e** = SHA256(**kRG** + **kSG** | **xRG** + **xSG** | **M**) + 8: Compute Recipient Schnorr signature **sR** = **kR** + **e** * **xR** + 9: Add **sR, xRG, kRG** to **Slate** + 10: Create wallet output function **rF** that stores **receiver_output** in wallet with status "Unconfirmed" + and identifying transaction log entry **TR** linking **receiver_output** with transaction. +end note +alt All Okay +recipient --> sender: Okay - **Slate** +recipient -> recipient: execute wallet output function **rF** +else Any Failure +recipient ->x]: Abort +recipient --> sender: Error +[x<- sender: Abort +end +== Finalize Transaction == +note left of sender + 1: Calculate message **M** = **fee | lock_height ** + 2: Compute Schnorr challenge **e** = SHA256(**kRG** + **kSG** | **xRG** + **xSG** | **M**) + 3: Verify **sR** by verifying **kRG** + **e** * **xRG** = **sRG** + 4: Compute Sender Schnorr signature **sS** = **kS** + **e** * **xS** + 5: Calculate final signature **s** = (**kSG**+**kRG**, **sS**+**sR**) + 6: Calculate public key for **s**: **xG** = **xRG** + **xSG** + 7: Verify **s** against excess values in final transaction using **xG** + 8: Create Transaction Kernel Containing: + Excess signature **s** + Public excess **xG** + **fee** + **lock_height** +end note +sender -> sender: Create final transaction **tx** from **Slate** +sender -> grin_node: Post **tx** to mempool +grin_node --> recipient: "Ok" +alt All Okay +recipient --> sender: "Ok" - **UUID** +sender -> sender: Execute wallet lock function **sF** +...Await confirmation... +recipient -> grin_node: Confirm **receiver_output** +recipient -> recipient: Change status of **receiver_output** to "Confirmed" +sender -> grin_node: Confirm **change_output** +sender -> sender: Change status of **inputs** to "Spent" +sender -> sender: Change status of **change_output** to "Confirmed" +else Any Error +recipient -> recipient: Manually remove **receiver_output** from wallet using transaction log entry **TR** +recipient ->x]: Abort +recipient --> sender: Error +sender -> sender: Unlock **inputs** and delete **change_output** identified in transaction log entry **TS** +[x<- sender: Abort +end + + +@enduml \ No newline at end of file diff --git a/impls/Cargo.toml b/impls/Cargo.toml new file mode 100644 index 0000000..7689d61 --- /dev/null +++ b/impls/Cargo.toml @@ -0,0 +1,81 @@ +[package] +name = "grin_wallet_impls" +version = "5.4.0-alpha.1" +authors = ["Grin Developers "] +description = "Concrete types derived from libwallet traits" +license = "Apache-2.0" +repository = "https://github.com/mimblewimble/grin-wallet" +keywords = [ "crypto", "grin", "mimblewimble" ] +exclude = ["**/*.grin", "**/*.grin2"] +edition = "2018" + +[dependencies] +blake2-rfc = "0.2" +thiserror = "1" +futures = "0.3" +rand = "0.6" +serde = "1" +serde_derive = "1" +serde_json = "1" +log = "0.4" +ring = "0.16" +uuid = { version = "0.8", features = ["serde", "v4"] } +chrono = { version = "0.4.11", features = ["serde"] } +lazy_static = "1" +tokio = { version = "0.2", features = ["full"] } +reqwest = { version = "0.10", features = ["rustls-tls", "socks"] } + +#Socks/Tor/Bridge/Proxy +byteorder = "1" +ed25519-dalek = "1.0.0-pre.4" +x25519-dalek = "0.6" +data-encoding = "2" +regex = "1.3" +timer = "0.2" +sysinfo = "0.29" +base64 = "0.12.0" +url = "2.1" + +grin_wallet_util = { path = "../util", version = "5.4.0-alpha.1" } +grin_wallet_config = { path = "../config", version = "5.4.0-alpha.1" } +grin_wallet_libwallet = { path = "../libwallet", version = "5.4.0-alpha.1" } + +##### Grin Imports + +# For Release +grin_core = "5.3.3" +grin_keychain = "5.3.3" +grin_chain = "5.3.3" +grin_util = "5.3.3" +grin_api = "5.3.3" +grin_store = "5.3.3" + +# For beta release + +# grin_core = { git = "https://github.com/mimblewimble/grin", tag = "v5.2.0-beta.3"} +# grin_keychain = { git = "https://github.com/mimblewimble/grin", tag = "v5.2.0-beta.3" } +# grin_chain = { git = "https://github.com/mimblewimble/grin", tag = "v5.2.0-beta.3" } +# grin_util = { git = "https://github.com/mimblewimble/grin", tag = "v5.2.0-beta.3" } +# grin_api = { git = "https://github.com/mimblewimble/grin", tag = "v5.2.0-beta.3" } +# grin_store = { git = "https://github.com/mimblewimble/grin", tag = "v5.2.0-beta.3" } + +# For bleeding edge +# grin_core = { git = "https://github.com/mimblewimble/grin", branch = "master" } +# grin_keychain = { git = "https://github.com/mimblewimble/grin", branch = "master" } +# grin_chain = { git = "https://github.com/mimblewimble/grin", branch = "master" } +# grin_util = { git = "https://github.com/mimblewimble/grin", branch = "master" } +# grin_api = { git = "https://github.com/mimblewimble/grin", branch = "master" } +# grin_store = { git = "https://github.com/mimblewimble/grin", branch = "master" } + +# For local testing +# grin_core = { path = "../../grin/core"} +# grin_keychain = { path = "../../grin/keychain"} +# grin_chain = { path = "../../grin/chain"} +# grin_util = { path = "../../grin/util"} +# grin_api = { path = "../../grin/api"} +# grin_store = { path = "../../grin/store"} + +##### + +[dev-dependencies] +"remove_dir_all" = "0.7" \ No newline at end of file diff --git a/impls/src/adapters/file.rs b/impls/src/adapters/file.rs new file mode 100644 index 0000000..4d2be6a --- /dev/null +++ b/impls/src/adapters/file.rs @@ -0,0 +1,73 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// File Output 'plugin' implementation +use std::fs::File; +use std::io::{Read, Write}; + +use crate::libwallet::{Error, Slate, SlateVersion, VersionedBinSlate, VersionedSlate}; +use crate::{SlateGetter, SlatePutter}; +use grin_wallet_util::byte_ser; +use std::convert::TryFrom; +use std::path::PathBuf; + +#[derive(Clone)] +pub struct PathToSlate(pub PathBuf); + +impl SlatePutter for PathToSlate { + fn put_tx(&self, slate: &Slate, as_bin: bool) -> Result<(), Error> { + // For testing (output raw slate data for reference) + /*{ + let mut raw_path = self.0.clone(); + raw_path.set_extension("raw"); + let mut raw_slate = File::create(&raw_path)?; + raw_slate.write_all(&format!("{:?}", slate).as_bytes())?; + raw_slate.sync_all()?; + }*/ + let mut pub_tx = File::create(&self.0)?; + // TODO: + let out_slate = VersionedSlate::into_version(slate.clone(), SlateVersion::V4)?; + if as_bin { + let bin_slate = VersionedBinSlate::try_from(out_slate).map_err(|_| Error::SlateSer)?; + pub_tx.write_all(&byte_ser::to_bytes(&bin_slate).map_err(|_| Error::SlateSer)?)?; + } else { + pub_tx.write_all( + serde_json::to_string_pretty(&out_slate) + .map_err(|_| Error::SlateSer)? + .as_bytes(), + )?; + } + pub_tx.sync_all()?; + Ok(()) + } +} + +impl SlateGetter for PathToSlate { + fn get_tx(&self) -> Result<(Slate, bool), Error> { + // try as bin first, then as json + let mut pub_tx_f = File::open(&self.0)?; + let mut data = Vec::new(); + pub_tx_f.read_to_end(&mut data)?; + let bin_res = byte_ser::from_bytes::(&data); + if let Err(e) = bin_res { + debug!("Not a valid binary slate: {} - Will try JSON", e); + } else if let Ok(s) = bin_res { + return Ok((Slate::upgrade(s.into())?, true)); + } + + // Otherwise try json + let content = String::from_utf8(data).map_err(|_| Error::SlateSer)?; + Ok((Slate::deserialize_upgrade(&content)?, false)) + } +} diff --git a/impls/src/adapters/http.rs b/impls/src/adapters/http.rs new file mode 100644 index 0000000..5b94dee --- /dev/null +++ b/impls/src/adapters/http.rs @@ -0,0 +1,292 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// HTTP Wallet 'plugin' implementation +use crate::client_utils::{Client, ClientError}; +use crate::libwallet::slate_versions::{SlateVersion, VersionedSlate}; +use crate::libwallet::{Error, Slate}; +use crate::tor::bridge::TorBridge; +use crate::tor::proxy::TorProxy; +use crate::SlateSender; +use grin_wallet_config::types::{TorBridgeConfig, TorProxyConfig}; +use serde::Serialize; +use serde_json::{json, Value}; +use std::collections::HashMap; +use std::convert::TryFrom; +use std::net::SocketAddr; +use std::path::MAIN_SEPARATOR; +use std::sync::Arc; + +use crate::tor::config as tor_config; +use crate::tor::process as tor_process; + +const TOR_CONFIG_PATH: &str = "tor/sender"; + +#[derive(Clone)] +pub struct HttpSlateSender { + base_url: String, + use_socks: bool, + socks_proxy_addr: Option, + tor_config_dir: String, + process: Option>, + bridge: TorBridgeConfig, + proxy: TorProxyConfig, +} + +impl HttpSlateSender { + /// Create, return Err if scheme is not "http" + fn new(base_url: &str) -> Result { + if !base_url.starts_with("http") && !base_url.starts_with("https") { + Err(SchemeNotHttp) + } else { + Ok(HttpSlateSender { + base_url: base_url.to_owned(), + use_socks: false, + socks_proxy_addr: None, + tor_config_dir: String::from(""), + process: None, + bridge: TorBridgeConfig::default(), + proxy: TorProxyConfig::default(), + }) + } + } + + /// Switch to using socks proxy + pub fn with_socks_proxy( + base_url: &str, + proxy_addr: &str, + tor_config_dir: &str, + tor_bridge: TorBridgeConfig, + tor_proxy: TorProxyConfig, + ) -> Result { + let mut ret = Self::new(base_url)?; + ret.use_socks = true; + //TODO: Unwrap + ret.socks_proxy_addr = Some(SocketAddr::V4(proxy_addr.parse().unwrap())); + ret.tor_config_dir = tor_config_dir.into(); + ret.bridge = tor_bridge; + ret.proxy = tor_proxy; + Ok(ret) + } + + /// launch TOR process + pub fn launch_tor(&mut self) -> Result<(), Error> { + // set up tor send process if needed + let mut tor = tor_process::TorProcess::new(); + if self.use_socks && self.process.is_none() { + let tor_dir = format!( + "{}{}{}", + &self.tor_config_dir, MAIN_SEPARATOR, TOR_CONFIG_PATH + ); + info!( + "Starting TOR Process for send at {:?}", + self.socks_proxy_addr + ); + + let mut hm_tor_bridge: HashMap = HashMap::new(); + if self.bridge.bridge_line.is_some() { + let bridge_struct = TorBridge::try_from(self.bridge.clone()) + .map_err(|e| Error::TorConfig(format!("{:?}", e)))?; + hm_tor_bridge = bridge_struct + .to_hashmap() + .map_err(|e| Error::TorConfig(format!("{:?}", e)))?; + } + + let mut hm_tor_proxy: HashMap = HashMap::new(); + if self.proxy.transport.is_some() || self.proxy.allowed_port.is_some() { + let proxy = TorProxy::try_from(self.proxy.clone()) + .map_err(|e| Error::TorConfig(format!("{:?}", e)))?; + hm_tor_proxy = proxy + .to_hashmap() + .map_err(|e| Error::TorConfig(format!("{:?}", e)))?; + } + + tor_config::output_tor_sender_config( + &tor_dir, + &self.socks_proxy_addr.unwrap().to_string(), + hm_tor_bridge, + hm_tor_proxy, + ) + .map_err(|e| Error::TorConfig(format!("{:?}", e)))?; + // Start TOR process + tor.torrc_path(&format!("{}/torrc", &tor_dir)) + .working_dir(&tor_dir) + .timeout(20) + .completion_percent(100) + .launch() + .map_err(|e| Error::TorProcess(format!("{:?}", e)))?; + self.process = Some(Arc::new(tor)); + } + Ok(()) + } + + /// Check version of the listening wallet + pub fn check_other_version(&mut self, url: &str) -> Result { + self.launch_tor()?; + let req = json!({ + "jsonrpc": "2.0", + "method": "check_version", + "id": 1, + "params": [] + }); + + let res: String = self.post(url, None, req).map_err(|e| { + let mut report = format!("Performing version check (is recipient listening?): {}", e); + let err_string = format!("{}", e); + if err_string.contains("404") { + // Report that the other version of the wallet is out of date + report = "Other wallet is incompatible and requires an upgrade. \ + Please urge the other wallet owner to upgrade and try the transaction again." + .to_string(); + error!("{}", report); + } + Error::ClientCallback(report) + })?; + + let res: Value = serde_json::from_str(&res).unwrap(); + trace!("Response: {}", res); + if res["error"] != json!(null) { + let report = format!( + "Posting transaction slate: Error: {}, Message: {}", + res["error"]["code"], res["error"]["message"] + ); + error!("{}", report); + return Err(Error::ClientCallback(report)); + } + + let resp_value = res["result"]["Ok"].clone(); + trace!("resp_value: {}", resp_value.clone()); + let foreign_api_version: u16 = + serde_json::from_value(resp_value["foreign_api_version"].clone()).unwrap(); + let supported_slate_versions: Vec = + serde_json::from_value(resp_value["supported_slate_versions"].clone()).unwrap(); + + // trivial tests for now, but will be expanded later + if foreign_api_version < 2 { + let report = "Other wallet reports unrecognized API format.".to_string(); + error!("{}", report); + return Err(Error::ClientCallback(report)); + } + + if supported_slate_versions.contains(&"V4".to_owned()) { + return Ok(SlateVersion::V4); + } + + let report = "Unable to negotiate slate format with other wallet.".to_string(); + error!("{}", report); + Err(Error::ClientCallback(report)) + } + + fn post( + &self, + url: &str, + api_secret: Option, + input: IN, + ) -> Result + where + IN: Serialize, + { + let client = if !self.use_socks { + Client::new() + } else { + Client::with_socks_proxy( + self.socks_proxy_addr + .ok_or_else(|| ClientError::Internal("No socks proxy address set".into()))?, + ) + } + .map_err(|_| ClientError::Internal("Unable to create http client".into()))?; + let req = client.create_post_request(url, api_secret, &input)?; + let res = client.send_request(req)?; + Ok(res) + } +} + +impl SlateSender for HttpSlateSender { + fn send_tx(&mut self, slate: &Slate, finalize: bool) -> Result { + let trailing = match self.base_url.ends_with('/') { + true => "", + false => "/", + }; + let url_str = format!("{}{}v2/foreign", self.base_url, trailing); + + self.launch_tor()?; + + let slate_send = match self.check_other_version(&url_str)? { + SlateVersion::V4 => VersionedSlate::into_version(slate.clone(), SlateVersion::V4)?, + }; + // Note: not using easy-jsonrpc as don't want the dependencies in this crate + let req = match finalize { + false => json!({ + "jsonrpc": "2.0", + "method": "receive_tx", + "id": 1, + "params": [ + slate_send, + null, + null + ] + }), + true => json!({ + "jsonrpc": "2.0", + "method": "finalize_tx", + "id": 1, + "params": [ + slate_send + ] + }), + }; + + trace!("Sending receive_tx request: {}", req); + + let res: String = self.post(&url_str, None, req).map_err(|e| { + let report = format!( + "Sending transaction slate to other wallet (is recipient listening?): {}", + e + ); + Error::ClientCallback(report) + })?; + + let res: Value = serde_json::from_str(&res).unwrap(); + trace!("Response: {}", res); + if res["error"] != json!(null) { + let report = format!( + "Posting transaction slate: Error: {}, Message: {}", + res["error"]["code"], res["error"]["message"] + ); + error!("{}", report); + return Err(Error::ClientCallback(report)); + } + + let slate_value = res["result"]["Ok"].clone(); + + trace!("slate_value: {}", slate_value); + let slate = Slate::deserialize_upgrade(&serde_json::to_string(&slate_value).unwrap()) + .map_err(|e| { + error!("Error deserializing response slate: {}", e); + Error::SlateDeser + })?; + + Ok(slate) + } +} + +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] +pub struct SchemeNotHttp; + +impl Into for SchemeNotHttp { + fn into(self) -> Error { + let err_str = "url scheme must be http".to_string(); + Error::GenericError(err_str) + } +} diff --git a/impls/src/adapters/mod.rs b/impls/src/adapters/mod.rs new file mode 100644 index 0000000..44fe8bf --- /dev/null +++ b/impls/src/adapters/mod.rs @@ -0,0 +1,58 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +mod file; +pub mod http; +mod slatepack; + +pub use self::file::PathToSlate; +pub use self::http::HttpSlateSender; +pub use self::slatepack::PathToSlatepack; + +use crate::config::WalletConfig; +use crate::libwallet::{Error, Slate}; +use crate::util::ZeroingString; + +/// Sends transactions to a corresponding SlateReceiver +pub trait SlateSender { + /// Send a transaction slate to another listening wallet and return result + /// TODO: Probably need a slate wrapper type + fn send_tx(&mut self, slate: &Slate, finalize: bool) -> Result; +} + +pub trait SlateReceiver { + /// Start a listener, passing received messages to the wallet api directly + /// Takes a wallet config for now to avoid needing all sorts of awkward + /// type parameters on this trait + fn listen( + &self, + config: WalletConfig, + passphrase: ZeroingString, + account: &str, + node_api_secret: Option, + ) -> Result<(), Error>; +} + +/// Posts slates to be read later by a corresponding getter +pub trait SlatePutter { + /// Send a transaction asynchronously + fn put_tx(&self, slate: &Slate, as_bin: bool) -> Result<(), Error>; +} + +/// Checks for a transaction from a corresponding SlatePutter, returns the transaction if it exists +pub trait SlateGetter { + /// Receive a transaction async. (Actually just read it from wherever and return the slate). + /// Returns (Slate, whether it was in binary form) + fn get_tx(&self) -> Result<(Slate, bool), Error>; +} diff --git a/impls/src/adapters/slatepack.rs b/impls/src/adapters/slatepack.rs new file mode 100644 index 0000000..850c3d5 --- /dev/null +++ b/impls/src/adapters/slatepack.rs @@ -0,0 +1,179 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Slatepack Output 'plugin' implementation +use std::fs::{metadata, File}; +use std::io::{Read, Write}; +use std::path::PathBuf; + +use crate::libwallet::{slatepack, Error, Slate, Slatepack, SlatepackBin, Slatepacker}; +use crate::{SlateGetter, SlatePutter}; +use grin_wallet_util::byte_ser; + +// And Slate putter impls to output to files +pub struct PathToSlatepack<'a> { + pub pathbuf: PathBuf, + pub packer: &'a Slatepacker<'a>, + pub armor_output: bool, +} + +impl<'a> PathToSlatepack<'a> { + /// Create with pathbuf and recipients + pub fn new(pathbuf: PathBuf, packer: &'a Slatepacker<'a>, armor_output: bool) -> Self { + Self { + pathbuf, + packer, + armor_output, + } + } + + pub fn get_slatepack_file_contents(&self) -> Result, Error> { + let metadata = metadata(&self.pathbuf)?; + let len = metadata.len(); + let min_len = slatepack::min_size(); + let max_len = slatepack::max_size(); + if len < min_len || len > max_len { + let msg = format!( + "Data is invalid length: {} | min: {}, max: {} |", + len, min_len, max_len + ); + return Err(Error::SlatepackDeser(msg)); + } + let mut pub_tx_f = File::open(&self.pathbuf)?; + let mut data = Vec::new(); + pub_tx_f.read_to_end(&mut data)?; + Ok(data) + } + + pub fn get_slatepack(&self, decrypt: bool) -> Result { + let data = self.get_slatepack_file_contents()?; + self.packer.deser_slatepack(&data, decrypt) + } +} + +impl<'a> SlatePutter for PathToSlatepack<'a> { + fn put_tx(&self, slate: &Slate, as_bin: bool) -> Result<(), Error> { + let slatepack = self.packer.create_slatepack(slate)?; + let mut pub_tx = File::create(&self.pathbuf)?; + if as_bin { + if self.armor_output { + let armored = self.packer.armor_slatepack(&slatepack)?; + pub_tx.write_all(armored.as_bytes())?; + } else { + pub_tx.write_all( + &byte_ser::to_bytes(&SlatepackBin(slatepack)) + .map_err(|_| Error::SlatepackSer)?, + )?; + } + } else { + pub_tx.write_all( + serde_json::to_string_pretty(&slatepack) + .map_err(|_| Error::SlateSer)? + .as_bytes(), + )?; + } + pub_tx.sync_all()?; + Ok(()) + } +} + +impl<'a> SlateGetter for PathToSlatepack<'a> { + fn get_tx(&self) -> Result<(Slate, bool), Error> { + let data = self.get_slatepack_file_contents()?; + let slatepack = self.packer.deser_slatepack(&data, true)?; + Ok((self.packer.get_slate(&slatepack)?, true)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + + use grin_core::global; + + fn clean_output_dir(test_dir: &str) { + let _ = remove_dir_all::remove_dir_all(test_dir); + } + + fn setup(test_dir: &str) { + clean_output_dir(test_dir); + } + + const SLATEPACK_DIR: &'static str = "target/test_output/slatepack"; + + #[test] + fn pathbuf_get_file_contents() { + global::set_local_chain_type(global::ChainTypes::AutomatedTesting); + setup(SLATEPACK_DIR); + + fs::create_dir_all(SLATEPACK_DIR).unwrap(); + let sp_path = PathBuf::from(SLATEPACK_DIR).join("pack_file"); + + // set Slatepack file to minimum allowable size + { + let f = File::create(sp_path.clone()).unwrap(); + f.set_len(slatepack::min_size()).unwrap(); + } + + let args = slatepack::SlatepackerArgs { + sender: None, + recipients: vec![], + dec_key: None, + }; + let packer = Slatepacker::new(args); + + let mut pack_path = PathToSlatepack::new(sp_path.clone(), &packer, true); + assert!(pack_path.get_slatepack_file_contents().is_ok()); + + pack_path = PathToSlatepack::new(sp_path.clone(), &packer, false); + assert!(pack_path.get_slatepack_file_contents().is_ok()); + + // set Slatepack file to maximum allowable size + { + let f = File::create(sp_path.clone()).unwrap(); + f.set_len(slatepack::max_size()).unwrap(); + } + + pack_path = PathToSlatepack::new(sp_path.clone(), &packer, true); + assert!(pack_path.get_slatepack_file_contents().is_ok()); + + pack_path = PathToSlatepack::new(sp_path.clone(), &packer, false); + assert!(pack_path.get_slatepack_file_contents().is_ok()); + + // set Slatepack file below minimum allowable size + { + let f = File::create(sp_path.clone()).unwrap(); + f.set_len(slatepack::min_size() - 1).unwrap(); + } + + pack_path = PathToSlatepack::new(sp_path.clone(), &packer, true); + assert!(pack_path.get_slatepack_file_contents().is_err()); + + pack_path = PathToSlatepack::new(sp_path.clone(), &packer, false); + assert!(pack_path.get_slatepack_file_contents().is_err()); + + // set Slatepack file above maximum allowable size + { + let f = File::create(sp_path.clone()).unwrap(); + f.set_len(slatepack::max_size() + 1).unwrap(); + } + + pack_path = PathToSlatepack::new(sp_path.clone(), &packer, true); + assert!(pack_path.get_slatepack_file_contents().is_err()); + + pack_path = PathToSlatepack::new(sp_path.clone(), &packer, false); + assert!(pack_path.get_slatepack_file_contents().is_err()); + } +} diff --git a/impls/src/backends/lmdb.rs b/impls/src/backends/lmdb.rs new file mode 100644 index 0000000..6a94d05 --- /dev/null +++ b/impls/src/backends/lmdb.rs @@ -0,0 +1,767 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::cell::RefCell; +use std::{fs, path}; + +// for writing stored transaction files +use std::fs::File; +use std::io::{Read, Write}; +use std::marker::PhantomData; +use std::path::Path; + +use uuid::Uuid; + +use crate::blake2::blake2b::{Blake2b, Blake2bResult}; + +use crate::keychain::{ChildNumber, ExtKeychain, Identifier, Keychain, SwitchCommitmentType}; +use crate::store::{self, option_to_not_found, to_key, to_key_u64}; + +use crate::core::core::Transaction; +use crate::core::ser; +use crate::libwallet::{ + AcctPathMapping, Context, Error, NodeClient, OutputData, ScannedBlockInfo, TxLogEntry, + WalletBackend, WalletInitStatus, WalletOutputBatch, +}; +use crate::util::secp::constants::SECRET_KEY_SIZE; +use crate::util::secp::key::SecretKey; +use crate::util::{self, secp, ToHex}; + +use rand::rngs::mock::StepRng; +use rand::thread_rng; + +pub const DB_DIR: &str = "db"; +pub const TX_SAVE_DIR: &str = "saved_txs"; + +const OUTPUT_PREFIX: u8 = b'o'; +const DERIV_PREFIX: u8 = b'd'; +const CONFIRMED_HEIGHT_PREFIX: u8 = b'c'; +const PRIVATE_TX_CONTEXT_PREFIX: u8 = b'p'; +const TX_LOG_ENTRY_PREFIX: u8 = b't'; +const TX_LOG_ID_PREFIX: u8 = b'i'; +const ACCOUNT_PATH_MAPPING_PREFIX: u8 = b'a'; +const LAST_SCANNED_BLOCK: u8 = b'l'; +const LAST_SCANNED_KEY: &str = "LAST_SCANNED_KEY"; +const WALLET_INIT_STATUS: u8 = b'w'; +const WALLET_INIT_STATUS_KEY: &str = "WALLET_INIT_STATUS"; + +/// test to see if database files exist in the current directory. If so, +/// use a DB backend for all operations +pub fn wallet_db_exists(data_file_dir: &str) -> bool { + let db_path = path::Path::new(data_file_dir).join(DB_DIR); + db_path.exists() +} + +/// Helper to derive XOR keys for storing private transaction keys in the DB +/// (blind_xor_key, nonce_xor_key) +fn private_ctx_xor_keys( + keychain: &K, + slate_id: &[u8], +) -> Result<([u8; SECRET_KEY_SIZE], [u8; SECRET_KEY_SIZE]), Error> +where + K: Keychain, +{ + let root_key = keychain.derive_key(0, &K::root_key_id(), SwitchCommitmentType::Regular)?; + + // derive XOR values for storing secret values in DB + // h(root_key|slate_id|"blind") + let mut hasher = Blake2b::new(SECRET_KEY_SIZE); + hasher.update(&root_key.0[..]); + hasher.update(&slate_id[..]); + hasher.update(&b"blind"[..]); + let blind_xor_key = hasher.finalize(); + let mut ret_blind = [0; SECRET_KEY_SIZE]; + ret_blind.copy_from_slice(&blind_xor_key.as_bytes()[0..SECRET_KEY_SIZE]); + + // h(root_key|slate_id|"nonce") + let mut hasher = Blake2b::new(SECRET_KEY_SIZE); + hasher.update(&root_key.0[..]); + hasher.update(&slate_id[..]); + hasher.update(&b"nonce"[..]); + let nonce_xor_key = hasher.finalize(); + let mut ret_nonce = [0; SECRET_KEY_SIZE]; + ret_nonce.copy_from_slice(&nonce_xor_key.as_bytes()[0..SECRET_KEY_SIZE]); + + Ok((ret_blind, ret_nonce)) +} + +pub struct LMDBBackend<'ck, C, K> +where + C: NodeClient + 'ck, + K: Keychain + 'ck, +{ + db: store::Store, + data_file_dir: String, + /// Keychain + pub keychain: Option, + /// Check value for XORed keychain seed + pub master_checksum: Box>, + /// Parent path to use by default for output operations + parent_key_id: Identifier, + /// wallet to node client + w2n_client: C, + ///phantom + _phantom: &'ck PhantomData, +} + +impl<'ck, C, K> LMDBBackend<'ck, C, K> +where + C: NodeClient + 'ck, + K: Keychain + 'ck, +{ + pub fn new(data_file_dir: &str, n_client: C) -> Result { + let db_path = path::Path::new(data_file_dir).join(DB_DIR); + fs::create_dir_all(&db_path).expect("Couldn't create wallet backend directory!"); + + let stored_tx_path = path::Path::new(data_file_dir).join(TX_SAVE_DIR); + fs::create_dir_all(&stored_tx_path) + .expect("Couldn't create wallet backend tx storage directory!"); + + let store = store::Store::new(db_path.to_str().unwrap(), None, Some(DB_DIR), None)?; + + // Make sure default wallet derivation path always exists + // as well as path (so it can be retrieved by batches to know where to store + // completed transactions, for reference + let default_account = AcctPathMapping { + label: "default".to_owned(), + path: LMDBBackend::::default_path(), + }; + let acct_key = to_key( + ACCOUNT_PATH_MAPPING_PREFIX, + &mut default_account.label.as_bytes().to_vec(), + ); + + { + let batch = store.batch()?; + batch.put_ser(&acct_key, &default_account)?; + batch.commit()?; + } + + let res = LMDBBackend { + db: store, + data_file_dir: data_file_dir.to_owned(), + keychain: None, + master_checksum: Box::new(None), + parent_key_id: LMDBBackend::::default_path(), + w2n_client: n_client, + _phantom: &PhantomData, + }; + Ok(res) + } + + fn default_path() -> Identifier { + // return the default parent wallet path, corresponding to the default account + // in the BIP32 spec. Parent is account 0 at level 2, child output identifiers + // are all at level 3 + ExtKeychain::derive_key_id(2, 0, 0, 0, 0) + } + + /// Just test to see if database files exist in the current directory. If + /// so, use a DB backend for all operations + pub fn exists(data_file_dir: &str) -> bool { + let db_path = path::Path::new(data_file_dir).join(DB_DIR); + db_path.exists() + } +} + +impl<'ck, C, K> WalletBackend<'ck, C, K> for LMDBBackend<'ck, C, K> +where + C: NodeClient + 'ck, + K: Keychain + 'ck, +{ + /// Set the keychain, which should already have been opened + fn set_keychain( + &mut self, + mut k: Box, + mask: bool, + use_test_rng: bool, + ) -> Result, Error> { + // store hash of master key, so it can be verified later after unmasking + let root_key = k.derive_key(0, &K::root_key_id(), SwitchCommitmentType::Regular)?; + let mut hasher = Blake2b::new(SECRET_KEY_SIZE); + hasher.update(&root_key.0[..]); + self.master_checksum = Box::new(Some(hasher.finalize())); + + let mask_value = { + match mask { + true => { + // Random value that must be XORed against the stored wallet seed + // before it is used + let mask_value = match use_test_rng { + true => { + let mut test_rng = StepRng::new(1_234_567_890_u64, 1); + secp::key::SecretKey::new(&k.secp(), &mut test_rng) + } + false => secp::key::SecretKey::new(&k.secp(), &mut thread_rng()), + }; + k.mask_master_key(&mask_value)?; + Some(mask_value) + } + false => None, + } + }; + + self.keychain = Some(*k); + Ok(mask_value) + } + + /// Close wallet + fn close(&mut self) -> Result<(), Error> { + self.keychain = None; + Ok(()) + } + + /// Return the keychain being used, cloned with XORed token value + /// for temporary use + fn keychain(&self, mask: Option<&SecretKey>) -> Result { + match self.keychain.as_ref() { + Some(k) => { + let mut k_masked = k.clone(); + if let Some(m) = mask { + k_masked.mask_master_key(m)?; + } + // Check if master seed is what is expected (especially if it's been xored) + let root_key = + k_masked.derive_key(0, &K::root_key_id(), SwitchCommitmentType::Regular)?; + let mut hasher = Blake2b::new(SECRET_KEY_SIZE); + hasher.update(&root_key.0[..]); + if *self.master_checksum != Some(hasher.finalize()) { + error!("Supplied keychain mask is invalid"); + return Err(Error::InvalidKeychainMask); + } + Ok(k_masked) + } + None => Err(Error::KeychainDoesntExist), + } + } + + /// Return the node client being used + fn w2n_client(&mut self) -> &mut C { + &mut self.w2n_client + } + + /// return the version of the commit for caching + fn calc_commit_for_cache( + &mut self, + keychain_mask: Option<&SecretKey>, + amount: u64, + id: &Identifier, + ) -> Result, Error> { + //TODO: Check if this is really necessary, it's the only thing + //preventing removing the need for config in the wallet backend + /*if self.config.no_commit_cache == Some(true) { + Ok(None) + } else {*/ + Ok(Some( + self.keychain(keychain_mask)? + .commit(amount, &id, SwitchCommitmentType::Regular)? + .0 + .to_vec() + .to_hex(), // TODO: proper support for different switch commitment schemes + )) + /*}*/ + } + + /// Set parent path by account name + fn set_parent_key_id_by_name(&mut self, label: &str) -> Result<(), Error> { + let label = label.to_owned(); + let res = self.acct_path_iter().find(|l| l.label == label); + if let Some(a) = res { + self.set_parent_key_id(a.path); + Ok(()) + } else { + Err(Error::UnknownAccountLabel(label)) + } + } + + /// set parent path + fn set_parent_key_id(&mut self, id: Identifier) { + self.parent_key_id = id; + } + + fn parent_key_id(&mut self) -> Identifier { + self.parent_key_id.clone() + } + + fn get(&self, id: &Identifier, mmr_index: &Option) -> Result { + let key = match mmr_index { + Some(i) => to_key_u64(OUTPUT_PREFIX, &mut id.to_bytes().to_vec(), *i), + None => to_key(OUTPUT_PREFIX, &mut id.to_bytes().to_vec()), + }; + option_to_not_found(self.db.get_ser(&key, None), || format!("Key Id: {}", id)) + .map_err(|e| e.into()) + } + + // TODO - fix this awkward conversion between PrefixIterator and our Box + fn iter<'a>(&'a self) -> Box + 'a> { + let protocol_version = self.db.protocol_version(); + let prefix_iter = self.db.iter(&[OUTPUT_PREFIX], move |_, mut v| { + ser::deserialize( + &mut v, + protocol_version, + ser::DeserializationMode::default(), + ) + .map_err(From::from) + }); + let iter = prefix_iter.expect("deserialize").into_iter(); + Box::new(iter) + } + + fn get_tx_log_entry(&self, u: &Uuid) -> Result, Error> { + let key = to_key(TX_LOG_ENTRY_PREFIX, &mut u.as_bytes().to_vec()); + self.db.get_ser(&key, None).map_err(|e| e.into()) + } + + // TODO - fix this awkward conversion between PrefixIterator and our Box + fn tx_log_iter<'a>(&'a self) -> Box + 'a> { + let protocol_version = self.db.protocol_version(); + let prefix_iter = self.db.iter(&[TX_LOG_ENTRY_PREFIX], move |_, mut v| { + ser::deserialize( + &mut v, + protocol_version, + ser::DeserializationMode::default(), + ) + .map_err(From::from) + }); + let iter = prefix_iter.expect("deserialize").into_iter(); + Box::new(iter) + } + + fn get_private_context( + &mut self, + keychain_mask: Option<&SecretKey>, + slate_id: &[u8], + ) -> Result { + let ctx_key = to_key_u64(PRIVATE_TX_CONTEXT_PREFIX, &mut slate_id.to_vec(), 0); + let (blind_xor_key, nonce_xor_key) = + private_ctx_xor_keys(&self.keychain(keychain_mask)?, slate_id)?; + + let mut ctx: Context = option_to_not_found(self.db.get_ser(&ctx_key, None), || { + format!("Slate id: {:x?}", slate_id.to_vec()) + })?; + + for i in 0..SECRET_KEY_SIZE { + ctx.sec_key.0[i] ^= blind_xor_key[i]; + ctx.sec_nonce.0[i] ^= nonce_xor_key[i]; + } + + Ok(ctx) + } + + // TODO - fix this awkward conversion between PrefixIterator and our Box + fn acct_path_iter<'a>(&'a self) -> Box + 'a> { + let protocol_version = self.db.protocol_version(); + let prefix_iter = self + .db + .iter(&[ACCOUNT_PATH_MAPPING_PREFIX], move |_, mut v| { + ser::deserialize( + &mut v, + protocol_version, + ser::DeserializationMode::default(), + ) + .map_err(From::from) + }); + let iter = prefix_iter.expect("deserialize").into_iter(); + Box::new(iter) + } + + fn get_acct_path(&self, label: String) -> Result, Error> { + let acct_key = to_key(ACCOUNT_PATH_MAPPING_PREFIX, &mut label.as_bytes().to_vec()); + self.db.get_ser(&acct_key, None).map_err(|e| e.into()) + } + + fn store_tx(&self, uuid: &str, tx: &Transaction) -> Result<(), Error> { + let filename = format!("{}.grintx", uuid); + let path = path::Path::new(&self.data_file_dir) + .join(TX_SAVE_DIR) + .join(filename); + let path_buf = Path::new(&path).to_path_buf(); + let mut stored_tx = File::create(path_buf)?; + let tx_hex = ser::ser_vec(tx, ser::ProtocolVersion(1)).unwrap().to_hex(); + stored_tx.write_all(&tx_hex.as_bytes())?; + stored_tx.sync_all()?; + Ok(()) + } + + fn get_stored_tx(&self, uuid: &str) -> Result, Error> { + let filename = format!("{}.grintx", uuid); + let path = path::Path::new(&self.data_file_dir) + .join(TX_SAVE_DIR) + .join(filename); + let tx_file = Path::new(&path).to_path_buf(); + let mut tx_f = File::open(tx_file)?; + let mut content = String::new(); + tx_f.read_to_string(&mut content)?; + let tx_bin = util::from_hex(&content).unwrap(); + Ok(Some( + ser::deserialize( + &mut &tx_bin[..], + ser::ProtocolVersion(1), + ser::DeserializationMode::default(), + ) + .unwrap(), + )) + } + + fn batch<'a>( + &'a mut self, + keychain_mask: Option<&SecretKey>, + ) -> Result + 'a>, Error> { + Ok(Box::new(Batch { + _store: self, + db: RefCell::new(Some(self.db.batch()?)), + keychain: Some(self.keychain(keychain_mask)?), + })) + } + + fn batch_no_mask<'a>(&'a mut self) -> Result + 'a>, Error> { + Ok(Box::new(Batch { + _store: self, + db: RefCell::new(Some(self.db.batch()?)), + keychain: None, + })) + } + + fn current_child_index<'a>(&mut self, parent_key_id: &Identifier) -> Result { + let index = { + let batch = self.db.batch()?; + let deriv_key = to_key(DERIV_PREFIX, &mut parent_key_id.to_bytes().to_vec()); + match batch.get_ser(&deriv_key, None)? { + Some(idx) => idx, + None => 0, + } + }; + Ok(index) + } + + fn next_child<'a>(&mut self, keychain_mask: Option<&SecretKey>) -> Result { + let parent_key_id = self.parent_key_id.clone(); + let mut deriv_idx = { + let batch = self.db.batch()?; + let deriv_key = to_key(DERIV_PREFIX, &mut self.parent_key_id.to_bytes().to_vec()); + match batch.get_ser(&deriv_key, None)? { + Some(idx) => idx, + None => 0, + } + }; + let mut return_path = self.parent_key_id.to_path(); + return_path.depth += 1; + return_path.path[return_path.depth as usize - 1] = ChildNumber::from(deriv_idx); + deriv_idx += 1; + let mut batch = self.batch(keychain_mask)?; + batch.save_child_index(&parent_key_id, deriv_idx)?; + batch.commit()?; + Ok(Identifier::from_path(&return_path)) + } + + fn last_confirmed_height<'a>(&mut self) -> Result { + let batch = self.db.batch()?; + let height_key = to_key( + CONFIRMED_HEIGHT_PREFIX, + &mut self.parent_key_id.to_bytes().to_vec(), + ); + let last_confirmed_height = match batch.get_ser(&height_key, None)? { + Some(h) => h, + None => 0, + }; + Ok(last_confirmed_height) + } + + fn last_scanned_block<'a>(&mut self) -> Result { + let batch = self.db.batch()?; + let scanned_block_key = to_key( + LAST_SCANNED_BLOCK, + &mut LAST_SCANNED_KEY.as_bytes().to_vec(), + ); + let last_scanned_block = match batch.get_ser(&scanned_block_key, None)? { + Some(b) => b, + None => ScannedBlockInfo { + height: 0, + hash: "".to_owned(), + start_pmmr_index: 0, + last_pmmr_index: 0, + }, + }; + Ok(last_scanned_block) + } + + fn init_status<'a>(&mut self) -> Result { + let batch = self.db.batch()?; + let init_status_key = to_key( + WALLET_INIT_STATUS, + &mut WALLET_INIT_STATUS_KEY.as_bytes().to_vec(), + ); + let status = match batch.get_ser(&init_status_key, None)? { + Some(s) => s, + None => WalletInitStatus::InitComplete, + }; + Ok(status) + } +} + +/// An atomic batch in which all changes can be committed all at once or +/// discarded on error. +pub struct Batch<'a, C, K> +where + C: NodeClient, + K: Keychain, +{ + _store: &'a LMDBBackend<'a, C, K>, + db: RefCell>>, + /// Keychain + keychain: Option, +} + +#[allow(missing_docs)] +impl<'a, C, K> WalletOutputBatch for Batch<'a, C, K> +where + C: NodeClient, + K: Keychain, +{ + fn keychain(&mut self) -> &mut K { + self.keychain.as_mut().unwrap() + } + + fn save(&mut self, out: OutputData) -> Result<(), Error> { + // Save the output data to the db. + { + let key = match out.mmr_index { + Some(i) => to_key_u64(OUTPUT_PREFIX, &mut out.key_id.to_bytes().to_vec(), i), + None => to_key(OUTPUT_PREFIX, &mut out.key_id.to_bytes().to_vec()), + }; + self.db.borrow().as_ref().unwrap().put_ser(&key, &out)?; + } + + Ok(()) + } + + fn get(&self, id: &Identifier, mmr_index: &Option) -> Result { + let key = match mmr_index { + Some(i) => to_key_u64(OUTPUT_PREFIX, &mut id.to_bytes().to_vec(), *i), + None => to_key(OUTPUT_PREFIX, &mut id.to_bytes().to_vec()), + }; + option_to_not_found( + self.db.borrow().as_ref().unwrap().get_ser(&key, None), + || format!("Key ID: {}", id), + ) + .map_err(|e| e.into()) + } + + // TODO - fix this awkward conversion between PrefixIterator and our Box + fn iter(&self) -> Box> { + let db = self.db.borrow(); + let db = db.as_ref().unwrap(); + let protocol_version = db.protocol_version(); + let prefix_iter = db.iter(&[OUTPUT_PREFIX], move |_, mut v| { + ser::deserialize( + &mut v, + protocol_version, + ser::DeserializationMode::default(), + ) + .map_err(From::from) + }); + let iter = prefix_iter.expect("deserialize").into_iter(); + Box::new(iter) + } + + fn delete(&mut self, id: &Identifier, mmr_index: &Option) -> Result<(), Error> { + // Delete the output data. + { + let key = match mmr_index { + Some(i) => to_key_u64(OUTPUT_PREFIX, &mut id.to_bytes().to_vec(), *i), + None => to_key(OUTPUT_PREFIX, &mut id.to_bytes().to_vec()), + }; + let _ = self.db.borrow().as_ref().unwrap().delete(&key); + } + + Ok(()) + } + + fn next_tx_log_id(&mut self, parent_key_id: &Identifier) -> Result { + let tx_id_key = to_key(TX_LOG_ID_PREFIX, &mut parent_key_id.to_bytes().to_vec()); + let last_tx_log_id = match self + .db + .borrow() + .as_ref() + .unwrap() + .get_ser(&tx_id_key, None)? + { + Some(t) => t, + None => 0, + }; + self.db + .borrow() + .as_ref() + .unwrap() + .put_ser(&tx_id_key, &(last_tx_log_id + 1))?; + Ok(last_tx_log_id) + } + + // TODO - fix this awkward conversion between PrefixIterator and our Box + fn tx_log_iter(&self) -> Box> { + let db = self.db.borrow(); + let db = db.as_ref().unwrap(); + let protocol_version = db.protocol_version(); + let prefix_iter = db.iter(&[TX_LOG_ENTRY_PREFIX], move |_, mut v| { + ser::deserialize( + &mut v, + protocol_version, + ser::DeserializationMode::default(), + ) + .map_err(From::from) + }); + let iter = prefix_iter.expect("deserialize").into_iter(); + Box::new(iter) + } + + fn save_last_confirmed_height( + &mut self, + parent_key_id: &Identifier, + height: u64, + ) -> Result<(), Error> { + let height_key = to_key( + CONFIRMED_HEIGHT_PREFIX, + &mut parent_key_id.to_bytes().to_vec(), + ); + self.db + .borrow() + .as_ref() + .unwrap() + .put_ser(&height_key, &height)?; + Ok(()) + } + + fn save_last_scanned_block(&mut self, block_info: ScannedBlockInfo) -> Result<(), Error> { + let pmmr_index_key = to_key( + LAST_SCANNED_BLOCK, + &mut LAST_SCANNED_KEY.as_bytes().to_vec(), + ); + self.db + .borrow() + .as_ref() + .unwrap() + .put_ser(&pmmr_index_key, &block_info)?; + Ok(()) + } + + fn save_init_status(&mut self, value: WalletInitStatus) -> Result<(), Error> { + let init_status_key = to_key( + WALLET_INIT_STATUS, + &mut WALLET_INIT_STATUS_KEY.as_bytes().to_vec(), + ); + self.db + .borrow() + .as_ref() + .unwrap() + .put_ser(&init_status_key, &value)?; + Ok(()) + } + + fn save_child_index(&mut self, parent_id: &Identifier, child_n: u32) -> Result<(), Error> { + let deriv_key = to_key(DERIV_PREFIX, &mut parent_id.to_bytes().to_vec()); + self.db + .borrow() + .as_ref() + .unwrap() + .put_ser(&deriv_key, &child_n)?; + Ok(()) + } + + fn save_tx_log_entry( + &mut self, + tx_in: TxLogEntry, + parent_id: &Identifier, + ) -> Result<(), Error> { + let tx_log_key = to_key_u64( + TX_LOG_ENTRY_PREFIX, + &mut parent_id.to_bytes().to_vec(), + tx_in.id as u64, + ); + self.db + .borrow() + .as_ref() + .unwrap() + .put_ser(&tx_log_key, &tx_in)?; + Ok(()) + } + + fn save_acct_path(&mut self, mapping: AcctPathMapping) -> Result<(), Error> { + let acct_key = to_key( + ACCOUNT_PATH_MAPPING_PREFIX, + &mut mapping.label.as_bytes().to_vec(), + ); + self.db + .borrow() + .as_ref() + .unwrap() + .put_ser(&acct_key, &mapping)?; + Ok(()) + } + + // TODO - fix this awkward conversion between PrefixIterator and our Box + fn acct_path_iter(&self) -> Box> { + let db = self.db.borrow(); + let db = db.as_ref().unwrap(); + let protocol_version = db.protocol_version(); + let prefix_iter = db.iter(&[ACCOUNT_PATH_MAPPING_PREFIX], move |_, mut v| { + ser::deserialize( + &mut v, + protocol_version, + ser::DeserializationMode::default(), + ) + .map_err(From::from) + }); + let iter = prefix_iter.expect("deserialize").into_iter(); + Box::new(iter) + } + + fn lock_output(&mut self, out: &mut OutputData) -> Result<(), Error> { + out.lock(); + self.save(out.clone()) + } + + fn save_private_context(&mut self, slate_id: &[u8], ctx: &Context) -> Result<(), Error> { + let ctx_key = to_key_u64(PRIVATE_TX_CONTEXT_PREFIX, &mut slate_id.to_vec(), 0); + let (blind_xor_key, nonce_xor_key) = private_ctx_xor_keys(self.keychain(), slate_id)?; + + let mut s_ctx = ctx.clone(); + for i in 0..SECRET_KEY_SIZE { + s_ctx.sec_key.0[i] ^= blind_xor_key[i]; + s_ctx.sec_nonce.0[i] ^= nonce_xor_key[i]; + } + + self.db + .borrow() + .as_ref() + .unwrap() + .put_ser(&ctx_key, &s_ctx)?; + Ok(()) + } + + fn delete_private_context(&mut self, slate_id: &[u8]) -> Result<(), Error> { + let ctx_key = to_key_u64(PRIVATE_TX_CONTEXT_PREFIX, &mut slate_id.to_vec(), 0); + self.db + .borrow() + .as_ref() + .unwrap() + .delete(&ctx_key) + .map_err(|e| e.into()) + } + + fn commit(&self) -> Result<(), Error> { + let db = self.db.replace(None); + db.unwrap().commit()?; + Ok(()) + } +} diff --git a/impls/src/backends/mod.rs b/impls/src/backends/mod.rs new file mode 100644 index 0000000..56b5df6 --- /dev/null +++ b/impls/src/backends/mod.rs @@ -0,0 +1,17 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +mod lmdb; + +pub use self::lmdb::{wallet_db_exists, LMDBBackend}; diff --git a/impls/src/client_utils/client.rs b/impls/src/client_utils/client.rs new file mode 100644 index 0000000..47f16fb --- /dev/null +++ b/impls/src/client_utils/client.rs @@ -0,0 +1,289 @@ +// Copyright 2018 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! High level JSON/HTTP client API + +use crate::util::to_base64; +use lazy_static::lazy_static; +use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, AUTHORIZATION, CONTENT_TYPE, USER_AGENT}; +use reqwest::{ClientBuilder, Method, Proxy, RequestBuilder}; +use serde::{Deserialize, Serialize}; +use serde_json; +use std::net::SocketAddr; +use std::sync::{Arc, Mutex}; +use std::time::Duration; +use tokio::runtime::{Builder, Handle, Runtime}; + +// Global Tokio runtime. +// Needs a `Mutex` because `Runtime::block_on` requires mutable access. +// Tokio v0.3 requires immutable self, but we are waiting on upstream +// updates before we can upgrade. +// See: https://github.com/seanmonstar/reqwest/pull/1076 +lazy_static! { + pub static ref RUNTIME: Arc> = Arc::new(Mutex::new( + Builder::new() + .threaded_scheduler() + .enable_all() + .build() + .unwrap() + )); +} + +#[derive(Clone, Eq, thiserror::Error, PartialEq, Debug)] +pub enum Error { + #[error("Internal error: {0}")] + Internal(String), + #[error("Bad arguments: {0}")] + _Argument(String), + #[error("Not found.")] + _NotFound, + #[error("Request error: {0}")] + RequestError(String), + #[error("ResponseError error: {0}")] + ResponseError(String), +} + +#[derive(Clone)] +pub struct Client { + client: reqwest::Client, +} + +impl Client { + /// New client + pub fn new() -> Result { + Self::build(None) + } + + pub fn with_socks_proxy(socks_proxy_addr: SocketAddr) -> Result { + Self::build(Some(socks_proxy_addr)) + } + + fn build(socks_proxy_addr: Option) -> Result { + let mut headers = HeaderMap::new(); + headers.insert(USER_AGENT, HeaderValue::from_static("grin-client")); + headers.insert(ACCEPT, HeaderValue::from_static("application/json")); + headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + + let mut builder = ClientBuilder::new() + .timeout(Duration::from_secs(20)) + .use_rustls_tls() + .default_headers(headers); + + if let Some(s) = socks_proxy_addr { + let proxy = Proxy::all(&format!("socks5h://{}:{}", s.ip(), s.port())) + .map_err(|e| Error::Internal(format!("Unable to create proxy: {}", e)))?; + builder = builder.proxy(proxy); + } + + let client = builder + .build() + .map_err(|e| Error::Internal(format!("Unable to build client: {}", e)))?; + + Ok(Client { client }) + } + + /// Helper function to easily issue a HTTP GET request against a given URL that + /// returns a JSON object. Handles request building, JSON deserialization and + /// response code checking. + pub fn _get<'a, T>(&self, url: &'a str, api_secret: Option) -> Result + where + for<'de> T: Deserialize<'de>, + { + self.handle_request(self.build_request(url, Method::GET, api_secret, None)?) + } + + /// Helper function to easily issue an async HTTP GET request against a given + /// URL that returns a future. Handles request building, JSON deserialization + /// and response code checking. + pub async fn _get_async<'a, T>( + &self, + url: &'a str, + api_secret: Option, + ) -> Result + where + for<'de> T: Deserialize<'de> + Send + 'static, + { + self.handle_request_async(self.build_request(url, Method::GET, api_secret, None)?) + .await + } + + /// Helper function to easily issue a HTTP GET request + /// on a given URL that returns nothing. Handles request + /// building and response code checking. + pub fn _get_no_ret(&self, url: &str, api_secret: Option) -> Result<(), Error> { + let req = self.build_request(url, Method::GET, api_secret, None)?; + self.send_request(req)?; + Ok(()) + } + + /// Helper function to easily issue a HTTP POST request with the provided JSON + /// object as body on a given URL that returns a JSON object. Handles request + /// building, JSON serialization and deserialization, and response code + /// checking. + pub fn post( + &self, + url: &str, + api_secret: Option, + input: &IN, + ) -> Result + where + IN: Serialize, + for<'de> OUT: Deserialize<'de>, + { + let req = self.create_post_request(url, api_secret, input)?; + self.handle_request(req) + } + + /// Helper function to easily issue an async HTTP POST request with the + /// provided JSON object as body on a given URL that returns a future. Handles + /// request building, JSON serialization and deserialization, and response code + /// checking. + pub async fn post_async( + &self, + url: &str, + input: &IN, + api_secret: Option, + ) -> Result + where + IN: Serialize, + OUT: Send + 'static, + for<'de> OUT: Deserialize<'de>, + { + self.handle_request_async(self.create_post_request(url, api_secret, input)?) + .await + } + + /// Helper function to easily issue a HTTP POST request with the provided JSON + /// object as body on a given URL that returns nothing. Handles request + /// building, JSON serialization, and response code + /// checking. + pub fn _post_no_ret( + &self, + url: &str, + api_secret: Option, + input: &IN, + ) -> Result<(), Error> + where + IN: Serialize, + { + let req = self.create_post_request(url, api_secret, input)?; + self.send_request(req)?; + Ok(()) + } + + /// Helper function to easily issue an async HTTP POST request with the + /// provided JSON object as body on a given URL that returns a future. Handles + /// request building, JSON serialization and deserialization, and response code + /// checking. + pub async fn _post_no_ret_async( + &self, + url: &str, + api_secret: Option, + input: &IN, + ) -> Result<(), Error> + where + IN: Serialize, + { + self.send_request_async(self.create_post_request(url, api_secret, input)?) + .await?; + Ok(()) + } + + fn build_request( + &self, + url: &str, + method: Method, + api_secret: Option, + body: Option, + ) -> Result { + let mut builder = self.client.request(method, url); + if let Some(api_secret) = api_secret { + let basic_auth = format!("Basic {}", to_base64(&format!("grin:{}", api_secret))); + builder = builder.header(AUTHORIZATION, basic_auth); + } + + if let Some(body) = body { + builder = builder.body(body); + } + + Ok(builder) + } + + pub fn create_post_request( + &self, + url: &str, + api_secret: Option, + input: &IN, + ) -> Result + where + IN: Serialize, + { + let json = serde_json::to_string(input) + .map_err(|_| Error::Internal("Could not serialize data to JSON".to_owned()))?; + self.build_request(url, Method::POST, api_secret, Some(json)) + } + + fn handle_request(&self, req: RequestBuilder) -> Result + where + for<'de> T: Deserialize<'de>, + { + let data = self.send_request(req)?; + serde_json::from_str(&data) + .map_err(|_| Error::ResponseError("Cannot parse response".to_owned())) + } + + async fn handle_request_async(&self, req: RequestBuilder) -> Result + where + for<'de> T: Deserialize<'de> + Send + 'static, + { + let data = self.send_request_async(req).await?; + let ser = serde_json::from_str(&data) + .map_err(|_| Error::ResponseError("Cannot parse response".to_owned()))?; + Ok(ser) + } + + async fn send_request_async(&self, req: RequestBuilder) -> Result { + let resp = req + .send() + .await + .map_err(|e| Error::RequestError(format!("Cannot make request: {}", e)))?; + let text = resp + .text() + .await + .map_err(|e| Error::ResponseError(format!("Cannot parse response: {}", e)))?; + Ok(text) + } + + pub fn send_request(&self, req: RequestBuilder) -> Result { + // This client is currently used both outside and inside of a tokio runtime + // context. In the latter case we are not allowed to do a blocking call to + // our global runtime, which unfortunately means we have to spawn a new thread + if Handle::try_current().is_ok() { + let rt = RUNTIME.clone(); + let client = self.clone(); + std::thread::spawn(move || { + rt.lock() + .unwrap() + .block_on(async { client.send_request_async(req).await }) + }) + .join() + .unwrap() + } else { + RUNTIME + .lock() + .unwrap() + .block_on(self.send_request_async(req)) + } + } +} diff --git a/impls/src/client_utils/json_rpc.rs b/impls/src/client_utils/json_rpc.rs new file mode 100644 index 0000000..06e4465 --- /dev/null +++ b/impls/src/client_utils/json_rpc.rs @@ -0,0 +1,264 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// Derived from https://github.com/apoelstra/rust-jsonrpc + +//! JSON RPC Client functionality +use std::{error, fmt}; + +use serde_json; + +/// Builds a request +pub fn build_request<'a, 'b>(name: &'a str, params: &'b serde_json::Value) -> Request<'a, 'b> { + Request { + method: name, + params: params, + id: From::from(1), + jsonrpc: Some("2.0"), + } +} + +#[derive(Debug, Clone, PartialEq, Serialize)] +/// A JSONRPC request object +pub struct Request<'a, 'b> { + /// The name of the RPC call + pub method: &'a str, + /// Parameters to the RPC call + pub params: &'b serde_json::Value, + /// Identifier for this Request, which should appear in the response + pub id: serde_json::Value, + /// jsonrpc field, MUST be "2.0" + pub jsonrpc: Option<&'a str>, +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +/// A JSONRPC response object +pub struct Response { + /// A result if there is one, or null + pub result: Option, + /// An error if there is one, or null + pub error: Option, + /// Identifier for this Request, which should match that of the request + pub id: serde_json::Value, + /// jsonrpc field, MUST be "2.0" + pub jsonrpc: Option, +} + +impl Response { + /// Extract the result from a response + pub fn result(&self) -> Result { + if let Some(ref e) = self.error { + return Err(Error::Rpc(e.clone())); + } + + let result = match self.result.clone() { + Some(r) => serde_json::from_value(r["Ok"].clone()).map_err(Error::Json), + None => serde_json::from_value(serde_json::Value::Null).map_err(Error::Json), + }?; + Ok(result) + } + + /// Extract the result from a response, consuming the response + pub fn into_result(self) -> Result { + if let Some(e) = self.error { + return Err(Error::Rpc(e)); + } + self.result() + } + + /// Return the RPC error, if there was one, but do not check the result + pub fn _check_error(self) -> Result<(), Error> { + if let Some(e) = self.error { + Err(Error::Rpc(e)) + } else { + Ok(()) + } + } + + /// Returns whether or not the `result` field is empty + pub fn _is_none(&self) -> bool { + self.result.is_none() + } +} + +/// A library error +#[derive(Debug)] +pub enum Error { + /// Json error + Json(serde_json::Error), + /// Error response + Rpc(RpcError), + /// Response to a request did not have the expected nonce + _NonceMismatch, + /// Response to a request had a jsonrpc field other than "2.0" + _VersionMismatch, + /// Batches can't be empty + _EmptyBatch, + /// Too many responses returned in batch + _WrongBatchResponseSize, + /// Batch response contained a duplicate ID + _BatchDuplicateResponseId(serde_json::Value), + /// Batch response contained an ID that didn't correspond to any request ID + _WrongBatchResponseId(serde_json::Value), +} + +impl From for Error { + fn from(e: serde_json::Error) -> Error { + Error::Json(e) + } +} + +impl From for Error { + fn from(e: RpcError) -> Error { + Error::Rpc(e) + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + Error::Json(ref e) => write!(f, "JSON decode error: {}", e), + Error::Rpc(ref r) => write!(f, "RPC error response: {:?}", r), + Error::_BatchDuplicateResponseId(ref v) => { + write!(f, "duplicate RPC batch response ID: {}", v) + } + Error::_WrongBatchResponseId(ref v) => write!(f, "wrong RPC batch response ID: {}", v), + ref e => f.write_str(&format!("{}", e)), + } + } +} + +impl std::error::Error for Error { + fn description(&self) -> &str { + match *self { + Error::Json(_) => "JSON decode error", + Error::Rpc(_) => "RPC error response", + Error::_NonceMismatch => "Nonce of response did not match nonce of request", + Error::_VersionMismatch => "`jsonrpc` field set to non-\"2.0\"", + Error::_EmptyBatch => "batches can't be empty", + Error::_WrongBatchResponseSize => "too many responses returned in batch", + Error::_BatchDuplicateResponseId(_) => "batch response contained a duplicate ID", + Error::_WrongBatchResponseId(_) => { + "batch response contained an ID that didn't correspond to any request ID" + } + } + } + + fn cause(&self) -> Option<&dyn error::Error> { + match *self { + Error::Json(ref e) => Some(e), + _ => None, + } + } +} + +/// Standard error responses, as described at at +/// http://www.jsonrpc.org/specification#error_object +/// +/// # Documentation Copyright +/// Copyright (C) 2007-2010 by the JSON-RPC Working Group +/// +/// This document and translations of it may be used to implement JSON-RPC, it +/// may be copied and furnished to others, and derivative works that comment +/// on or otherwise explain it or assist in its implementation may be prepared, +/// copied, published and distributed, in whole or in part, without restriction +/// of any kind, provided that the above copyright notice and this paragraph +/// are included on all such copies and derivative works. However, this document +/// itself may not be modified in any way. +/// +/// The limited permissions granted above are perpetual and will not be revoked. +/// +/// This document and the information contained herein is provided "AS IS" and +/// ALL WARRANTIES, EXPRESS OR IMPLIED are DISCLAIMED, INCLUDING BUT NOT LIMITED +/// TO ANY WARRANTY THAT THE USE OF THE INFORMATION HEREIN WILL NOT INFRINGE ANY +/// RIGHTS OR ANY IMPLIED WARRANTIES OF MERCHANTABILITY OR FITNESS FOR A +/// PARTICULAR PURPOSE. +/// +#[allow(dead_code)] +#[derive(Debug)] +pub enum StandardError { + /// Invalid JSON was received by the server. + /// An error occurred on the server while parsing the JSON text. + ParseError, + /// The JSON sent is not a valid Request object. + InvalidRequest, + /// The method does not exist / is not available. + MethodNotFound, + /// Invalid method parameter(s). + InvalidParams, + /// Internal JSON-RPC error. + InternalError, +} + +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +/// A JSONRPC error object +pub struct RpcError { + /// The integer identifier of the error + pub code: i32, + /// A string describing the error + pub message: String, + /// Additional data specific to the error + pub data: Option, +} + +/// Create a standard error responses +pub fn _standard_error(code: StandardError, data: Option) -> RpcError { + match code { + StandardError::ParseError => RpcError { + code: -32700, + message: "Parse error".to_string(), + data: data, + }, + StandardError::InvalidRequest => RpcError { + code: -32600, + message: "Invalid Request".to_string(), + data: data, + }, + StandardError::MethodNotFound => RpcError { + code: -32601, + message: "Method not found".to_string(), + data: data, + }, + StandardError::InvalidParams => RpcError { + code: -32602, + message: "Invalid params".to_string(), + data: data, + }, + StandardError::InternalError => RpcError { + code: -32603, + message: "Internal error".to_string(), + data: data, + }, + } +} + +/// Converts a Rust `Result` to a JSONRPC response object +pub fn _result_to_response( + result: Result, + id: serde_json::Value, +) -> Response { + match result { + Ok(data) => Response { + result: Some(data), + error: None, + id: id, + jsonrpc: Some(String::from("2.0")), + }, + Err(err) => Response { + result: None, + error: Some(err), + id: id, + jsonrpc: Some(String::from("2.0")), + }, + } +} diff --git a/impls/src/client_utils/mod.rs b/impls/src/client_utils/mod.rs new file mode 100644 index 0000000..1e6777d --- /dev/null +++ b/impls/src/client_utils/mod.rs @@ -0,0 +1,18 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +mod client; +pub mod json_rpc; + +pub use client::{Client, Error as ClientError, RUNTIME}; diff --git a/impls/src/error.rs b/impls/src/error.rs new file mode 100644 index 0000000..4169e68 --- /dev/null +++ b/impls/src/error.rs @@ -0,0 +1,104 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Implementation specific error types +use crate::core::libtx; +use crate::keychain; +use crate::libwallet; +use crate::util::secp; +use grin_wallet_util::OnionV3AddressError; + +/// Wallet errors, mostly wrappers around underlying crypto or I/O errors. +#[derive(Clone, thiserror::Error, Eq, PartialEq, Debug)] +pub enum Error { + /// LibTX Error + #[error("LibTx Error")] + LibTX(#[from] libtx::Error), + + /// LibWallet Error + #[error("LibWallet Error: {0}")] + LibWallet(#[from] libwallet::Error), + + /// Keychain error + #[error("Keychain error")] + Keychain(#[from] keychain::Error), + + /// Onion V3 Address Error + #[error("Onion V3 Address Error")] + OnionV3Address(#[from] OnionV3AddressError), + + /// Error when obfs4proxy is not in the user path if TOR brigde is enabled + #[error("Unable to find obfs4proxy binary in your path; {}", _0)] + Obfs4proxyBin(String), + + /// Error the bridge input is in bad format + #[error("Bridge line is in bad format; {}", _0)] + BridgeLine(String), + + /// Error when formatting json + #[error("IO error")] + IO, + + /// Secp Error + #[error("Secp error")] + Secp(#[from] secp::Error), + + /// Error when formatting json + #[error("Serde JSON error")] + Format, + + /// Wallet seed already exists + #[error("Wallet seed file exists: {}", _0)] + WalletSeedExists(String), + + /// Wallet seed doesn't exist + #[error("Wallet seed doesn't exist error")] + WalletSeedDoesntExist, + + /// Wallet seed doesn't exist + #[error("Wallet doesn't exist at {}. {}", _0, _1)] + WalletDoesntExist(String, String), + + /// Enc/Decryption Error + #[error("Enc/Decryption error (check password?)")] + Encryption, + + /// BIP 39 word list + #[error("BIP39 Mnemonic (word list) Error")] + Mnemonic, + + /// Command line argument error + #[error("{}", _0)] + ArgumentError(String), + + /// Tor Bridge error + #[error("Tor Bridge Error: {}", _0)] + TorBridge(String), + + /// Tor Proxy error + #[error("Tor Proxy Error: {}", _0)] + TorProxy(String), + + /// Generating ED25519 Public Key + #[error("Error generating ed25519 secret key: {}", _0)] + ED25519Key(String), + + /// Checking for onion address + #[error("Address is not an Onion v3 Address: {}", _0)] + NotOnion(String), + + /// Other + #[error("Generic error: {}", _0)] + GenericError(String), +} diff --git a/impls/src/lib.rs b/impls/src/lib.rs new file mode 100644 index 0000000..b18ad7c --- /dev/null +++ b/impls/src/lib.rs @@ -0,0 +1,91 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Concrete implementations of types found in libwallet, organised this +//! way mostly to avoid any circular dependencies of any kind +//! Functions in this crate should not use the wallet api crate directly + +use blake2_rfc as blake2; + +#[macro_use] +extern crate serde_derive; +#[macro_use] +extern crate log; +#[macro_use] +extern crate serde_json; +use grin_api as api; +use grin_chain as chain; +use grin_core as core; +use grin_keychain as keychain; +use grin_store as store; +use grin_util as util; +use grin_wallet_libwallet as libwallet; + +use grin_wallet_config as config; + +mod adapters; +mod backends; +mod client_utils; +mod error; +mod lifecycle; +mod node_clients; +pub mod test_framework; +pub mod tor; + +pub use crate::adapters::{ + HttpSlateSender, PathToSlate, PathToSlatepack, SlateGetter, SlatePutter, SlateReceiver, + SlateSender, +}; +pub use crate::backends::{wallet_db_exists, LMDBBackend}; +pub use crate::error::Error; +pub use crate::lifecycle::DefaultLCProvider; +pub use crate::node_clients::HTTPNodeClient; + +use crate::keychain::{ExtKeychain, Keychain}; + +use libwallet::{NodeClient, WalletInst, WalletLCProvider}; + +/// Main wallet instance +pub struct DefaultWalletImpl<'a, C> +where + C: NodeClient + 'a, +{ + lc_provider: DefaultLCProvider<'a, C, ExtKeychain>, +} + +impl<'a, C> DefaultWalletImpl<'a, C> +where + C: NodeClient + 'a, +{ + pub fn new(node_client: C) -> Result { + let lc_provider = DefaultLCProvider::new(node_client); + Ok(DefaultWalletImpl { + lc_provider: lc_provider, + }) + } +} + +impl<'a, L, C, K> WalletInst<'a, L, C, K> for DefaultWalletImpl<'a, C> +where + DefaultLCProvider<'a, C, ExtKeychain>: WalletLCProvider<'a, C, K>, + L: WalletLCProvider<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + fn lc_provider( + &mut self, + ) -> Result<&mut (dyn WalletLCProvider<'a, C, K> + 'a), libwallet::Error> { + Ok(&mut self.lc_provider) + } +} diff --git a/impls/src/lifecycle/default.rs b/impls/src/lifecycle/default.rs new file mode 100644 index 0000000..4ea83ee --- /dev/null +++ b/impls/src/lifecycle/default.rs @@ -0,0 +1,395 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Default wallet lifecycle provider + +use crate::config::{ + config, GlobalWalletConfig, GlobalWalletConfigMembers, TorConfig, WalletConfig, GRIN_WALLET_DIR, +}; +use crate::core::global; +use crate::keychain::Keychain; +use crate::libwallet::{Error, NodeClient, WalletBackend, WalletInitStatus, WalletLCProvider}; +use crate::lifecycle::seed::WalletSeed; +use crate::util::secp::key::SecretKey; +use crate::util::ZeroingString; +use crate::LMDBBackend; +use grin_util::logger::LoggingConfig; +use std::fs; +use std::path::PathBuf; +use std::path::MAIN_SEPARATOR; + +// Helper fuction to format paths according to OS, avoids bugs on Linux +pub fn fmt_path(path: String) -> String { + let sep = &MAIN_SEPARATOR.to_string(); + let path = path.replace("/", &sep); + let path = path.replace("\\", &sep); + path +} + +pub struct DefaultLCProvider<'a, C, K> +where + C: NodeClient + 'a, + K: Keychain + 'a, +{ + data_dir: String, + node_client: C, + backend: Option + 'a>>, +} + +impl<'a, C, K> DefaultLCProvider<'a, C, K> +where + C: NodeClient + 'a, + K: Keychain + 'a, +{ + /// Create new provider + pub fn new(node_client: C) -> Self { + DefaultLCProvider { + node_client, + data_dir: "default".to_owned(), + backend: None, + } + } +} + +impl<'a, C, K> WalletLCProvider<'a, C, K> for DefaultLCProvider<'a, C, K> +where + C: NodeClient + 'a, + K: Keychain + 'a, +{ + fn set_top_level_directory(&mut self, dir: &str) -> Result<(), Error> { + self.data_dir = dir.to_owned(); + Ok(()) + } + + fn get_top_level_directory(&self) -> Result { + let sep = &MAIN_SEPARATOR.to_string(); + let data_dir = self + .data_dir + .to_owned() + .replace("/", &sep) + .replace("\\", &sep); + Ok(data_dir) + } + + fn create_config( + &self, + chain_type: &global::ChainTypes, + file_name: &str, + wallet_config: Option, + logging_config: Option, + tor_config: Option, + ) -> Result<(), Error> { + let mut default_config = GlobalWalletConfig::for_chain(&chain_type); + let config_file_version = match default_config.members.as_ref() { + Some(m) => m.clone().config_file_version, + None => None, + }; + let logging = match logging_config { + Some(l) => Some(l), + None => match default_config.members.as_ref() { + Some(m) => m.clone().logging, + None => None, + }, + }; + // Check if config was provided, if not load default and set update to "true" + let (wallet, update) = match wallet_config { + Some(w) => (w, false), + None => match default_config.members.as_ref() { + Some(m) => (m.clone().wallet, true), + None => (WalletConfig::default(), true), + }, + }; + let tor = match tor_config { + Some(t) => Some(t), + None => match default_config.members.as_ref() { + Some(m) => m.clone().tor, + None => Some(TorConfig::default()), + }, + }; + default_config = GlobalWalletConfig { + members: Some(GlobalWalletConfigMembers { + config_file_version, + wallet, + tor, + logging, + }), + ..default_config + }; + let mut config_file_name = PathBuf::from(self.data_dir.clone()); + config_file_name.push(file_name); + + let mut data_dir_name = PathBuf::from(self.data_dir.clone()); + data_dir_name.push(GRIN_WALLET_DIR); + + if config_file_name.exists() && data_dir_name.exists() { + let msg = format!( + "{} already exists in the target directory ({}). Please remove it first", + file_name, + config_file_name.to_str().unwrap() + ); + return Err(Error::Lifecycle(msg)); + } + + // If config exists but the datadir return ok + if config_file_name.exists() { + return Ok(()); + } + // default settings are updated if no config was provided, no support for top_dir/here + let mut abs_path_node = std::env::current_dir()?; + abs_path_node.push(self.data_dir.clone()); + let mut absolute_path_wallet = std::env::current_dir()?; + absolute_path_wallet.push(self.data_dir.clone()); + + // if no config provided, update defaults + if update == true { + // create top level dir if it doesn't exist + let dd = PathBuf::from(self.data_dir.clone()); + if !dd.exists() { + // try create + fs::create_dir_all(dd)?; + default_config.update_paths(&abs_path_node, &absolute_path_wallet); + } + }; + let res = + default_config.write_to_file(config_file_name.to_str().unwrap(), false, None, None); + if let Err(e) = res { + let msg = format!( + "Error creating config file as ({}): {}", + config_file_name.to_str().unwrap(), + e + ); + return Err(Error::Lifecycle(msg)); + } + + info!( + "File {} configured and created", + config_file_name.to_str().unwrap(), + ); + + let mut api_secret_path = PathBuf::from(self.data_dir.clone()); + api_secret_path.push(PathBuf::from(config::API_SECRET_FILE_NAME)); + if !api_secret_path.exists() { + config::init_api_secret(&api_secret_path).unwrap(); + } else { + config::check_api_secret(&api_secret_path).unwrap(); + } + + Ok(()) + } + + fn create_wallet( + &mut self, + _name: Option<&str>, + mnemonic: Option, + mnemonic_length: usize, + password: ZeroingString, + test_mode: bool, + ) -> Result<(), Error> { + let mut data_dir_name = PathBuf::from(self.data_dir.clone()); + data_dir_name.push(GRIN_WALLET_DIR); + let data_dir_name = fmt_path((data_dir_name.to_str().unwrap()).to_string()); + let exists = WalletSeed::seed_file_exists(&data_dir_name); + if !test_mode { + if let Ok(true) = exists { + let msg = format!("Wallet seed already exists at4565: {}", data_dir_name); + return Err(Error::WalletSeedExists(msg)); + } + } + WalletSeed::init_file( + &data_dir_name, + mnemonic_length, + mnemonic.clone(), + password, + test_mode, + ) + .map_err(|_| { + Error::Lifecycle("Error creating wallet seed (is mnemonic valid?)".to_owned()) + })?; + info!("Wallet seed file created"); + let mut wallet: LMDBBackend<'a, C, K> = + match LMDBBackend::new(&data_dir_name, self.node_client.clone()) { + Err(e) => { + let msg = format!("Error creating wallet: {}, Data Dir: {}", e, &data_dir_name); + error!("{}", msg); + return Err(Error::Lifecycle(msg).into()); + } + Ok(d) => d, + }; + // Save init status of this wallet, to determine whether it needs a full UTXO scan + let mut batch = wallet.batch_no_mask()?; + match mnemonic { + Some(_) => batch.save_init_status(WalletInitStatus::InitNeedsScanning)?, + None => batch.save_init_status(WalletInitStatus::InitNoScanning)?, + }; + batch.commit()?; + info!("Wallet database backend created at {}", data_dir_name); + Ok(()) + } + + fn open_wallet( + &mut self, + _name: Option<&str>, + password: ZeroingString, + create_mask: bool, + use_test_rng: bool, + ) -> Result, Error> { + let mut data_dir_name = PathBuf::from(self.data_dir.clone()); + data_dir_name.push(GRIN_WALLET_DIR); + let data_dir_name = fmt_path(data_dir_name.to_str().unwrap().to_string()); + let mut wallet: LMDBBackend<'a, C, K> = + match LMDBBackend::new(&data_dir_name, self.node_client.clone()) { + Err(e) => { + let msg = format!("Error opening wallet: {}, Data Dir: {}", e, &data_dir_name); + return Err(Error::Lifecycle(msg)); + } + Ok(d) => d, + }; + let wallet_seed = WalletSeed::from_file(&data_dir_name, password).map_err(|_| { + Error::Lifecycle("Error opening wallet (is password correct?)".to_owned()) + })?; + let keychain = wallet_seed + .derive_keychain(global::is_testnet()) + .map_err(|_| Error::Lifecycle("Error deriving keychain".to_owned()))?; + + let mask = wallet.set_keychain(Box::new(keychain), create_mask, use_test_rng)?; + self.backend = Some(Box::new(wallet)); + Ok(mask) + } + + fn close_wallet(&mut self, _name: Option<&str>) -> Result<(), Error> { + if let Some(b) = self.backend.as_mut() { + b.close()? + } + self.backend = None; + Ok(()) + } + + fn wallet_exists(&self, _name: Option<&str>) -> Result { + let mut data_dir_name = PathBuf::from(self.data_dir.clone()); + data_dir_name.push(GRIN_WALLET_DIR); + let data_dir_name = data_dir_name.to_str().unwrap(); + let res = WalletSeed::seed_file_exists(&data_dir_name) + .map_err(|_| Error::CallbackImpl("Error checking for wallet existence"))?; + Ok(res) + } + + fn get_mnemonic( + &self, + _name: Option<&str>, + password: ZeroingString, + ) -> Result { + let mut data_dir_name = PathBuf::from(self.data_dir.clone()); + data_dir_name.push(GRIN_WALLET_DIR); + let data_dir_name = fmt_path(data_dir_name.display().to_string()); + let wallet_seed = WalletSeed::from_file(&data_dir_name, password) + .map_err(|_| Error::Lifecycle("Error opening wallet seed file".into()))?; + let res = wallet_seed + .to_mnemonic() + .map_err(|_| Error::Lifecycle("Error recovering wallet seed".into()))?; + Ok(ZeroingString::from(res)) + } + + fn validate_mnemonic(&self, mnemonic: ZeroingString) -> Result<(), Error> { + match WalletSeed::from_mnemonic(mnemonic) { + Ok(_) => Ok(()), + Err(_) => Err(Error::GenericError("Validating mnemonic".into())), + } + } + + fn recover_from_mnemonic( + &self, + mnemonic: ZeroingString, + password: ZeroingString, + ) -> Result<(), Error> { + let mut data_dir_name = PathBuf::from(self.data_dir.clone()); + data_dir_name.push(GRIN_WALLET_DIR); + let data_dir_name = data_dir_name.to_str().unwrap(); + WalletSeed::recover_from_phrase(data_dir_name, mnemonic, password) + .map_err(|_| Error::Lifecycle("Error recovering from mnemonic".into()))?; + Ok(()) + } + + fn change_password( + &self, + _name: Option<&str>, + old: ZeroingString, + new: ZeroingString, + ) -> Result<(), Error> { + let mut data_dir_name = PathBuf::from(self.data_dir.clone()); + data_dir_name.push(GRIN_WALLET_DIR); + let data_dir_name = data_dir_name.to_str().unwrap(); + // get seed for later check + + let orig_wallet_seed = WalletSeed::from_file(&data_dir_name, old) + .map_err(|_| Error::Lifecycle("Error opening wallet seed file".into()))?; + let orig_mnemonic = orig_wallet_seed + .to_mnemonic() + .map_err(|_| Error::Lifecycle("Error recovering mnemonic".into()))?; + + // Back up existing seed, and keep track of filename as we're deleting it + // once the password change is confirmed + let backup_name = WalletSeed::backup_seed(data_dir_name) + .map_err(|_| Error::Lifecycle("Error temporarily backing up existing seed".into()))?; + + // Delete seed file + WalletSeed::delete_seed_file(data_dir_name).map_err(|_| { + Error::Lifecycle("Unable to delete seed file for password change".into()) + })?; + + // Init a new file + let _ = WalletSeed::init_file( + data_dir_name, + 0, + Some(ZeroingString::from(orig_mnemonic)), + new.clone(), + false, + ); + info!("Wallet seed file created"); + + let new_wallet_seed = WalletSeed::from_file(&data_dir_name, new) + .map_err(|_| Error::Lifecycle("Error opening wallet seed file".into()))?; + + if orig_wallet_seed != new_wallet_seed { + let msg = + "New and Old wallet seeds are not equal on password change, not removing backups." + .to_string(); + return Err(Error::Lifecycle(msg)); + } + // Removin + info!("Password change confirmed, removing old seed file."); + fs::remove_file(backup_name).map_err(|e| Error::IO(e.to_string()))?; + + Ok(()) + } + + fn delete_wallet(&self, _name: Option<&str>) -> Result<(), Error> { + let data_dir_name = PathBuf::from(self.data_dir.clone()); + warn!( + "Removing all wallet data from: {}", + data_dir_name.to_str().unwrap() + ); + fs::remove_dir_all(data_dir_name).map_err(|e| Error::IO(e.to_string()))?; + Ok(()) + } + + fn wallet_inst(&mut self) -> Result<&mut Box + 'a>, Error> { + match self.backend.as_mut() { + None => { + let msg = "Wallet has not been opened".into(); + Err(Error::Lifecycle(msg)) + } + Some(_) => Ok(&mut *self.backend.as_mut().unwrap()), + } + } +} diff --git a/impls/src/lifecycle/mod.rs b/impls/src/lifecycle/mod.rs new file mode 100644 index 0000000..fb9113d --- /dev/null +++ b/impls/src/lifecycle/mod.rs @@ -0,0 +1,18 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +mod default; +mod seed; + +pub use self::default::DefaultLCProvider; diff --git a/impls/src/lifecycle/seed.rs b/impls/src/lifecycle/seed.rs new file mode 100644 index 0000000..59dcd34 --- /dev/null +++ b/impls/src/lifecycle/seed.rs @@ -0,0 +1,385 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::blake2; +use core::num::NonZeroU32; +use rand::{thread_rng, Rng}; +use serde_json; +use std::fs::{self, File}; +use std::io::{Read, Write}; +use std::path::Path; +use std::path::MAIN_SEPARATOR; + +use ring::aead; +use ring::pbkdf2; + +use crate::keychain::{mnemonic, Keychain}; +use crate::util::{self, ToHex}; +use crate::Error; + +pub const SEED_FILE: &str = "wallet.seed"; + +#[derive(Clone, Debug, PartialEq)] +pub struct WalletSeed(Vec); + +impl WalletSeed { + pub fn from_bytes(bytes: &[u8]) -> WalletSeed { + WalletSeed(bytes.to_vec()) + } + + pub fn from_mnemonic(word_list: util::ZeroingString) -> Result { + let res = mnemonic::to_entropy(&word_list); + match res { + Ok(s) => Ok(WalletSeed::from_bytes(&s)), + Err(_) => Err(Error::Mnemonic.into()), + } + } + + pub fn _from_hex(hex: &str) -> Result { + let bytes = util::from_hex(&hex.to_string()) + .map_err(|_| Error::GenericError("Invalid hex".to_owned()))?; + Ok(WalletSeed::from_bytes(&bytes)) + } + + pub fn _to_hex(&self) -> String { + self.0.to_vec().to_hex() + } + + // Helper fuction to format paths according to OS, avoids bugs on Linux + pub fn fmt_path(path: String) -> String { + let sep = &MAIN_SEPARATOR.to_string(); + let path = path.replace("/", &sep); + let path = path.replace("\\", &sep); + path + } + pub fn to_mnemonic(&self) -> Result { + let result = mnemonic::from_entropy(&self.0); + match result { + Ok(r) => Ok(r), + Err(_) => Err(Error::Mnemonic.into()), + } + } + + pub fn _derive_keychain_old(old_wallet_seed: [u8; 32], password: &str) -> Vec { + let seed = blake2::blake2b::blake2b(64, password.as_bytes(), &old_wallet_seed); + seed.as_bytes().to_vec() + } + + pub fn derive_keychain(&self, is_testnet: bool) -> Result { + let result = K::from_seed(&self.0, is_testnet)?; + Ok(result) + } + + pub fn init_new( + seed_length: usize, + test_mode: bool, + password: Option, + ) -> WalletSeed { + let mut seed: Vec = vec![]; + let mut rng = thread_rng(); + if !test_mode { + for _ in 0..seed_length { + seed.push(rng.gen()); + } + } else { + // Hash password and use for test seed so we have a way of keeping test wallets unique + // but predictable + seed = blake2::blake2b::blake2b(32, b"", password.unwrap().as_bytes()) + .as_bytes() + .to_vec(); + } + WalletSeed(seed) + } + + pub fn seed_file_exists(data_file_dir: &str) -> Result { + let seed_file_path = &format!( + "{}{}{}", + Self::fmt_path(data_file_dir.to_string()), + MAIN_SEPARATOR, + SEED_FILE, + ); + debug!("Seed file path: {}", seed_file_path); + if Path::new(seed_file_path).exists() { + Ok(true) + } else { + Ok(false) + } + } + + pub fn backup_seed(data_file_dir: &str) -> Result { + let seed_file_name = &format!("{}{}{}", data_file_dir, MAIN_SEPARATOR, SEED_FILE,); + + let mut path = Path::new(seed_file_name).to_path_buf(); + path.pop(); + let mut backup_seed_file_name = + format!("{}{}{}.bak", data_file_dir, MAIN_SEPARATOR, SEED_FILE); + let mut i = 1; + while Path::new(&backup_seed_file_name).exists() { + backup_seed_file_name = + format!("{}{}{}.bak.{}", data_file_dir, MAIN_SEPARATOR, SEED_FILE, i); + i += 1; + } + path.push(backup_seed_file_name.clone()); + if fs::rename(seed_file_name, backup_seed_file_name.as_str()).is_err() { + return Err(Error::GenericError("Can't rename wallet seed file".to_owned()).into()); + } + warn!("{} backed up as {}", seed_file_name, backup_seed_file_name); + Ok(backup_seed_file_name) + } + + pub fn recover_from_phrase( + data_file_dir: &str, + word_list: util::ZeroingString, + password: util::ZeroingString, + ) -> Result<(), Error> { + let seed_file_path = &format!( + "{}{}{}", + Self::fmt_path(data_file_dir.to_string()), + MAIN_SEPARATOR, + SEED_FILE, + ); + debug!("data file dir: {}", data_file_dir); + if let Ok(true) = WalletSeed::seed_file_exists(data_file_dir) { + debug!("seed file exists"); + WalletSeed::backup_seed(data_file_dir)?; + } + if !Path::new(&data_file_dir).exists() { + return Err(Error::WalletDoesntExist( + data_file_dir.to_owned(), + "To create a new wallet from a recovery phrase, use 'grin-wallet init -r'" + .to_owned(), + ) + .into()); + } + let seed = WalletSeed::from_mnemonic(word_list)?; + let enc_seed = EncryptedWalletSeed::from_seed(&seed, password)?; + let enc_seed_json = serde_json::to_string_pretty(&enc_seed).map_err(|_| Error::Format)?; + let mut file = File::create(seed_file_path).map_err(|_| Error::IO)?; + file.write_all(&enc_seed_json.as_bytes()) + .map_err(|_| Error::IO)?; + warn!("Seed created from word list"); + Ok(()) + } + + pub fn init_file( + data_file_dir: &str, + seed_length: usize, + recovery_phrase: Option, + password: util::ZeroingString, + test_mode: bool, + ) -> Result { + // create directory if it doesn't exist + fs::create_dir_all(data_file_dir).map_err(|_| Error::IO)?; + + let seed_file_path = &format!( + "{}{}{}", + Self::fmt_path(data_file_dir.to_string()), + MAIN_SEPARATOR, + SEED_FILE, + ); + let data_file_dir = Self::fmt_path(data_file_dir.to_string()); + warn!("Generating wallet seed file at: {}", seed_file_path); + let exists = WalletSeed::seed_file_exists(&data_file_dir)?; + if exists && !test_mode { + let msg = format!("Wallet seed already exists at: {}", data_file_dir); + error!("{}", msg); + return Err(Error::WalletSeedExists(msg)); + } + + let seed = match recovery_phrase { + Some(p) => WalletSeed::from_mnemonic(p)?, + None => WalletSeed::init_new(seed_length, test_mode, Some(password.clone())), + }; + + let enc_seed = EncryptedWalletSeed::from_seed(&seed, password)?; + let enc_seed_json = serde_json::to_string_pretty(&enc_seed).map_err(|_| Error::Format)?; + let mut file = File::create(seed_file_path).map_err(|_| Error::IO)?; + file.write_all(&enc_seed_json.as_bytes()) + .map_err(|_| Error::IO)?; + Ok(seed) + } + + pub fn from_file( + data_file_dir: &str, + password: util::ZeroingString, + ) -> Result { + // create directory if it doesn't exist + fs::create_dir_all(data_file_dir).map_err(|_| Error::IO)?; + + let seed_file_path = &format!( + "{}{}{}", + Self::fmt_path(data_file_dir.to_string()), + MAIN_SEPARATOR, + SEED_FILE, + ); + + debug!("Using wallet seed file at: {}", seed_file_path); + + if Path::new(seed_file_path).exists() { + let mut file = File::open(seed_file_path).map_err(|_| Error::IO)?; + let mut buffer = String::new(); + file.read_to_string(&mut buffer).map_err(|_| Error::IO)?; + let enc_seed: EncryptedWalletSeed = + serde_json::from_str(&buffer).map_err(|_| Error::Format)?; + let wallet_seed = enc_seed.decrypt(&password)?; + Ok(wallet_seed) + } else { + error!( + "wallet seed file {} could not be opened (grin-wallet init). \ + Run \"grin-wallet init\" to initialize a new wallet.", + seed_file_path + ); + Err(Error::WalletSeedDoesntExist) + } + } + + pub fn delete_seed_file(data_file_dir: &str) -> Result<(), Error> { + let seed_file_path = &format!( + "{}{}{}", + Self::fmt_path(data_file_dir.to_string()), + MAIN_SEPARATOR, + SEED_FILE, + ); + if Path::new(seed_file_path).exists() { + debug!("Deleting wallet seed file at: {}", seed_file_path); + fs::remove_file(seed_file_path).map_err(|_| Error::IO)?; + } + Ok(()) + } +} + +/// Encrypted wallet seed, for storing on disk and decrypting +/// with provided password + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub struct EncryptedWalletSeed { + encrypted_seed: String, + /// Salt, not so useful in single case but include anyhow for situations + /// where someone wants to store many of these + pub salt: String, + /// Nonce + pub nonce: String, +} + +impl EncryptedWalletSeed { + /// Create a new encrypted seed from the given seed + password + pub fn from_seed( + seed: &WalletSeed, + password: util::ZeroingString, + ) -> Result { + let salt: [u8; 8] = thread_rng().gen(); + let nonce: [u8; 12] = thread_rng().gen(); + let password = password.as_bytes(); + let mut key = [0; 32]; + pbkdf2::derive( + ring::pbkdf2::PBKDF2_HMAC_SHA512, + NonZeroU32::new(100).unwrap(), + &salt, + password, + &mut key, + ); + let content = seed.0.to_vec(); + let mut enc_bytes = content; + /*let suffix_len = aead::CHACHA20_POLY1305.tag_len(); + for _ in 0..suffix_len { + enc_bytes.push(0); + }*/ + let unbound_key = aead::UnboundKey::new(&aead::CHACHA20_POLY1305, &key).unwrap(); + let sealing_key: aead::LessSafeKey = aead::LessSafeKey::new(unbound_key); + let aad = aead::Aad::from(&[]); + let res = sealing_key.seal_in_place_append_tag( + aead::Nonce::assume_unique_for_key(nonce), + aad, + &mut enc_bytes, + ); + if let Err(_) = res { + return Err(Error::Encryption); + } + + Ok(EncryptedWalletSeed { + encrypted_seed: enc_bytes.to_hex(), + salt: salt.to_hex(), + nonce: nonce.to_hex(), + }) + } + + /// Decrypt seed + pub fn decrypt(&self, password: &str) -> Result { + let mut encrypted_seed = match util::from_hex(&self.encrypted_seed.clone()) { + Ok(s) => s, + Err(_) => return Err(Error::Encryption), + }; + let salt = match util::from_hex(&self.salt.clone()) { + Ok(s) => s, + Err(_) => return Err(Error::Encryption), + }; + let nonce = match util::from_hex(&self.nonce.clone()) { + Ok(s) => s, + Err(_) => return Err(Error::Encryption), + }; + let password = password.as_bytes(); + let mut key = [0; 32]; + pbkdf2::derive( + ring::pbkdf2::PBKDF2_HMAC_SHA512, + NonZeroU32::new(100).unwrap(), + &salt, + password, + &mut key, + ); + + let mut n = [0u8; 12]; + n.copy_from_slice(&nonce[0..12]); + let unbound_key = aead::UnboundKey::new(&aead::CHACHA20_POLY1305, &key).unwrap(); + let opening_key: aead::LessSafeKey = aead::LessSafeKey::new(unbound_key); + let aad = aead::Aad::from(&[]); + let res = opening_key.open_in_place( + aead::Nonce::assume_unique_for_key(n), + aad, + &mut encrypted_seed, + ); + if let Err(_) = res { + return Err(Error::Encryption); + } + for _ in 0..aead::AES_256_GCM.tag_len() { + encrypted_seed.pop(); + } + + Ok(WalletSeed::from_bytes(&encrypted_seed)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::util::ZeroingString; + #[test] + fn wallet_seed_encrypt() { + let password = ZeroingString::from("passwoid"); + let wallet_seed = WalletSeed::init_new(32, false, None); + let mut enc_wallet_seed = + EncryptedWalletSeed::from_seed(&wallet_seed, password.clone()).unwrap(); + println!("EWS: {:?}", enc_wallet_seed); + let decrypted_wallet_seed = enc_wallet_seed.decrypt(&password).unwrap(); + assert_eq!(wallet_seed, decrypted_wallet_seed); + + // Wrong password + let decrypted_wallet_seed = enc_wallet_seed.decrypt(""); + assert!(decrypted_wallet_seed.is_err()); + + // Wrong nonce + enc_wallet_seed.nonce = "wrongnonce".to_owned(); + let decrypted_wallet_seed = enc_wallet_seed.decrypt(&password); + assert!(decrypted_wallet_seed.is_err()); + } +} diff --git a/impls/src/node_clients/http.rs b/impls/src/node_clients/http.rs new file mode 100644 index 0000000..6251d38 --- /dev/null +++ b/impls/src/node_clients/http.rs @@ -0,0 +1,436 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Client functions, implementations of the NodeClient trait + +use crate::api::{self, LocatedTxKernel, OutputListing, OutputPrintable}; +use crate::core::core::{Transaction, TxKernel}; +use crate::libwallet::{NodeClient, NodeVersionInfo}; +use futures::stream::FuturesUnordered; +use futures::TryStreamExt; +use std::collections::HashMap; +use std::env; + +use crate::client_utils::{Client, RUNTIME}; +use crate::libwallet; +use crate::util::secp::pedersen; +use crate::util::ToHex; + +use super::resp_types::*; +use crate::client_utils::json_rpc::*; + +const ENDPOINT: &str = "/v2/foreign"; + +#[derive(Clone)] +pub struct HTTPNodeClient { + client: Client, + node_url: String, + node_api_secret: Option, + node_version_info: Option, +} + +impl HTTPNodeClient { + /// Create a new client that will communicate with the given grin node + pub fn new( + node_url: &str, + node_api_secret: Option, + ) -> Result { + Ok(HTTPNodeClient { + client: Client::new().map_err(|_| libwallet::Error::Node)?, + node_url: node_url.to_owned(), + node_api_secret: node_api_secret, + node_version_info: None, + }) + } + + /// Allow returning the chain height without needing a wallet instantiated + pub fn chain_height(&self) -> Result<(u64, String), libwallet::Error> { + self.get_chain_tip() + } + + fn send_json_request( + &self, + method: &str, + params: &serde_json::Value, + ) -> Result { + let url = format!("{}{}", self.node_url(), ENDPOINT); + let req = build_request(method, params); + let res = self + .client + .post::(url.as_str(), self.node_api_secret(), &req); + + match res { + Err(e) => { + let report = format!("Error calling {}: {}", method, e); + error!("{}", report); + Err(libwallet::Error::ClientCallback(report)) + } + Ok(inner) => match inner.clone().into_result() { + Ok(r) => Ok(r), + Err(e) => { + error!("{:?}", inner); + let report = format!("Unable to parse response for {}: {}", method, e); + error!("{}", report); + Err(libwallet::Error::ClientCallback(report)) + } + }, + } + } +} + +impl NodeClient for HTTPNodeClient { + fn node_url(&self) -> &str { + &self.node_url + } + fn node_api_secret(&self) -> Option { + self.node_api_secret.clone() + } + + fn set_node_url(&mut self, node_url: &str) { + self.node_url = node_url.to_owned(); + } + + fn set_node_api_secret(&mut self, node_api_secret: Option) { + self.node_api_secret = node_api_secret; + } + + fn get_version_info(&mut self) -> Option { + if let Some(v) = self.node_version_info.as_ref() { + return Some(v.clone()); + } + let retval = match self + .send_json_request::("get_version", &serde_json::Value::Null) + { + Ok(n) => NodeVersionInfo { + node_version: n.node_version, + block_header_version: n.block_header_version, + verified: Some(true), + }, + Err(e) => { + // If node isn't available, allow offline functions + // unfortunately have to parse string due to error structure + let err_string = format!("{}", e); + if err_string.contains("404") { + return Some(NodeVersionInfo { + node_version: "1.0.0".into(), + block_header_version: 1, + verified: Some(false), + }); + } else { + error!("Unable to contact Node to get version info: {}, check your node is running", e); + warn!("Warning: a) Node is offline, or b) 'node_api_secret_path' in 'grin-wallet.toml' is set incorrectly"); + return None; + } + } + }; + self.node_version_info = Some(retval.clone()); + Some(retval) + } + + /// Posts a transaction to a grin node + fn post_tx(&self, tx: &Transaction, fluff: bool) -> Result<(), libwallet::Error> { + let params = json!([tx, fluff]); + self.send_json_request::("push_transaction", ¶ms)?; + Ok(()) + } + + /// Return the chain tip from a given node + fn get_chain_tip(&self) -> Result<(u64, String), libwallet::Error> { + let result = self.send_json_request::("get_tip", &serde_json::Value::Null)?; + Ok((result.height, result.last_block_pushed)) + } + + /// Get kernel implementation + fn get_kernel( + &mut self, + excess: &pedersen::Commitment, + min_height: Option, + max_height: Option, + ) -> Result, libwallet::Error> { + let method = "get_kernel"; + let params = json!([excess.0.as_ref().to_hex(), min_height, max_height]); + // have to handle this manually since the error needs to be parsed + let url = format!("{}{}", self.node_url(), ENDPOINT); + let req = build_request(method, ¶ms); + let res = self + .client + .post::(url.as_str(), self.node_api_secret(), &req); + + match res { + Err(e) => { + let report = format!("Error calling {}: {}", method, e); + error!("{}", report); + Err(libwallet::Error::ClientCallback(report)) + } + Ok(inner) => match inner.clone().into_result::() { + Ok(r) => Ok(Some((r.tx_kernel, r.height, r.mmr_index))), + Err(e) => { + let contents = format!("{:?}", inner); + if contents.contains("NotFound") { + Ok(None) + } else { + let report = format!("Unable to parse response for {}: {}", method, e); + error!("{}", report); + Err(libwallet::Error::ClientCallback(report)) + } + } + }, + } + } + + /// Retrieve outputs from node + fn get_outputs_from_node( + &self, + wallet_outputs: Vec, + ) -> Result, libwallet::Error> { + // build a map of api outputs by commit so we can look them up efficiently + let mut api_outputs: HashMap = HashMap::new(); + + if wallet_outputs.is_empty() { + return Ok(api_outputs); + } + + // build vec of commits for inclusion in query + let query_params: Vec = wallet_outputs + .iter() + .map(|commit| format!("{}", commit.as_ref().to_hex())) + .collect(); + + // going to leave this here even though we're moving + // to the json RPC api to keep the functionality of + // parallelizing larger requests. Will raise default + // from 200 to 500, however + let chunk_default = 500; + let chunk_size = match env::var("GRIN_OUTPUT_QUERY_SIZE") { + Ok(s) => match s.parse::() { + Ok(c) => c, + Err(e) => { + error!( + "Unable to parse GRIN_OUTPUT_QUERY_SIZE, defaulting to {}", + chunk_default + ); + error!("Reason: {}", e); + chunk_default + } + }, + Err(_) => chunk_default, + }; + + trace!("Output query chunk size is: {}", chunk_size); + + let url = format!("{}{}", self.node_url(), ENDPOINT); + let api_secret = self.node_api_secret(); + let cl = self.client.clone(); + let task = async move { + let params: Vec<_> = query_params + .chunks(chunk_size) + .map(|c| json!([c, null, null, false, false])) + .collect(); + + let mut reqs = Vec::with_capacity(params.len()); + for p in ¶ms { + reqs.push(build_request("get_outputs", p)); + } + + let mut tasks = Vec::with_capacity(params.len()); + for req in &reqs { + tasks.push(cl.post_async::( + url.as_str(), + req, + api_secret.clone(), + )); + } + + let task: FuturesUnordered<_> = tasks.into_iter().collect(); + task.try_collect().await + }; + + let rt = RUNTIME.clone(); + let res: Result, _> = + std::thread::spawn(move || rt.lock().unwrap().block_on(async move { task.await })) + .join() + .unwrap(); + + let results: Vec = match res { + Ok(resps) => { + let mut results = vec![]; + for r in resps { + match r.into_result::>() { + Ok(mut r) => results.append(&mut r), + Err(e) => { + let report = format!("Unable to parse response for get_outputs: {}", e); + error!("{}", report); + return Err(libwallet::Error::ClientCallback(report)); + } + }; + } + results + } + Err(e) => { + let report = format!("Getting outputs by id: {}", e); + error!("Outputs by id failed: {}", e); + return Err(libwallet::Error::ClientCallback(report)); + } + }; + + for out in results.iter() { + let height = match out.block_height { + Some(h) => h, + None => { + let msg = format!("Missing block height for output {:?}", out.commit); + return Err(libwallet::Error::ClientCallback(msg)); + } + }; + api_outputs.insert( + out.commit, + (out.commit.as_ref().to_hex(), height, out.mmr_index), + ); + } + Ok(api_outputs) + } + + fn get_outputs_by_pmmr_index( + &self, + start_index: u64, + end_index: Option, + max_outputs: u64, + ) -> Result< + ( + u64, + u64, + Vec<(pedersen::Commitment, pedersen::RangeProof, bool, u64, u64)>, + ), + libwallet::Error, + > { + let mut api_outputs: Vec<(pedersen::Commitment, pedersen::RangeProof, bool, u64, u64)> = + Vec::new(); + + let params = json!([start_index, end_index, max_outputs, Some(true)]); + let res = self.send_json_request::("get_unspent_outputs", ¶ms)?; + // We asked for unspent outputs via the api but defensively filter out spent outputs just in case. + for out in res.outputs.into_iter().filter(|out| out.spent == false) { + let is_coinbase = match out.output_type { + api::OutputType::Coinbase => true, + api::OutputType::Transaction => false, + }; + let range_proof = match out.range_proof() { + Ok(r) => r, + Err(e) => { + let msg = format!( + "Unexpected error in returned output (missing range proof): {:?}. {:?}, {}", + out.commit, out, e + ); + error!("{}", msg); + return Err(libwallet::Error::ClientCallback(msg)); + } + }; + let block_height = match out.block_height { + Some(h) => h, + None => { + let msg = format!( + "Unexpected error in returned output (missing block height): {:?}. {:?}", + out.commit, out + ); + error!("{}", msg); + return Err(libwallet::Error::ClientCallback(msg)); + } + }; + api_outputs.push(( + out.commit, + range_proof, + is_coinbase, + block_height, + out.mmr_index, + )); + } + Ok((res.highest_index, res.last_retrieved_index, api_outputs)) + } + + fn height_range_to_pmmr_indices( + &self, + start_height: u64, + end_height: Option, + ) -> Result<(u64, u64), libwallet::Error> { + let params = json!([start_height, end_height]); + let res = self.send_json_request::("get_pmmr_indices", ¶ms)?; + + Ok((res.last_retrieved_index, res.highest_index)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::core::core::{KernelFeatures, OutputFeatures, OutputIdentifier}; + use crate::core::libtx::build; + use crate::core::libtx::ProofBuilder; + use crate::keychain::{ExtKeychain, Keychain}; + + // JSON api for "push_transaction" between wallet->node currently only supports "feature and commit" inputs. + // We will need to revisit this if we decide to support "commit only" inputs (no features) at wallet level. + fn tx1i1o_v2_compatible() -> Transaction { + let keychain = ExtKeychain::from_random_seed(false).unwrap(); + let builder = ProofBuilder::new(&keychain); + let key_id1 = ExtKeychain::derive_key_id(1, 1, 0, 0, 0); + let key_id2 = ExtKeychain::derive_key_id(1, 2, 0, 0, 0); + let tx = build::transaction( + KernelFeatures::Plain { fee: 2.into() }, + &[build::input(5, key_id1), build::output(3, key_id2)], + &keychain, + &builder, + ) + .unwrap(); + + let inputs: Vec<_> = tx.inputs().into(); + let inputs: Vec<_> = inputs + .iter() + .map(|input| OutputIdentifier { + features: OutputFeatures::Plain, + commit: input.commitment(), + }) + .collect(); + Transaction { + body: tx.body.replace_inputs(inputs.as_slice().into()), + ..tx + } + } + + // Wallet will "push" a transaction to node, serializing the transaction as json. + // We are testing the json structure is what we expect here. + #[test] + fn test_transaction_json_ser_deser() { + let tx1 = tx1i1o_v2_compatible(); + let value = serde_json::to_value(&tx1).unwrap(); + + assert!(value["offset"].is_string()); + assert_eq!(value["body"]["inputs"][0]["features"], "Plain"); + assert!(value["body"]["inputs"][0]["commit"].is_string()); + assert_eq!(value["body"]["outputs"][0]["features"], "Plain"); + assert!(value["body"]["outputs"][0]["commit"].is_string()); + assert!(value["body"]["outputs"][0]["proof"].is_string()); + + // Note: Tx kernel "features" serialize in a slightly unexpected way. + assert_eq!(value["body"]["kernels"][0]["features"]["Plain"]["fee"], 2); + assert!(value["body"]["kernels"][0]["excess"].is_string()); + assert!(value["body"]["kernels"][0]["excess_sig"].is_string()); + + let tx2: Transaction = serde_json::from_value(value).unwrap(); + assert_eq!(tx1, tx2); + + let str = serde_json::to_string(&tx1).unwrap(); + println!("{}", str); + let tx2: Transaction = serde_json::from_str(&str).unwrap(); + assert_eq!(tx1, tx2); + } +} diff --git a/impls/src/node_clients/mod.rs b/impls/src/node_clients/mod.rs new file mode 100644 index 0000000..5b3596d --- /dev/null +++ b/impls/src/node_clients/mod.rs @@ -0,0 +1,18 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +mod http; +mod resp_types; + +pub use self::http::HTTPNodeClient; diff --git a/impls/src/node_clients/resp_types.rs b/impls/src/node_clients/resp_types.rs new file mode 100644 index 0000000..fc5eda5 --- /dev/null +++ b/impls/src/node_clients/resp_types.rs @@ -0,0 +1,31 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// Derived from https://github.com/apoelstra/rust-jsonrpc + +//! JSON RPC Types for V2 node client + +#[allow(dead_code)] +#[derive(Debug, Deserialize)] +pub struct GetTipResp { + pub height: u64, + pub last_block_pushed: String, + pub prev_block_to_last: String, + pub total_difficulty: u64, +} + +#[derive(Debug, Deserialize)] +pub struct GetVersionResp { + pub node_version: String, + pub block_header_version: u16, +} diff --git a/impls/src/test_framework/mod.rs b/impls/src/test_framework/mod.rs new file mode 100644 index 0000000..2479793 --- /dev/null +++ b/impls/src/test_framework/mod.rs @@ -0,0 +1,276 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::api; +use crate::chain; +use crate::chain::Chain; +use crate::core; +use crate::core::core::{Output, Transaction, TxKernel}; +use crate::core::{consensus, global, pow}; +use crate::keychain; +use crate::libwallet; +use crate::libwallet::api_impl::{foreign, owner}; +use crate::libwallet::{ + BlockFees, InitTxArgs, NodeClient, WalletInfo, WalletInst, WalletLCProvider, +}; +use crate::util::secp::key::SecretKey; +use crate::util::secp::pedersen; +use crate::util::Mutex; +use chrono::Duration; +use std::sync::Arc; +use std::thread; + +mod testclient; + +pub use self::{testclient::LocalWalletClient, testclient::WalletProxy}; + +/// Get an output from the chain locally and present it back as an API output +fn get_output_local(chain: &chain::Chain, commit: pedersen::Commitment) -> Option { + if chain.get_unspent(commit).unwrap().is_some() { + let block_height = chain.get_header_for_output(commit).unwrap().height; + let output_pos = chain.get_output_pos(&commit).unwrap_or(0); + Some(api::Output::new(&commit, block_height, output_pos)) + } else { + None + } +} + +/// Get a kernel from the chain locally +fn get_kernel_local( + chain: Arc, + excess: &pedersen::Commitment, + min_height: Option, + max_height: Option, +) -> Option { + chain + .get_kernel_height(&excess, min_height, max_height) + .unwrap() + .map(|(tx_kernel, height, mmr_index)| api::LocatedTxKernel { + tx_kernel, + height, + mmr_index, + }) +} + +/// get output listing traversing pmmr from local +fn get_outputs_by_pmmr_index_local( + chain: Arc, + start_index: u64, + end_index: Option, + max: u64, +) -> api::OutputListing { + let outputs = chain + .unspent_outputs_by_pmmr_index(start_index, max, end_index) + .unwrap(); + api::OutputListing { + last_retrieved_index: outputs.0, + highest_index: outputs.1, + outputs: outputs + .2 + .iter() + .map(|x| api::OutputPrintable::from_output(x, &chain, None, true, false).unwrap()) + .collect(), + } +} + +/// get output listing in a given block range +fn height_range_to_pmmr_indices_local( + chain: Arc, + start_index: u64, + end_index: Option, +) -> api::OutputListing { + let indices = chain + .block_height_range_to_pmmr_indices(start_index, end_index) + .unwrap(); + api::OutputListing { + last_retrieved_index: indices.0, + highest_index: indices.1, + outputs: vec![], + } +} + +fn create_block_with_reward( + chain: &Chain, + prev: core::core::BlockHeader, + txs: &[Transaction], + reward_output: Output, + reward_kernel: TxKernel, +) -> core::core::Block { + let next_header_info = + consensus::next_difficulty(prev.height + 1, chain.difficulty_iter().unwrap()); + let mut b = core::core::Block::new( + &prev, + txs, + next_header_info.clone().difficulty, + (reward_output, reward_kernel), + ) + .unwrap(); + b.header.timestamp = prev.timestamp + Duration::seconds(60); + b.header.pow.secondary_scaling = next_header_info.secondary_scaling; + chain.set_txhashset_roots(&mut b).unwrap(); + pow::pow_size( + &mut b.header, + next_header_info.difficulty, + global::proofsize(), + global::min_edge_bits(), + ) + .unwrap(); + b +} + +/// Adds a block with a given reward to the chain and mines it +pub fn add_block_with_reward( + chain: &Chain, + txs: &[Transaction], + reward_output: Output, + reward_kernel: TxKernel, +) { + let prev = chain.head_header().unwrap(); + let block = create_block_with_reward(chain, prev, txs, reward_output, reward_kernel); + process_block(chain, block); +} + +/// adds a reward output to a wallet, includes that reward in a block +/// and return the block +pub fn create_block_for_wallet<'a, L, C, K>( + chain: &Chain, + prev: core::core::BlockHeader, + txs: &[Transaction], + wallet: Arc + 'a>>>, + keychain_mask: Option<&SecretKey>, +) -> Result +where + L: WalletLCProvider<'a, C, K>, + C: NodeClient + 'a, + K: keychain::Keychain + 'a, +{ + // build block fees + let fee_amt = txs.iter().map(|tx| tx.fee()).sum(); + let block_fees = BlockFees { + fees: fee_amt, + key_id: None, + height: prev.height + 1, + }; + // build coinbase (via api) and add block + let coinbase_tx = { + let mut w_lock = wallet.lock(); + let w = w_lock.lc_provider()?.wallet_inst()?; + foreign::build_coinbase(&mut **w, keychain_mask, &block_fees, false)? + }; + let block = create_block_with_reward(chain, prev, txs, coinbase_tx.output, coinbase_tx.kernel); + Ok(block) +} + +/// adds a reward output to a wallet, includes that reward in a block, mines +/// the block and adds it to the chain, with option transactions included. +/// Helpful for building up precise wallet balances for testing. +pub fn award_block_to_wallet<'a, L, C, K>( + chain: &Chain, + txs: &[Transaction], + wallet: Arc + 'a>>>, + keychain_mask: Option<&SecretKey>, +) -> Result<(), libwallet::Error> +where + L: WalletLCProvider<'a, C, K>, + C: NodeClient + 'a, + K: keychain::Keychain + 'a, +{ + let prev = chain.head_header().unwrap(); + let block = create_block_for_wallet(chain, prev, txs, wallet, keychain_mask)?; + process_block(chain, block); + Ok(()) +} + +pub fn process_block(chain: &Chain, block: core::core::Block) { + chain.process_block(block, chain::Options::MINE).unwrap(); + chain.validate(false).unwrap(); +} + +/// Award a blocks to a wallet directly +pub fn award_blocks_to_wallet<'a, L, C, K>( + chain: &Chain, + wallet: Arc + 'a>>>, + keychain_mask: Option<&SecretKey>, + number: usize, + pause_between: bool, +) -> Result<(), libwallet::Error> +where + L: WalletLCProvider<'a, C, K>, + C: NodeClient + 'a, + K: keychain::Keychain + 'a, +{ + for _ in 0..number { + award_block_to_wallet(chain, &[], wallet.clone(), keychain_mask)?; + if pause_between { + thread::sleep(std::time::Duration::from_millis(100)); + } + } + Ok(()) +} + +/// send an amount to a destination +pub fn send_to_dest<'a, L, C, K>( + wallet: Arc>>>, + keychain_mask: Option<&SecretKey>, + client: LocalWalletClient, + dest: &str, + amount: u64, + test_mode: bool, +) -> Result<(), libwallet::Error> +where + L: WalletLCProvider<'a, C, K>, + C: NodeClient + 'a, + K: keychain::Keychain + 'a, +{ + let slate = { + let mut w_lock = wallet.lock(); + let w = w_lock.lc_provider()?.wallet_inst()?; + let args = InitTxArgs { + src_acct_name: None, + amount, + minimum_confirmations: 2, + max_outputs: 500, + num_change_outputs: 1, + selection_strategy_is_use_all: true, + ..Default::default() + }; + let slate_i = owner::init_send_tx(&mut **w, keychain_mask, args, test_mode)?; + let slate = client.send_tx_slate_direct(dest, &slate_i)?; + owner::tx_lock_outputs(&mut **w, keychain_mask, &slate)?; + owner::finalize_tx(&mut **w, keychain_mask, &slate)? + }; + let client = { + let mut w_lock = wallet.lock(); + let w = w_lock.lc_provider()?.wallet_inst()?; + w.w2n_client().clone() + }; + owner::post_tx(&client, slate.tx_or_err()?, false)?; // mines a block + Ok(()) +} + +/// get wallet info totals +pub fn wallet_info<'a, L, C, K>( + wallet: Arc>>>, + keychain_mask: Option<&SecretKey>, +) -> Result +where + L: WalletLCProvider<'a, C, K>, + C: NodeClient + 'a, + K: keychain::Keychain + 'a, +{ + let (wallet_refreshed, wallet_info) = + owner::retrieve_summary_info(wallet, keychain_mask, &None, true, 1)?; + assert!(wallet_refreshed); + Ok(wallet_info) +} diff --git a/impls/src/test_framework/testclient.rs b/impls/src/test_framework/testclient.rs new file mode 100644 index 0000000..05eb083 --- /dev/null +++ b/impls/src/test_framework/testclient.rs @@ -0,0 +1,644 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Test client that acts against a local instance of a node +//! so that wallet API can be fully exercised +//! Operates directly on a chain instance + +use crate::api::{self, LocatedTxKernel}; +use crate::chain::types::NoopAdapter; +use crate::chain::Chain; +use crate::core::core::{Transaction, TxKernel}; +use crate::core::global::{set_local_chain_type, ChainTypes}; +use crate::core::pow; +use crate::keychain::Keychain; +use crate::libwallet; +use crate::libwallet::api_impl::foreign; +use crate::libwallet::slate_versions::v4::SlateV4; +use crate::libwallet::{NodeClient, NodeVersionInfo, Slate, WalletInst, WalletLCProvider}; +use crate::util; +use crate::util::secp::key::SecretKey; +use crate::util::secp::pedersen; +use crate::util::secp::pedersen::Commitment; +use crate::util::{Mutex, ToHex}; +use serde_json; +use std::collections::HashMap; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::mpsc::{channel, Receiver, Sender}; +use std::sync::Arc; +use std::thread; +use std::time::Duration; + +/// Messages to simulate wallet requests/responses +#[derive(Clone, Debug)] +pub struct WalletProxyMessage { + /// sender ID + pub sender_id: String, + /// destination wallet (or server) + pub dest: String, + /// method (like a GET url) + pub method: String, + /// payload (json body) + pub body: String, +} + +/// communicates with a chain instance or other wallet +/// listener APIs via message queues +pub struct WalletProxy<'a, L, C, K> +where + L: WalletLCProvider<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + /// directory to create the chain in + pub chain_dir: String, + /// handle to chain itself + pub chain: Arc, + /// list of interested wallets + pub wallets: HashMap< + String, + ( + Sender, + Arc + 'a>>>, + Option, + ), + >, + /// simulate json send to another client + /// address, method, payload (simulate HTTP request) + pub tx: Sender, + /// simulate json receiving + pub rx: Receiver, + /// queue control + pub running: Arc, +} + +impl<'a, L, C, K> WalletProxy<'a, L, C, K> +where + L: WalletLCProvider<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + /// Create a new client that will communicate with the given grin node + pub fn new(chain_dir: &str) -> Self { + set_local_chain_type(ChainTypes::AutomatedTesting); + let genesis_block = pow::mine_genesis_block().unwrap(); + let dir_name = format!("{}/.grin", chain_dir); + let c = Chain::init( + dir_name, + Arc::new(NoopAdapter {}), + genesis_block, + pow::verify_size, + false, + ) + .unwrap(); + let (tx, rx) = channel(); + WalletProxy { + chain_dir: chain_dir.to_owned(), + chain: Arc::new(c), + tx: tx, + rx: rx, + wallets: HashMap::new(), + running: Arc::new(AtomicBool::new(false)), + } + } + + /// Add wallet with a given "address" + pub fn add_wallet( + &mut self, + addr: &str, + tx: Sender, + wallet: Arc + 'a>>>, + keychain_mask: Option, + ) { + self.wallets + .insert(addr.to_owned(), (tx, wallet, keychain_mask)); + } + + pub fn stop(&mut self) { + self.running.store(false, Ordering::Relaxed); + } + + /// Run the incoming message queue and respond more or less + /// synchronously + pub fn run(&mut self) -> Result<(), libwallet::Error> { + // We run the wallet_proxy within a spawned thread in tests. + // We set the local chain_type here within the thread. + set_local_chain_type(ChainTypes::AutomatedTesting); + + self.running.store(true, Ordering::Relaxed); + loop { + thread::sleep(Duration::from_millis(10)); + if !self.running.load(Ordering::Relaxed) { + info!("Proxy stopped"); + return Ok(()); + } + + // read queue + let m = match self.rx.recv_timeout(Duration::from_millis(10)) { + Ok(m) => m, + Err(_) => continue, + }; + trace!("Wallet Client Proxy Received: {:?}", m); + let resp = match m.method.as_ref() { + "get_chain_tip" => self.get_chain_tip(m)?, + "get_outputs_from_node" => self.get_outputs_from_node(m)?, + "get_outputs_by_pmmr_index" => self.get_outputs_by_pmmr_index(m)?, + "height_range_to_pmmr_indices" => self.height_range_to_pmmr_indices(m)?, + "send_tx_slate" => self.send_tx_slate(m)?, + "post_tx" => self.post_tx(m)?, + "get_kernel" => self.get_kernel(m)?, + _ => panic!("Unknown Wallet Proxy Message"), + }; + + self.respond(resp); + } + } + + /// Return a message to a given wallet client + fn respond(&mut self, m: WalletProxyMessage) { + if let Some(s) = self.wallets.get_mut(&m.dest) { + if let Err(e) = s.0.send(m.clone()) { + panic!("Error sending response from proxy: {:?}, {}", m, e); + } + } else { + panic!("Unknown wallet recipient for response message: {:?}", m); + } + } + + /// post transaction to the chain (and mine it, taking the reward) + fn post_tx(&mut self, m: WalletProxyMessage) -> Result { + let dest_wallet = self.wallets.get_mut(&m.sender_id).unwrap().1.clone(); + let dest_wallet_mask = self.wallets.get_mut(&m.sender_id).unwrap().2.clone(); + let tx: Transaction = serde_json::from_str(&m.body).map_err(|_| { + libwallet::Error::ClientCallback("Error parsing Transaction".to_owned()) + })?; + + super::award_block_to_wallet( + &self.chain, + &[tx], + dest_wallet, + (&dest_wallet_mask).as_ref(), + )?; + + Ok(WalletProxyMessage { + sender_id: "node".to_owned(), + dest: m.sender_id, + method: m.method, + body: "".to_owned(), + }) + } + + /// send tx slate + fn send_tx_slate( + &mut self, + m: WalletProxyMessage, + ) -> Result { + let dest_wallet = self.wallets.get_mut(&m.dest); + let wallet = match dest_wallet { + None => panic!("Unknown wallet destination for send_tx_slate: {:?}", m), + Some(w) => w, + }; + + let slate: SlateV4 = serde_json::from_str(&m.body) + .map_err(|_| libwallet::Error::ClientCallback("Error parsing TxWrapper".to_owned()))?; + + let slate: Slate = { + let mut w_lock = wallet.1.lock(); + let w = w_lock.lc_provider()?.wallet_inst()?; + let mask = wallet.2.clone(); + // receive tx + match foreign::receive_tx(&mut **w, (&mask).as_ref(), &Slate::from(slate), None, false) + { + Err(e) => { + return Ok(WalletProxyMessage { + sender_id: m.dest, + dest: m.sender_id, + method: m.method, + body: serde_json::to_string(&format!("Error: {}", e)).unwrap(), + }) + } + Ok(s) => s, + } + }; + + Ok(WalletProxyMessage { + sender_id: m.dest, + dest: m.sender_id, + method: m.method, + body: serde_json::to_string(&SlateV4::from(slate)).unwrap(), + }) + } + + /// get chain height + fn get_chain_tip( + &mut self, + m: WalletProxyMessage, + ) -> Result { + let height = self.chain.head().unwrap().height; + let hash = self.chain.head().unwrap().last_block_h.to_hex(); + + Ok(WalletProxyMessage { + sender_id: "node".to_owned(), + dest: m.sender_id, + method: m.method, + body: format!("{},{}", height, hash), + }) + } + + /// get api outputs + fn get_outputs_from_node( + &mut self, + m: WalletProxyMessage, + ) -> Result { + let split = m.body.split(','); + //let mut api_outputs: HashMap = HashMap::new(); + let mut outputs: Vec = vec![]; + for o in split { + let o_str = String::from(o); + if o_str.is_empty() { + continue; + } + let c = util::from_hex(&o_str).unwrap(); + let commit = Commitment::from_vec(c); + let out = super::get_output_local(&self.chain.clone(), commit); + if let Some(o) = out { + outputs.push(o); + } + } + Ok(WalletProxyMessage { + sender_id: "node".to_owned(), + dest: m.sender_id, + method: m.method, + body: serde_json::to_string(&outputs).unwrap(), + }) + } + + /// get api outputs + fn get_outputs_by_pmmr_index( + &mut self, + m: WalletProxyMessage, + ) -> Result { + let split = m.body.split(',').collect::>(); + let start_index = std::cmp::max(split[0].parse::().unwrap(), 1); + let max = split[1].parse::().unwrap(); + let end_index = split[2].parse::().unwrap(); + let end_index = match end_index { + 0 => None, + e => Some(e), + }; + let ol = + super::get_outputs_by_pmmr_index_local(self.chain.clone(), start_index, end_index, max); + Ok(WalletProxyMessage { + sender_id: "node".to_owned(), + dest: m.sender_id, + method: m.method, + body: serde_json::to_string(&ol).unwrap(), + }) + } + + /// get api outputs by height + fn height_range_to_pmmr_indices( + &mut self, + m: WalletProxyMessage, + ) -> Result { + let split = m.body.split(',').collect::>(); + let start_index = split[0].parse::().unwrap(); + let end_index = split[1].parse::().unwrap(); + let end_index = match end_index { + 0 => None, + e => Some(e), + }; + let ol = + super::height_range_to_pmmr_indices_local(self.chain.clone(), start_index, end_index); + Ok(WalletProxyMessage { + sender_id: "node".to_owned(), + dest: m.sender_id, + method: m.method, + body: serde_json::to_string(&ol).unwrap(), + }) + } + + /// get kernel + fn get_kernel( + &mut self, + m: WalletProxyMessage, + ) -> Result { + let split = m.body.split(',').collect::>(); + let excess = split[0].parse::().unwrap(); + let min = split[1].parse::().unwrap(); + let max = split[2].parse::().unwrap(); + let commit_bytes = util::from_hex(&excess).unwrap(); + let commit = pedersen::Commitment::from_vec(commit_bytes); + let min = match min { + 0 => None, + m => Some(m), + }; + let max = match max { + 0 => None, + m => Some(m), + }; + let k = super::get_kernel_local(self.chain.clone(), &commit, min, max); + Ok(WalletProxyMessage { + sender_id: "node".to_owned(), + dest: m.sender_id, + method: m.method, + body: serde_json::to_string(&k).unwrap(), + }) + } +} + +#[derive(Clone)] +pub struct LocalWalletClient { + /// wallet identifier for the proxy queue + pub id: String, + /// proxy's tx queue (receive messages from other wallets or node + pub proxy_tx: Arc>>, + /// my rx queue + pub rx: Arc>>, + /// my tx queue + pub tx: Arc>>, +} + +impl LocalWalletClient { + /// new + pub fn new(id: &str, proxy_rx: Sender) -> Self { + let (tx, rx) = channel(); + LocalWalletClient { + id: id.to_owned(), + proxy_tx: Arc::new(Mutex::new(proxy_rx)), + rx: Arc::new(Mutex::new(rx)), + tx: Arc::new(Mutex::new(tx)), + } + } + + /// get an instance of the send queue for other senders + pub fn get_send_instance(&self) -> Sender { + self.tx.lock().clone() + } + + /// Send the slate to a listening wallet instance + pub fn send_tx_slate_direct( + &self, + dest: &str, + slate: &Slate, + ) -> Result { + let m = WalletProxyMessage { + sender_id: self.id.clone(), + dest: dest.to_owned(), + method: "send_tx_slate".to_owned(), + body: serde_json::to_string(&SlateV4::from(slate)).unwrap(), + }; + { + let p = self.proxy_tx.lock(); + p.send(m) + .map_err(|_| libwallet::Error::ClientCallback("Send TX Slate".to_owned()))?; + } + let r = self.rx.lock(); + let m = r.recv().unwrap(); + trace!("Received send_tx_slate response: {:?}", m.clone()); + let slate: SlateV4 = serde_json::from_str(&m.body).map_err(|_| { + libwallet::Error::ClientCallback("Parsing send_tx_slate response".to_owned()) + })?; + Ok(Slate::from(slate)) + } +} + +impl NodeClient for LocalWalletClient { + fn node_url(&self) -> &str { + "node" + } + fn node_api_secret(&self) -> Option { + None + } + fn set_node_url(&mut self, _node_url: &str) {} + fn set_node_api_secret(&mut self, _node_api_secret: Option) {} + fn get_version_info(&mut self) -> Option { + None + } + /// Posts a transaction to a grin node + /// In this case it will create a new block with award rewarded to + fn post_tx(&self, tx: &Transaction, _fluff: bool) -> Result<(), libwallet::Error> { + let m = WalletProxyMessage { + sender_id: self.id.clone(), + dest: self.node_url().to_owned(), + method: "post_tx".to_owned(), + body: serde_json::to_string(tx).unwrap(), + }; + { + let p = self.proxy_tx.lock(); + p.send(m) + .map_err(|_| libwallet::Error::ClientCallback("post_tx send".to_owned()))?; + } + let r = self.rx.lock(); + let m = r.recv().unwrap(); + trace!("Received post_tx response: {:?}", m); + Ok(()) + } + + /// Return the chain tip from a given node + fn get_chain_tip(&self) -> Result<(u64, String), libwallet::Error> { + let m = WalletProxyMessage { + sender_id: self.id.clone(), + dest: self.node_url().to_owned(), + method: "get_chain_tip".to_owned(), + body: "".to_owned(), + }; + { + let p = self.proxy_tx.lock(); + p.send(m).map_err(|_| { + libwallet::Error::ClientCallback("Get chain height send".to_owned()) + })?; + } + let r = self.rx.lock(); + let m = r.recv().unwrap(); + trace!("Received get_chain_tip response: {:?}", m.clone()); + let res = m.body.parse::().map_err(|_| { + libwallet::Error::ClientCallback("Parsing get_height response".to_owned()) + })?; + let split: Vec<&str> = res.split(',').collect(); + Ok((split[0].parse::().unwrap(), split[1].to_owned())) + } + + /// Retrieve outputs from node + fn get_outputs_from_node( + &self, + wallet_outputs: Vec, + ) -> Result, libwallet::Error> { + let query_params: Vec = wallet_outputs + .iter() + .map(|commit| commit.as_ref().to_hex()) + .collect(); + let query_str = query_params.join(","); + let m = WalletProxyMessage { + sender_id: self.id.clone(), + dest: self.node_url().to_owned(), + method: "get_outputs_from_node".to_owned(), + body: query_str, + }; + { + let p = self.proxy_tx.lock(); + p.send(m).map_err(|_| { + libwallet::Error::ClientCallback("Get outputs from node send".to_owned()) + })?; + } + let r = self.rx.lock(); + let m = r.recv().unwrap(); + let outputs: Vec = serde_json::from_str(&m.body).unwrap(); + let mut api_outputs: HashMap = HashMap::new(); + for out in outputs { + api_outputs.insert( + out.commit.commit(), + (out.commit.to_hex(), out.height, out.mmr_index), + ); + } + Ok(api_outputs) + } + + fn get_kernel( + &mut self, + excess: &pedersen::Commitment, + min_height: Option, + max_height: Option, + ) -> Result, libwallet::Error> { + let mut query = format!("{},", excess.0.as_ref().to_hex()); + if let Some(h) = min_height { + query += &format!("{},", h); + } else { + query += "0," + } + if let Some(h) = max_height { + query += &format!("{}", h); + } else { + query += "0" + } + + let m = WalletProxyMessage { + sender_id: self.id.clone(), + dest: self.node_url().to_owned(), + method: "get_kernel".to_owned(), + body: query, + }; + { + let p = self.proxy_tx.lock(); + p.send(m).map_err(|_| { + libwallet::Error::ClientCallback( + "Get outputs from node by PMMR index send".to_owned(), + ) + })?; + } + let r = self.rx.lock(); + let m = r.recv().unwrap(); + let res: Option = serde_json::from_str(&m.body).map_err(|_| { + libwallet::Error::ClientCallback("Get transaction kernels send".to_owned()) + })?; + match res { + Some(k) => Ok(Some((k.tx_kernel, k.height, k.mmr_index))), + None => Ok(None), + } + } + + fn get_outputs_by_pmmr_index( + &self, + start_index: u64, + end_index: Option, + max_outputs: u64, + ) -> Result< + ( + u64, + u64, + Vec<(pedersen::Commitment, pedersen::RangeProof, bool, u64, u64)>, + ), + libwallet::Error, + > { + // start index, max + let mut query_str = format!("{},{}", start_index, max_outputs); + match end_index { + Some(e) => query_str = format!("{},{}", query_str, e), + None => query_str = format!("{},0", query_str), + }; + let m = WalletProxyMessage { + sender_id: self.id.clone(), + dest: self.node_url().to_owned(), + method: "get_outputs_by_pmmr_index".to_owned(), + body: query_str, + }; + { + let p = self.proxy_tx.lock(); + p.send(m).map_err(|_| { + libwallet::Error::ClientCallback( + "Get outputs from node by PMMR index send".to_owned(), + ) + })?; + } + + let r = self.rx.lock(); + let m = r.recv().unwrap(); + let o: api::OutputListing = serde_json::from_str(&m.body).unwrap(); + + let mut api_outputs: Vec<(pedersen::Commitment, pedersen::RangeProof, bool, u64, u64)> = + Vec::new(); + + for out in o.outputs { + let is_coinbase = match out.output_type { + api::OutputType::Coinbase => true, + api::OutputType::Transaction => false, + }; + api_outputs.push(( + out.commit, + out.range_proof().unwrap(), + is_coinbase, + out.block_height.unwrap(), + out.mmr_index, + )); + } + Ok((o.highest_index, o.last_retrieved_index, api_outputs)) + } + + fn height_range_to_pmmr_indices( + &self, + start_height: u64, + end_height: Option, + ) -> Result<(u64, u64), libwallet::Error> { + // start index, max + let mut query_str = format!("{}", start_height); + match end_height { + Some(e) => query_str = format!("{},{}", query_str, e), + None => query_str = format!("{},0", query_str), + }; + let m = WalletProxyMessage { + sender_id: self.id.clone(), + dest: self.node_url().to_owned(), + method: "height_range_to_pmmr_indices".to_owned(), + body: query_str, + }; + { + let p = self.proxy_tx.lock(); + p.send(m).map_err(|_| { + libwallet::Error::ClientCallback("Get outputs within height range send".to_owned()) + })?; + } + + let r = self.rx.lock(); + let m = r.recv().unwrap(); + let o: api::OutputListing = serde_json::from_str(&m.body).unwrap(); + Ok((o.last_retrieved_index, o.highest_index)) + } +} +unsafe impl<'a, L, C, K> Send for WalletProxy<'a, L, C, K> +where + L: WalletLCProvider<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ +} diff --git a/impls/src/tor/bridge.rs b/impls/src/tor/bridge.rs new file mode 100644 index 0000000..834e0b9 --- /dev/null +++ b/impls/src/tor/bridge.rs @@ -0,0 +1,660 @@ +// Copyright 2022 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::Error; +use base64; +use grin_wallet_config::types::TorBridgeConfig; +use std::collections::HashMap; +use std::convert::TryFrom; +use std::net::SocketAddr; +use std::{env, str}; +use url::{Host, Url}; + +use crate::tor::proxy::TorProxy; + +#[cfg(windows)] +const OBFS4_EXE_NAME: &str = "obfs4proxy.exe"; +#[cfg(not(windows))] +const OBFS4_EXE_NAME: &str = "obfs4proxy"; + +#[cfg(windows)] +const SNOWFLAKE_EXE_NAME: &str = "snowflake-client.exe"; +#[cfg(not(windows))] +const SNOWFLAKE_EXE_NAME: &str = "snowflake-client"; + +pub struct FlagParser<'a> { + /// line left to be parsed + line: &'a str, + /// all flags, bool flags and flags that takes a value + flags: Vec<&'a str>, + /// bool flags, present in the client line + bool_flags: Vec<&'a str>, + /// is current parsed flag is a bool + is_bool_flag: bool, + // parsing client or bridge line + client: bool, +} + +/// Flag parser, help to retrieve flags and it's value whether on the bridge or client option line +impl<'a> FlagParser<'a> { + pub fn new(line: &'a str, flags: Vec<&'a str>, bool_flags: Vec<&'a str>, client: bool) -> Self { + Self { + line, + flags, + bool_flags, + is_bool_flag: false, + client, + } + } + + /// Used only on the client option line parsing, help to retrieve a known flags + fn is_flag(&mut self) -> usize { + let mut split_index = 0; + let line = self.line.split_whitespace(); + self.is_bool_flag = false; + for is_flag in line { + let index = self.flags.iter().position(|&flag| flag == is_flag); + if let Some(m) = index { + let i = self.line.find(is_flag).unwrap(); + split_index = i + is_flag.len() + 1; + let idx_b_flag = self + .bool_flags + .iter() + .position(|&bool_flag| bool_flag == is_flag); + if let Some(i) = idx_b_flag { + self.is_bool_flag = true; + self.bool_flags.remove(i); + } + self.flags.remove(m); + return split_index; + } + } + split_index + } + + /// Determine at which index we should take the value linked to its flags + fn end(&mut self, is_bool_flag: bool, right: &str) -> usize { + if is_bool_flag { + 0 + } else if right.starts_with('"') { + right[1..].find('"').unwrap_or(0) + 2 + } else { + right.find(' ').unwrap_or(right.len()) + } + } +} + +impl<'a> Iterator for FlagParser<'a> { + type Item = (&'a str, &'a str); + + fn next(&mut self) -> Option { + let (left, right) = if self.client { + // Client parser + let split_index = self.is_flag(); + let (l, r) = self.line.split_at(split_index); + (l, r) + } else { + // Bridge parser + let split_index = self.line.find("=")?; + let (l, r) = self.line.split_at(split_index + 1); + (l, r) + }; + let end = self.end(self.is_bool_flag, right); + let key = left.split_whitespace().last()?; + let val = &right[..end]; + self.line = &right[end..].trim(); + Some((key, val)) + } +} + +/// Every args field that could be in the bridge line +/// obfs4 args : https://github.com/Yawning/obfs4/blob/40245c4a1cf221395c59d1f4bf274127045352f9/transports/obfs4/obfs4.go#L86-L91 +/// meek_lite args : https://github.com/Yawning/obfs4/blob/40245c4a1cf221395c59d1f4bf274127045352f9/transports/meeklite/meek.go#L93-L127 +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +pub struct Transport { + /// transport type: obfs4, meek_lite, meek, snowflake + pub transport: Option, + /// server address + pub server: Option, + /// fingerprint + pub fingerprint: Option, + /// certificate (obfs4) + pub cert: Option, + /// IAT obfuscation: 0 disabled, 1 enabled, 2 paranoid (obfs4) + pub iatmode: Option, + /// URL of signaling broker (meek) + pub url: Option, + /// optional - front domain (meek) + pub front: Option, + /// optional - URL of AMP cache to use as a proxy for signaling (meek) + pub utls: Option, + /// optional - HPKP disable argument. (meek) + pub disablehpkp: Option, +} + +impl Default for Transport { + fn default() -> Transport { + Transport { + transport: None, + server: None, + fingerprint: None, + cert: None, + iatmode: None, + url: None, + front: None, + utls: None, + disablehpkp: None, + } + } +} + +impl Transport { + /// Parse the server address of the bridge line + fn parse_socketaddr_arg(arg: Option<&&str>) -> Result { + match arg { + Some(addr) => { + let address = addr.parse::().map_err(|_e| { + Error::TorBridge(format!("Invalid bridge server address: {}", addr)) + })?; + Ok(address.to_string()) + } + None => { + let msg = format!("Missing bridge server address"); + Err(Error::TorBridge(msg)) + } + } + } + + /// Parse the fingerprint of the bridge line (obfs4/snowflake/meek) + fn parse_fingerprint_arg(arg: Option<&&str>) -> Result, Error> { + match arg { + Some(f) => { + let fgp = f.to_owned(); + let is_hex = fgp.chars().all(|c| c.is_ascii_hexdigit()); + let fingerprint = fgp.to_uppercase(); + if !(is_hex && fingerprint.len() == 40) { + let msg = format!("Invalid fingerprint: {}", fingerprint); + return Err(Error::TorBridge(msg)); + } + Ok(Some(fingerprint)) + } + None => Ok(None), + } + } + /// Parse the certificate of the bridge line (obfs4) + pub fn parse_cert_arg(arg: &str) -> Result { + let cert_vec = base64::decode(arg).map_err(|_e| { + Error::TorBridge(format!( + "Invalid certificate, error decoding bridge certificate: {}", + arg + )) + })?; + if cert_vec.len() != 52 { + let msg = format!("Invalid certificate: {}", arg); + return Err(Error::TorBridge(msg).into()); + } + Ok(arg.to_string()) + } + /// Parse the iatmode of the bridge line (obfs4) + pub fn parse_iatmode_arg(arg: &str) -> Result { + let iatmode = arg.parse::().unwrap_or(0); + if !((0..3).contains(&iatmode)) { + let msg = format!("Invalid iatmode: {}, must be between 0 and 2", iatmode); + return Err(Error::TorBridge(msg)); + } + Ok(iatmode.to_string()) + } + + /// Parse the max value for the arg -max in the client line option (snowflake) + fn parse_hpkp_arg(arg: &str) -> Result { + let max = arg.parse::().map_err(|_e| { + Error::TorBridge( + format!("Invalid -max value: {}, must be \"true\" or \"false\"", arg).into(), + ) + })?; + Ok(max.to_string()) + } +} + +// Client Plugin such as snowflake or obfs4proxy +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +pub struct PluginClient { + // Path plugin client + pub path: Option, + // Plugin client option + pub option: Option, +} + +impl Default for PluginClient { + fn default() -> PluginClient { + PluginClient { + path: None, + option: None, + } + } +} + +impl PluginClient { + /// Get the hashmap key(argument) and attached value of the client option line. + pub fn get_flags(s: &str) -> HashMap<&str, &str> { + let flags = vec![ + "-url", + "-front", + "-ice", + "-log", + "-log-to-state-dir", + "-keep-local-addresses", + "-unsafe-logging", + "-max", + "-loglevel", + "-enableLogging", + "-unsafeLogging", + ]; + let bool_flags = vec![ + "-log-to-state-dir", + "-keep-local-addresses", + "-unsafe-logging", + "-enableLogging", + "-unsafeLogging", + ]; + FlagParser::new(s, flags, bool_flags, true).collect() + } + + /// Try to find the plugin client path + pub fn get_client_path(plugin: &str) -> Result { + let plugin_path = env::var_os("PATH").and_then(|path| { + env::split_paths(&path) + .filter_map(|dir| { + let full_path = dir.join(plugin); + if full_path.is_file() { + Some(full_path) + } else { + None + } + }) + .next() + }); + match plugin_path { + Some(path) => Ok(path.into_os_string().into_string().unwrap()), + None => { + let msg = format!("Transport client \"{}\" is missing, make sure it's installed and on your path.", plugin); + Err(Error::TorBridge(msg)) + } + } + } + + /// Parse the URL value for the arg -url in the client line option (snowflake) + fn parse_url_arg(arg: &str) -> Result { + let url = arg + .parse::() + .map_err(|_e| Error::TorBridge(format!("Invalid -url value: {}", arg)))?; + Ok(url.to_string()) + } + + /// Parse the DNS domain value for the arg -front in the client line option (snowflake) + fn parse_front_arg(arg: &str) -> Result { + let front = Host::parse(arg) + .map_err(|_e| Error::TorBridge(format!("Invalid -front hostname value: {}", arg)))?; + match front { + Host::Domain(_) => Ok(front.to_string()), + Host::Ipv4(_) | Host::Ipv6(_) => { + let msg = format!( + "Invalid front argument: {}, in the client option. Must be a DNS Domain", + front + ); + Err(Error::TorBridge(msg)) + } + } + } + + /// Parse the ICE address value for the arg -ice in the client line option (snowflake) + fn parse_ice_arg(arg: &str) -> Result { + let ice_addr = arg.trim(); + let vec_ice_addr = ice_addr.split(","); + for addr in vec_ice_addr { + let addr = addr.to_lowercase(); + if addr.starts_with("stun:") || addr.starts_with("turn:") { + let address = addr.replace("stun:", "").replace("turn:", ""); + let _p_address = TorProxy::parse_address(&address) + .map_err(|e| Error::TorBridge(format!("{}", e)))?; + } else { + let msg = format!( + "Invalid ICE address: {}. Must be a stun or turn address", + addr + ); + return Err(Error::TorBridge(msg).into()); + } + } + Ok(ice_addr.to_string()) + } + + /// Parse the max value for the arg -max in the client line option (snowflake) + fn parse_max_arg(arg: &str) -> Result { + match arg.parse::() { + Ok(max) => Ok(max.to_string()), + Err(_e) => { + let msg = format!("Invalid -max argument: {} in the client option.", arg); + Err(Error::TorBridge(msg)) + } + } + } + + /// Parse the loglevel value for the arg -loglevel in the client line option (obfs4) + fn parse_loglevel_arg(arg: &str) -> Result { + let log_level = arg.to_uppercase(); + match log_level.as_str() { + "ERROR" | "WARN" | "INFO" | "DEBUG" => Ok(log_level.to_string()), + _ => { + let msg = format!("Invalid log level argurment: {}, in the client option. Must be: ERROR, WARN, INFO or DEBUG", log_level); + Err(Error::TorBridge(msg)) + } + } + } + + /// Parse and verify if the client option line of obfs4proxy or snowflake are correct + /// Obfs4proxy client args : https://github.com/Yawning/obfs4/blob/40245c4a1cf221395c59d1f4bf274127045352f9/obfs4proxy/obfs4proxy.go#L313-L316 + /// Snowflake client args : https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/-/blob/main/client/snowflake.go#L123-132 + pub fn parse_client(option: &str, snowflake: bool) -> Result { + let hm_flags = PluginClient::get_flags(option); + let mut string = String::from(""); + if snowflake { + let (ck_url, ck_ice) = (hm_flags.contains_key("-url"), hm_flags.contains_key("-ice")); + if !(ck_url || ck_ice) { + let msg = if !ck_url { + format!("Missing URL argurment for snowflake transport, specify \"-url\"") + } else { + format!("Missing ICE argurment for snowflake transport, specify \"-ice\"") + }; + return Err(Error::TorBridge(msg)); + } + for (key, value) in hm_flags { + let p_value = match key { + "-url" => PluginClient::parse_url_arg(value)?, + "-front" => PluginClient::parse_front_arg(value)?, + "-ice" => PluginClient::parse_ice_arg(value)?, + "-ampcache" => value.to_string(), + "-log" => value.to_string(), + "-log-to-state-dir" => String::from(""), + "-keep-local-addresses" => String::from(""), + "-unsafe-logging" => String::from(""), + "-max" => PluginClient::parse_max_arg(value)?, + _ => continue, + }; + string.push_str(format!(" {} {}", key, p_value).trim_end()) + } + } else { + for (key, value) in hm_flags { + let p_value = match key { + "-loglevel" => PluginClient::parse_loglevel_arg(value)?, + "-enableLogging" => String::from(""), + "-unsafeLogging" => String::from(""), + _ => continue, + }; + string.push_str(format!(" {} {}", key, p_value).trim_end()) + } + } + let p_string = string.trim_start().to_string(); + Ok(p_string) + } +} + +/// Tor Bridge Field +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +pub struct TorBridge { + /// tor bridge (transport field) + pub bridge: Transport, + // tor bridge plugin client (path and option) + pub client: PluginClient, +} + +impl Default for TorBridge { + fn default() -> TorBridge { + TorBridge { + bridge: Transport::default(), + client: PluginClient::default(), + } + } +} + +impl TorBridge { + /// Get the hashmap key(argument) and attached value of the bridge line. r + pub fn get_flags(s: &str) -> HashMap<&str, &str> { + FlagParser::new(s, vec![], vec![], false).collect() + } + + /// Bridge and client option convertion to hashmap, facility for the writing of the torrc config + pub fn to_hashmap(&self) -> Result, Error> { + let bridge = self.bridge.clone(); + let client = self.client.clone(); + let transport = bridge.transport.as_ref().unwrap().as_str(); + let mut ret_val = HashMap::new(); + match transport { + "obfs4" => { + let string_un = &String::from(""); + let chskey = "ClientTransportPlugin".to_string(); + let chsvalue = format!( + "{} exec {} {}", + transport, + client.path.as_ref().unwrap(), + client.option.as_ref().unwrap_or(string_un) + ); + ret_val.insert(chskey, chsvalue); + + let hskey = "Bridge".to_string(); + let mut hsvalue = format!("{} {}", transport, bridge.server.as_ref().unwrap()); + if let Some(fingerprint) = bridge.fingerprint { + hsvalue.push_str(format!(" {}", fingerprint).as_str()) + } + hsvalue.push_str(format!(" cert={}", bridge.cert.unwrap()).as_str()); + hsvalue.push_str(format!(" iat-mode={}", bridge.iatmode.unwrap()).as_str()); + ret_val.insert(hskey, hsvalue); + + Ok(ret_val) + } + + "meek_lite" => { + let chskey = "ClientTransportPlugin".to_string(); + let mut chsvalue = format!("{} exec {}", transport, client.path.as_ref().unwrap()); + if let Some(option) = client.option { + chsvalue.push_str(format!(" {}", option).as_str()) + } + ret_val.insert(chskey, chsvalue); + + let hskey = "Bridge".to_string(); + let mut hsvalue = format!("{} {}", transport, bridge.server.as_ref().unwrap()); + if let Some(fingerprint) = bridge.fingerprint { + hsvalue.push_str(format!(" {}", fingerprint).as_str()) + } + + hsvalue.push_str(format!(" url={}", bridge.url.as_ref().unwrap()).as_str()); + + if let Some(front) = bridge.front { + hsvalue.push_str(format!(" front={}", front).as_str()) + } + if let Some(utls) = bridge.utls { + hsvalue.push_str(format!(" utls={}", utls).as_str()) + } + if let Some(disablehpkp) = bridge.disablehpkp { + hsvalue.push_str(format!(" disableHPKP={}", disablehpkp).as_str()) + } + ret_val.insert(hskey, hsvalue); + Ok(ret_val) + } + + "snowflake" => { + let chskey = "ClientTransportPlugin".to_string(); + let chsvalue = format!( + "{} exec {} {}", + transport, + client.path.as_ref().unwrap(), + client.option.as_ref().unwrap() + ); + ret_val.insert(chskey, chsvalue); + + let hskey = "Bridge".to_string(); + let mut hsvalue = format!("{} {}", transport, bridge.server.as_ref().unwrap()); + if let Some(fingerprint) = bridge.fingerprint { + hsvalue.push_str(format!(" {}", fingerprint).as_str()) + } + ret_val.insert(hskey, hsvalue); + Ok(ret_val) + } + + _ => { + let msg = format!( + "Invalid transport method: {} - must be obfs4/meek_lite/meek/snowflake", + transport + ); + Err(Error::TorBridge(msg)) + } + } + } +} + +impl TryFrom for TorBridge { + type Error = Error; + + fn try_from(tbc: TorBridgeConfig) -> Result { + let bridge = match tbc.bridge_line { + Some(b) => b, + None => return Ok(TorBridge::default()), + }; + let flags = TorBridge::get_flags(&bridge); + let split = bridge.split_whitespace().collect::>(); + let mut iter = split.iter(); + let transport = iter.next().unwrap().to_lowercase(); + match transport.as_str() { + "obfs4" => { + let socketaddr = Transport::parse_socketaddr_arg(iter.next())?; + let fingerprint = Transport::parse_fingerprint_arg(iter.next())?; + let cert = match flags.get_key_value("cert=") { + Some(hm) => Transport::parse_cert_arg(hm.1)?, + None => { + let msg = + format!("Missing cert argurment in obfs4 transport, specify \"cert=\""); + return Err(Error::TorBridge(msg)); + } + }; + let iatmode = match flags.get_key_value("iat-mode=") { + Some(hm) => Transport::parse_iatmode_arg(hm.1)?, + None => String::from("0"), + }; + let path = PluginClient::get_client_path(OBFS4_EXE_NAME)?; + let option = match tbc.client_option { + Some(o) => Some(PluginClient::parse_client(&o, false)?), + None => None, + }; + let tbpc = TorBridge { + bridge: Transport { + transport: Some("obfs4".into()), + server: Some(socketaddr.to_string()), + fingerprint: fingerprint, + cert: Some(cert.into()), + iatmode: Some(iatmode), + ..Transport::default() + }, + client: PluginClient { + path: Some(path), + option: option, + }, + }; + Ok(tbpc) + } + + "meek_lite" | "meek" => { + let socketaddr = Transport::parse_socketaddr_arg(iter.next())?; + let fingerprint = Transport::parse_fingerprint_arg(iter.next())?; + let url = match flags.get_key_value("url=") { + Some(hm) => PluginClient::parse_url_arg(hm.1)?, + None => { + let msg = format!( + "Missing url argurment in meek_lite transport, specify \"url=\"" + ); + return Err(Error::TorBridge(msg)); + } + }; + let front = match flags.get_key_value("front=") { + Some(hm) => Some(PluginClient::parse_front_arg(hm.1)?), + None => None, + }; + let utls = match flags.get_key_value("utls=") { + Some(hm) => Some(hm.1.to_string()), + None => None, + }; + let disablehpkp = match flags.get_key_value("disablehpkp=") { + Some(hm) => Some(Transport::parse_hpkp_arg(hm.1)?), + None => None, + }; + let path = PluginClient::get_client_path(OBFS4_EXE_NAME)?; + let option = match tbc.client_option { + Some(o) => Some(PluginClient::parse_client(&o, false)?), + None => None, + }; + let tbpc = TorBridge { + bridge: Transport { + transport: Some("meek_lite".into()), + server: Some(socketaddr.to_string()), + fingerprint: fingerprint, + url: Some(url), + front: front, + utls: utls, + disablehpkp: disablehpkp, + ..Transport::default() + }, + client: PluginClient { + path: Some(path), + option: option, + }, + }; + Ok(tbpc) + } + + "snowflake" => { + let socketaddr = Transport::parse_socketaddr_arg(iter.next())?; + let fingerprint = Transport::parse_fingerprint_arg(iter.next())?; + let path = PluginClient::get_client_path(SNOWFLAKE_EXE_NAME)?; + let option = match tbc.client_option { + Some(o) => PluginClient::parse_client(&o, true)?, + None => { + let url = + "-url https://snowflake-broker.torproject.net.global.prod.fastly.net/"; + let front = "-front cdn.sstatic.net"; + let ice = "-ice stun:stun.l.google.com:19302,stun:stun.voip.blackberry.com:3478,stun:stun.altar.com.pl:3478,stun:stun.antisip.com:3478,stun:stun.bluesip.net:3478,stun:stun.dus.net:3478,stun:stun.epygi.com:3478,stun:stun.sonetel.com:3478,stun:stun.sonetel.net:3478,stun:stun.stunprotocol.org:3478,stun:stun.uls.co.za:3478,stun:stun.voipgate.com:3478,stun:stun.voys.nl:3478"; + format!("{} {} {}", url, front, ice) + } + }; + let tbpc = TorBridge { + bridge: Transport { + transport: Some("snowflake".into()), + server: Some(socketaddr.to_string()), + fingerprint: fingerprint, + ..Transport::default() + }, + client: PluginClient { + path: Some(path), + option: Some(option), + }, + }; + Ok(tbpc) + } + _ => { + let msg = format!( + "Invalid transport method: {} - must be obfs4/meek_lite/meek/snowflake", + transport + ); + Err(Error::TorBridge(msg)) + } + } + } +} diff --git a/impls/src/tor/config.rs b/impls/src/tor/config.rs new file mode 100644 index 0000000..d5e0374 --- /dev/null +++ b/impls/src/tor/config.rs @@ -0,0 +1,382 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Tor Configuration + Onion (Hidden) Service operations +use crate::util::secp::key::SecretKey; +use crate::Error; +use grin_wallet_util::OnionV3Address; + +use ed25519_dalek::ExpandedSecretKey; +use ed25519_dalek::PublicKey as DalekPublicKey; +use ed25519_dalek::SecretKey as DalekSecretKey; +use std::collections::HashMap; +use std::convert::TryFrom; +use std::fs::{self, File}; +use std::io::Write; +use std::path::{Path, MAIN_SEPARATOR}; +use std::string::String; + +const SEC_KEY_FILE: &str = "hs_ed25519_secret_key"; +const PUB_KEY_FILE: &str = "hs_ed25519_public_key"; +const HOSTNAME_FILE: &str = "hostname"; +const TORRC_FILE: &str = "torrc"; +const TOR_DATA_DIR: &str = "data"; +const AUTH_CLIENTS_DIR: &str = "authorized_clients"; +const HIDDEN_SERVICES_DIR: &str = "onion_service_addresses"; + +#[cfg(unix)] +fn set_permissions(file_path: &str) -> Result<(), Error> { + use std::os::unix::prelude::*; + fs::set_permissions(file_path, fs::Permissions::from_mode(0o700)).map_err(|_| Error::IO)?; + Ok(()) +} + +#[cfg(windows)] +fn set_permissions(_file_path: &str) -> Result<(), Error> { + Ok(()) +} + +struct TorRcConfigItem { + pub name: String, + pub value: String, +} + +impl TorRcConfigItem { + /// Create new + pub fn new(name: &str, value: &str) -> Self { + Self { + name: name.into(), + value: value.into(), + } + } +} + +struct TorRcConfig { + pub items: Vec, +} + +impl TorRcConfig { + /// Create new + pub fn new() -> Self { + Self { items: vec![] } + } + + /// add item + pub fn add_item(&mut self, name: &str, value: &str) { + self.items.push(TorRcConfigItem::new(name, value)); + } + + /// write to file + pub fn write_to_file(&self, file_path: &str) -> Result<(), Error> { + let mut file = File::create(file_path).map_err(|_| Error::IO)?; + for item in &self.items { + file.write_all(item.name.as_bytes()) + .map_err(|_| Error::IO)?; + file.write_all(b" ").map_err(|_| Error::IO)?; + file.write_all(item.value.as_bytes()) + .map_err(|_| Error::IO)?; + file.write_all(b"\n").map_err(|_| Error::IO)?; + } + Ok(()) + } +} + +pub fn create_onion_service_sec_key_file( + os_directory: &str, + sec_key: &DalekSecretKey, +) -> Result<(), Error> { + let key_file_path = &format!("{}{}{}", os_directory, MAIN_SEPARATOR, SEC_KEY_FILE); + let mut file = File::create(key_file_path).map_err(|_| Error::IO)?; + // Tag is always 32 bytes, so pad with null zeroes + file.write(b"== ed25519v1-secret: type0 ==\0\0\0") + .map_err(|_| Error::IO)?; + let expanded_skey: ExpandedSecretKey = ExpandedSecretKey::from(sec_key); + file.write_all(&expanded_skey.to_bytes()) + .map_err(|_| Error::IO)?; + Ok(()) +} + +pub fn create_onion_service_pub_key_file( + os_directory: &str, + pub_key: &DalekPublicKey, +) -> Result<(), Error> { + let key_file_path = &format!("{}{}{}", os_directory, MAIN_SEPARATOR, PUB_KEY_FILE); + let mut file = File::create(key_file_path).map_err(|_| Error::IO)?; + // Tag is always 32 bytes, so pad with null zeroes + file.write(b"== ed25519v1-public: type0 ==\0\0\0") + .map_err(|_| Error::IO)?; + file.write_all(pub_key.as_bytes()).map_err(|_| Error::IO)?; + Ok(()) +} + +pub fn create_onion_service_hostname_file(os_directory: &str, hostname: &str) -> Result<(), Error> { + let file_path = &format!("{}{}{}", os_directory, MAIN_SEPARATOR, HOSTNAME_FILE); + let mut file = File::create(file_path).map_err(|_| Error::IO)?; + file.write_all(&format!("{}.onion\n", hostname).as_bytes()) + .map_err(|_| Error::IO)?; + Ok(()) +} + +pub fn create_onion_auth_clients_dir(os_directory: &str) -> Result<(), Error> { + let auth_dir_path = &format!("{}{}{}", os_directory, MAIN_SEPARATOR, AUTH_CLIENTS_DIR); + fs::create_dir_all(auth_dir_path).map_err(|_| Error::IO)?; + Ok(()) +} +/// output an onion service config for the secret key, and return the address +pub fn output_onion_service_config( + tor_config_directory: &str, + sec_key: &SecretKey, +) -> Result { + let d_sec_key = DalekSecretKey::from_bytes(&sec_key.0) + .map_err(|_| Error::ED25519Key("Unable to parse private key".into()))?; + let address = OnionV3Address::from_private(&sec_key.0)?; + let hs_dir_file_path = format!( + "{}{}{}{}{}", + tor_config_directory, MAIN_SEPARATOR, HIDDEN_SERVICES_DIR, MAIN_SEPARATOR, address + ); + + // If file already exists, don't overwrite it, just return address + if Path::new(&hs_dir_file_path).exists() { + return Ok(address); + } + + // create directory if it doesn't exist + fs::create_dir_all(&hs_dir_file_path).map_err(|_| Error::IO)?; + + create_onion_service_sec_key_file(&hs_dir_file_path, &d_sec_key)?; + create_onion_service_pub_key_file(&hs_dir_file_path, &address.to_ed25519()?)?; + create_onion_service_hostname_file(&hs_dir_file_path, &address.to_string())?; + create_onion_auth_clients_dir(&hs_dir_file_path)?; + + set_permissions(&hs_dir_file_path)?; + + Ok(address) +} + +/// output torrc file given a list of hidden service directories +pub fn output_torrc( + tor_config_directory: &str, + wallet_listener_addr: &str, + socks_port: &str, + service_dirs: &[String], + hm_tor_bridge: HashMap, + hm_tor_proxy: HashMap, +) -> Result<(), Error> { + let torrc_file_path = format!("{}{}{}", tor_config_directory, MAIN_SEPARATOR, TORRC_FILE); + + let tor_data_dir = format!("./{}", TOR_DATA_DIR); + + let mut props = TorRcConfig::new(); + props.add_item("SocksPort", socks_port); + props.add_item("DataDirectory", &tor_data_dir); + + for dir in service_dirs { + let service_file_name = format!("./{}{}{}", HIDDEN_SERVICES_DIR, MAIN_SEPARATOR, dir); + props.add_item("HiddenServiceDir", &service_file_name); + props.add_item("HiddenServicePort", &format!("80 {}", wallet_listener_addr)); + } + + if !hm_tor_bridge.is_empty() { + props.add_item("UseBridges", "1"); + for (key, value) in hm_tor_bridge { + props.add_item(&key, &value); + } + } + + if !hm_tor_proxy.is_empty() { + for (key, value) in hm_tor_proxy { + props.add_item(&key, &value); + } + } + + props.write_to_file(&torrc_file_path)?; + + Ok(()) +} + +/// output entire tor config for a list of secret keys +pub fn output_tor_listener_config( + tor_config_directory: &str, + wallet_listener_addr: &str, + listener_keys: &[SecretKey], + hm_tor_bridge: HashMap, + hm_tor_proxy: HashMap, +) -> Result<(), Error> { + let tor_data_dir = format!("{}{}{}", tor_config_directory, MAIN_SEPARATOR, TOR_DATA_DIR); + + // create data directory if it doesn't exist + fs::create_dir_all(&tor_data_dir).map_err(|_| Error::IO)?; + + let mut service_dirs = vec![]; + + for k in listener_keys { + let service_dir = output_onion_service_config(tor_config_directory, &k)?; + service_dirs.push(service_dir.to_string()); + } + + // hidden service listener doesn't need a socks port + output_torrc( + tor_config_directory, + wallet_listener_addr, + "0", + &service_dirs, + hm_tor_bridge, + hm_tor_proxy, + )?; + + Ok(()) +} + +/// output tor config for a send +pub fn output_tor_sender_config( + tor_config_dir: &str, + socks_listener_addr: &str, + hm_tor_bridge: HashMap, + hm_tor_proxy: HashMap, +) -> Result<(), Error> { + // create data directory if it doesn't exist + fs::create_dir_all(&tor_config_dir).map_err(|_| Error::IO)?; + + output_torrc( + tor_config_dir, + "", + socks_listener_addr, + &[], + hm_tor_bridge, + hm_tor_proxy, + )?; + + Ok(()) +} + +pub fn is_tor_address(input: &str) -> Result<(), Error> { + match OnionV3Address::try_from(input) { + Ok(_) => Ok(()), + Err(e) => { + let msg = format!("{:?}", e); + Err(Error::NotOnion(msg).into()) + } + } +} + +pub fn complete_tor_address(input: &str) -> Result { + is_tor_address(input)?; + let mut input = input.to_uppercase(); + if !input.starts_with("HTTP://") && !input.starts_with("HTTPS://") { + input = format!("HTTP://{}", input); + } + if !input.ends_with(".ONION") { + input = format!("{}.ONION", input); + } + Ok(input.to_lowercase()) +} + +#[cfg(test)] +mod tests { + use super::*; + + use rand::rngs::mock::StepRng; + + use crate::util::{self, secp, static_secp_instance}; + + pub fn clean_output_dir(test_dir: &str) { + let _ = remove_dir_all::remove_dir_all(test_dir); + } + + pub fn setup(test_dir: &str) { + util::init_test_logger(); + clean_output_dir(test_dir); + } + + #[test] + fn test_service_config() -> Result<(), Error> { + let test_dir = "target/test_output/onion_service"; + setup(test_dir); + let secp_inst = static_secp_instance(); + let secp = secp_inst.lock(); + let mut test_rng = StepRng::new(1_234_567_890_u64, 1); + let sec_key = secp::key::SecretKey::new(&secp, &mut test_rng); + output_onion_service_config(test_dir, &sec_key)?; + clean_output_dir(test_dir); + Ok(()) + } + + #[test] + fn test_output_tor_config() -> Result<(), Error> { + let test_dir = "./target/test_output/tor"; + setup(test_dir); + let secp_inst = static_secp_instance(); + let secp = secp_inst.lock(); + let mut test_rng = StepRng::new(1_234_567_890_u64, 1); + let sec_key = secp::key::SecretKey::new(&secp, &mut test_rng); + let hm = HashMap::new(); + output_tor_listener_config(test_dir, "127.0.0.1:3415", &[sec_key], hm.clone(), hm)?; + clean_output_dir(test_dir); + Ok(()) + } + + #[test] + fn test_is_tor_address() -> Result<(), Error> { + assert!(is_tor_address("2a6at2obto3uvkpkitqp4wxcg6u36qf534eucbskqciturczzc5suyid").is_ok()); + assert!(is_tor_address("2a6at2obto3uvkpkitqp4wxcg6u36qf534eucbskqciturczzc5suyid").is_ok()); + assert!(is_tor_address("kcgiy5g6m76nzlzz4vyqmgdv34f6yokdqwfhdhaafanpo5p4fceibyid").is_ok()); + assert!(is_tor_address( + "http://kcgiy5g6m76nzlzz4vyqmgdv34f6yokdqwfhdhaafanpo5p4fceibyid.onion" + ) + .is_ok()); + assert!(is_tor_address( + "https://kcgiy5g6m76nzlzz4vyqmgdv34f6yokdqwfhdhaafanpo5p4fceibyid.onion" + ) + .is_ok()); + assert!( + is_tor_address("http://kcgiy5g6m76nzlzz4vyqmgdv34f6yokdqwfhdhaafanpo5p4fceibyid") + .is_ok() + ); + assert!( + is_tor_address("kcgiy5g6m76nzlzz4vyqmgdv34f6yokdqwfhdhaafanpo5p4fceibyid.onion") + .is_ok() + ); + // address too short + assert!(is_tor_address( + "http://kcgiy5g6m76nzlz4vyqmgdv34f6yokdqwfhdhaafanpo5p4fceibyid.onion" + ) + .is_err()); + assert!(is_tor_address("kcgiy5g6m76nzlz4vyqmgdv34f6yokdqwfhdhaafanpo5p4fceibyid").is_err()); + Ok(()) + } + + #[test] + fn test_complete_tor_address() -> Result<(), Error> { + assert_eq!( + "http://2a6at2obto3uvkpkitqp4wxcg6u36qf534eucbskqciturczzc5suyid.onion", + complete_tor_address("2a6at2obto3uvkpkitqp4wxcg6u36qf534eucbskqciturczzc5suyid") + .unwrap() + ); + assert_eq!( + "http://2a6at2obto3uvkpkitqp4wxcg6u36qf534eucbskqciturczzc5suyid.onion", + complete_tor_address("http://2a6at2obto3uvkpkitqp4wxcg6u36qf534eucbskqciturczzc5suyid") + .unwrap() + ); + assert_eq!( + "http://2a6at2obto3uvkpkitqp4wxcg6u36qf534eucbskqciturczzc5suyid.onion", + complete_tor_address("2a6at2obto3uvkpkitqp4wxcg6u36qf534eucbskqciturczzc5suyid.onion") + .unwrap() + ); + assert!( + complete_tor_address("2a6at2obto3uvkpkitqp4wxcg6u36qf534eucbskqciturczzc5suyi") + .is_err() + ); + Ok(()) + } +} diff --git a/impls/src/tor/mod.rs b/impls/src/tor/mod.rs new file mode 100644 index 0000000..37821c9 --- /dev/null +++ b/impls/src/tor/mod.rs @@ -0,0 +1,18 @@ +// Copyright 2018 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod bridge; +pub mod config; +pub mod process; +pub mod proxy; diff --git a/impls/src/tor/process.rs b/impls/src/tor/process.rs new file mode 100644 index 0000000..f964326 --- /dev/null +++ b/impls/src/tor/process.rs @@ -0,0 +1,288 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// BSD 3-Clause License +// +// Copyright (c) 2016, Dhole +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// * Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//! Tor process control +//! Derived from from from https://github.com/Dhole/rust-tor-controller.git + +extern crate chrono; +extern crate regex; +extern crate timer; + +use regex::Regex; +use std::fs::{self, File}; +use std::io; +use std::io::Write; +use std::io::{BufRead, BufReader}; +use std::path::{Path, MAIN_SEPARATOR}; +use std::process::{Child, ChildStdout, Command, Stdio}; +use std::sync::mpsc::channel; +use std::thread; +use sysinfo::{Process, ProcessExt, System, SystemExt}; + +#[cfg(windows)] +const TOR_EXE_NAME: &str = "tor.exe"; +#[cfg(not(windows))] +const TOR_EXE_NAME: &str = "tor"; + +#[derive(Debug)] +pub enum Error { + Process(String), + IO(io::Error), + PID(String), + Tor(String, Vec), + InvalidLogLine, + InvalidBootstrapLine(String), + Regex(regex::Error), + ProcessNotStarted, + Timeout, +} + +pub struct TorProcess { + tor_cmd: String, + args: Vec, + torrc_path: Option, + completion_percent: u8, + timeout: u32, + working_dir: Option, + pub stdout: Option>, + pub process: Option, + sys: System, +} + +impl TorProcess { + pub fn new() -> Self { + TorProcess { + tor_cmd: TOR_EXE_NAME.to_string(), + args: vec![], + torrc_path: None, + completion_percent: 100 as u8, + timeout: 0 as u32, + working_dir: None, + stdout: None, + process: None, + sys: System::new(), + } + } + + fn get_process(&mut self, pid: i32) -> Option<&Process> { + self.sys.refresh_all(); + self.sys.process((pid as usize).into()) + } + + pub fn tor_cmd(&mut self, tor_cmd: &str) -> &mut Self { + self.tor_cmd = tor_cmd.to_string(); + self + } + + pub fn torrc_path(&mut self, torrc_path: &str) -> &mut Self { + self.torrc_path = Some(torrc_path.to_string()); + self + } + + pub fn arg(&mut self, arg: String) -> &mut Self { + self.args.push(arg); + self + } + + pub fn args(&mut self, args: Vec) -> &mut Self { + for arg in args { + self.arg(arg); + } + self + } + + pub fn completion_percent(&mut self, completion_percent: u8) -> &mut Self { + self.completion_percent = completion_percent; + self + } + + pub fn timeout(&mut self, timeout: u32) -> &mut Self { + self.timeout = timeout; + self + } + + pub fn working_dir(&mut self, dir: &str) -> &mut Self { + self.working_dir = Some(dir.to_string()); + self + } + + // The tor process will have its stdout piped, so if the stdout lines are not consumed they + // will keep accumulating over time, increasing the consumed memory. + pub fn launch(&mut self) -> Result<&mut Self, Error> { + let mut tor = Command::new(&self.tor_cmd); + + if let Some(ref d) = self.working_dir { + tor.current_dir(&d); + let pid_file_name = format!("{}{}pid", d, MAIN_SEPARATOR); + // kill off PID if its already running + if Path::new(&pid_file_name).exists() { + let pid = fs::read_to_string(&pid_file_name).map_err(Error::IO)?; + let pid = pid + .parse::() + .map_err(|err| Error::PID(format!("{:?}", err)))?; + if let Some(p) = self.get_process(pid) { + let _ = p.kill(); + } + } + } + if let Some(ref torrc_path) = self.torrc_path { + tor.args(&vec!["-f", torrc_path]); + } + let mut tor_process = tor + .args(&self.args) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .map_err(|err| { + let msg = format!("TOR executable (`{}`) not found. Please ensure TOR is installed and on the path: {:?}", TOR_EXE_NAME, err); + Error::Process(msg) + })?; + + if let Some(ref d) = self.working_dir { + // Split out the process id, so if we don't exit cleanly + // we can take it down on the next run + let pid_file_name = format!("{}{}pid", d, MAIN_SEPARATOR); + let mut file = File::create(pid_file_name).map_err(Error::IO)?; + file.write_all(format!("{}", tor_process.id()).as_bytes()) + .map_err(Error::IO)?; + } + + let stdout = BufReader::new(tor_process.stdout.take().unwrap()); + + self.process = Some(tor_process); + let completion_percent = self.completion_percent; + + let (stdout_tx, stdout_rx) = channel(); + let stdout_timeout_tx = stdout_tx.clone(); + + let timer = timer::Timer::new(); + let _guard = + timer.schedule_with_delay(chrono::Duration::seconds(self.timeout as i64), move || { + stdout_timeout_tx.send(Err(Error::Timeout)).unwrap_or(()); + }); + let stdout_thread = thread::spawn(move || { + stdout_tx + .send(Self::parse_tor_stdout(stdout, completion_percent)) + .unwrap_or(()); + }); + match stdout_rx.recv().unwrap() { + Ok(stdout) => { + stdout_thread.join().unwrap(); + self.stdout = Some(stdout); + Ok(self) + } + Err(err) => { + self.kill().unwrap_or(()); + stdout_thread.join().unwrap(); + Err(err) + } + } + } + + fn parse_tor_stdout( + mut stdout: BufReader, + completion_perc: u8, + ) -> Result, Error> { + let re_bootstrap = Regex::new(r"^\[notice\] Bootstrapped (?P[0-9]+)%(.*): ") + .map_err(Error::Regex)?; + + let timestamp_len = "May 16 02:50:08.792".len(); + let mut warnings = Vec::new(); + let mut raw_line = String::new(); + + while stdout + .read_line(&mut raw_line) + .map_err(|err| Error::Process(format!("{}", err)))? + > 0 + { + { + if raw_line.len() < timestamp_len + 1 { + return Err(Error::InvalidLogLine); + } + let timestamp = &raw_line[..timestamp_len]; + let line = &raw_line[timestamp_len + 1..raw_line.len() - 1]; + debug!("{} {}", timestamp, line); + match line.split(' ').nth(0) { + Some("[notice]") => { + if let Some("Bootstrapped") = line.split(' ').nth(1) { + let perc = re_bootstrap + .captures(line) + .and_then(|c| c.name("perc")) + .and_then(|pc| pc.as_str().parse::().ok()) + .ok_or_else(|| Error::InvalidBootstrapLine(line.to_string()))?; + + if perc >= completion_perc { + break; + } + } + } + Some("[warn]") => warnings.push(line.to_string()), + Some("[err]") => return Err(Error::Tor(line.to_string(), warnings)), + _ => (), + } + } + raw_line.clear(); + } + Ok(stdout) + } + + pub fn kill(&mut self) -> Result<(), Error> { + if let Some(ref mut process) = self.process { + Ok(process + .kill() + .map_err(|err| Error::Process(format!("{}", err)))?) + } else { + Err(Error::ProcessNotStarted) + } + } +} + +impl Drop for TorProcess { + // kill the child + fn drop(&mut self) { + debug!("Dropping TOR process"); + self.kill().unwrap_or(()); + } +} diff --git a/impls/src/tor/proxy.rs b/impls/src/tor/proxy.rs new file mode 100644 index 0000000..9e0deb2 --- /dev/null +++ b/impls/src/tor/proxy.rs @@ -0,0 +1,191 @@ +// Copyright 2022 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::Error; +use grin_wallet_config::types::TorProxyConfig; +use std::collections::HashMap; +use std::convert::TryFrom; +use std::str; +use url::Host; + +/// Tor Proxy +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct TorProxy { + /// proxy type used for the proxy, eg "socks4", "socks5", "http", "https" + pub transport: Option, + /// Proxy address for the proxy, eg IP:PORT or Hostname + pub address: Option, + /// Username for the proxy authentification + pub username: Option, + /// Password for the proxy authentification + pub password: Option, + /// computer goes through a firewall that only allows connections to certain ports + pub allowed_port: Option>, +} + +impl Default for TorProxy { + fn default() -> TorProxy { + TorProxy { + transport: None, + address: None, + username: None, + password: None, + allowed_port: None, + } + } +} + +impl TorProxy { + fn parse_host_port(addr: &str) -> Result<(String, Option), Error> { + let host: String; + let str_port: Option; + let address = addr + .chars() + .filter(|c| !c.is_whitespace()) + .collect::(); + if address.starts_with('[') { + let split = address.split_once("]:").unwrap(); + host = split.0.to_string(); + str_port = Some(split.1.to_string()); + } else if address.contains(":") && !address.ends_with(":") { + let split = address.split_once(":").unwrap(); + host = split.0.to_string(); + str_port = Some(split.1.to_string()); + } else { + host = address.to_string(); + str_port = None; + }; + Ok((host, str_port)) + } + + pub fn parse_address(addr: &str) -> Result<(String, Option), Error> { + let (host, str_port) = TorProxy::parse_host_port(&addr)?; + let host = Host::parse(&host) + .map_err(|_e| Error::TorProxy(format!("Invalid host address: {}", host)))?; + let port = if let Some(p) = str_port { + let res = p + .parse::() + .map_err(|_e| Error::TorProxy(format!("Invalid port number: {}", p)))?; + Some(res) + } else { + None + }; + Ok((host.to_string(), port)) + } + + pub fn to_hashmap(self) -> Result, Error> { + let mut hm = HashMap::new(); + if let Some(ports) = self.allowed_port { + let mut allowed_ports = "".to_string(); + let last_port = ports.last().unwrap().to_owned(); + for port in ports.clone() { + allowed_ports.push_str(format!("*:{}", port).as_str()); + if port != last_port { + allowed_ports.push_str(","); + } + } + hm.insert( + "ReachableAddresses".to_string(), + format!("{}", allowed_ports.clone()), + ); + } + + let transport = match self.transport { + Some(t) => t, + None => return Ok(hm), + }; + match transport.as_str() { + "socks4" => { + hm.insert("Socks4Proxy".to_string(), self.address.unwrap()); + Ok(hm) + } + "socks5" => { + hm.insert("Socks5Proxy".to_string(), self.address.unwrap()); + + if let Some(s) = self.username { + hm.insert("Socks5ProxyUsername".to_string(), s); + } + if let Some(s) = self.password { + hm.insert("Socks5ProxyPassword".to_string(), s); + } + Ok(hm) + } + "http" | "https" | "http(s)" => { + hm.insert("HTTPSProxy".to_string(), self.address.unwrap()); + + if let Some(user) = self.username { + let pass = self.password.unwrap_or("".to_string()); + hm.insert( + "HTTPSProxyAuthenticator".to_string(), + format!("{}:{}", user, pass), + ); + } + Ok(hm) + } + _ => Ok(hm), + } + } +} + +impl TryFrom for TorProxy { + type Error = Error; + + fn try_from(tb: TorProxyConfig) -> Result { + if let Some(t) = tb.transport { + let transport = t.to_lowercase(); + match transport.as_str() { + "socks4" | "socks5" | "http" | "https" | "http(s)" => { + // Can't parse socket address --> trying to parse a domain name + if let Some(address) = tb.address { + let address_addr: String; + let (host, port) = TorProxy::parse_address(&address)?; + if let Some(p) = port { + address_addr = format!("{}:{}", host, p); + } else { + address_addr = host + } + Ok(TorProxy { + transport: Some(transport.into()), + address: Some(address_addr), + username: tb.username, + password: tb.password, + allowed_port: tb.allowed_port, + }) + } else { + let msg = format!( + "Missing proxy address: {} - must be or ", + transport + ); + return Err(Error::TorProxy(msg).into()); + } + } + // Missing transport type + _ => { + let msg = format!( + "Invalid proxy transport: {} - must be socks4/socks5/http(s)", + transport + ); + Err(Error::TorProxy(msg).into()) + } + } + } else { + // In case the user want to allow only some ports + let ports = tb.allowed_port.unwrap(); + Ok(TorProxy { + allowed_port: Some(ports), + ..TorProxy::default() + }) + } + } +} diff --git a/integration/Cargo.toml b/integration/Cargo.toml new file mode 100644 index 0000000..a624573 --- /dev/null +++ b/integration/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "grin_integration" +version = "1.1.0" +authors = ["Grin Developers "] +description = "Simple, private and scalable cryptocurrency implementation based on the MimbleWimble chain format." +license = "Apache-2.0" +repository = "https://github.com/mimblewimble/grin" +keywords = [ "crypto", "grin", "mimblewimble" ] +workspace = ".." +edition = "2018" + +[dependencies] +hyper = "0.12" +futures = "0.1" +http = "0.1" +itertools = "0.7" +rand = "0.5" +serde = "1" +log = "0.4" +serde_derive = "1" +serde_json = "1" +chrono = "0.4.4" +tokio = "0.1.11" +blake2-rfc = "0.2" +bufstream = "0.1" + +#grin_apiwallet = { path = "../apiwallet", version = "1.1.0" } +#grin_libwallet = { path = "../libwallet", version = "1.1.0" } +#grin_refwallet = { path = "../refwallet", version = "1.1.0" } +#grin_wallet_config = { path = "../config", version = "1.1.0" } + +#grin_core = { git = "https://github.com/mimblewimble/grin", branch = "milestone/1.1.0" } +#grin_keychain = { git = "https://github.com/mimblewimble/grin", branch = "milestone/1.1.0" } +#grin_chain = { git = "https://github.com/mimblewimble/grin", branch = "milestone/1.1.0" } +#grin_util = { git = "https://github.com/mimblewimble/grin", branch = "milestone/1.1.0" } +#grin_api = { git = "https://github.com/mimblewimble/grin", branch = "milestone/1.1.0" } +#grin_store = { git = "https://github.com/mimblewimble/grin", branch = "milestone/1.1.0" } +#grin_p2p = { git = "https://github.com/mimblewimble/grin", branch = "milestone/1.1.0" } +#grin_servers = { git = "https://github.com/mimblewimble/grin", branch = "milestone/1.1.0" } diff --git a/integration/src/lib.rs b/integration/src/lib.rs new file mode 100644 index 0000000..6083201 --- /dev/null +++ b/integration/src/lib.rs @@ -0,0 +1,21 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Grin integration test crate + +#![deny(non_upper_case_globals)] +#![deny(non_camel_case_types)] +#![deny(non_snake_case)] +#![deny(unused_mut)] +#![warn(missing_docs)] diff --git a/integration/tests/api.rs b/integration/tests/api.rs new file mode 100644 index 0000000..2158805 --- /dev/null +++ b/integration/tests/api.rs @@ -0,0 +1,485 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#[macro_use] +extern crate log; + +mod framework; + +use self::core::global::{self, ChainTypes}; +use self::util::init_test_logger; +use self::util::Mutex; +use crate::framework::{LocalServerContainer, LocalServerContainerConfig}; +use grin_api as api; +use grin_core as core; +use grin_p2p as p2p; +use grin_util as util; +use std::sync::Arc; +use std::{thread, time}; + +#[test] +fn simple_server_wallet() { + init_test_logger(); + info!("starting simple_server_wallet"); + let _test_name_dir = "test_servers"; + core::global::set_local_chain_type(core::global::ChainTypes::AutomatedTesting); + + // Run a separate coinbase wallet for coinbase transactions + let coinbase_dir = "coinbase_wallet_api"; + framework::clean_all_output(coinbase_dir); + let mut coinbase_config = LocalServerContainerConfig::default(); + coinbase_config.name = String::from(coinbase_dir); + coinbase_config.wallet_validating_node_url = String::from("http://127.0.0.1:40001"); + coinbase_config.wallet_port = 50002; + let coinbase_wallet = Arc::new(Mutex::new( + LocalServerContainer::new(coinbase_config).unwrap(), + )); + + let _ = thread::spawn(move || { + let mut w = coinbase_wallet.lock(); + w.run_wallet(0); + }); + + // Wait for the wallet to start + thread::sleep(time::Duration::from_millis(1000)); + + let api_server_one_dir = "api_server_one"; + framework::clean_all_output(api_server_one_dir); + let mut server_config = LocalServerContainerConfig::default(); + server_config.name = String::from(api_server_one_dir); + server_config.p2p_server_port = 40000; + server_config.api_server_port = 40001; + server_config.start_miner = true; + server_config.start_wallet = false; + server_config.coinbase_wallet_address = + String::from(format!("http://{}:{}", server_config.base_addr, 50002)); + let mut server_one = LocalServerContainer::new(server_config.clone()).unwrap(); + + // Spawn server and let it run for a bit + let _ = thread::spawn(move || server_one.run_server(120)); + + //Wait for chain to build + thread::sleep(time::Duration::from_millis(5000)); + + // Starting tests + let base_addr = server_config.base_addr; + let api_server_port = server_config.api_server_port; + + warn!("Testing chain handler"); + let tip = get_tip(&base_addr, api_server_port); + assert!(tip.is_ok()); + + warn!("Testing status handler"); + let status = get_status(&base_addr, api_server_port); + assert!(status.is_ok()); + + // Be sure that at least a block is mined by Travis + let mut current_tip = get_tip(&base_addr, api_server_port).unwrap(); + while current_tip.height == 0 { + thread::sleep(time::Duration::from_millis(1000)); + current_tip = get_tip(&base_addr, api_server_port).unwrap(); + } + + warn!("Testing block handler"); + let last_block_by_height = get_block_by_height(&base_addr, api_server_port, current_tip.height); + assert!(last_block_by_height.is_ok()); + let last_block_by_height_compact = + get_block_by_height_compact(&base_addr, api_server_port, current_tip.height); + assert!(last_block_by_height_compact.is_ok()); + + let block_hash = current_tip.last_block_pushed; + let last_block_by_hash = get_block_by_hash(&base_addr, api_server_port, &block_hash); + assert!(last_block_by_hash.is_ok()); + let last_block_by_hash_compact = + get_block_by_hash_compact(&base_addr, api_server_port, &block_hash); + assert!(last_block_by_hash_compact.is_ok()); + + warn!("Testing chain output handler"); + let start_height = 0; + let end_height = current_tip.height; + let outputs_by_height = + get_outputs_by_height(&base_addr, api_server_port, start_height, end_height); + assert!(outputs_by_height.is_ok()); + let ids = get_ids_from_block_outputs(outputs_by_height.unwrap()); + let outputs_by_ids1 = get_outputs_by_ids1(&base_addr, api_server_port, ids.clone()); + assert!(outputs_by_ids1.is_ok()); + let outputs_by_ids2 = get_outputs_by_ids2(&base_addr, api_server_port, ids.clone()); + assert!(outputs_by_ids2.is_ok()); + + warn!("Testing txhashset handler"); + let roots = get_txhashset_roots(&base_addr, api_server_port); + assert!(roots.is_ok()); + let last_10_outputs = get_txhashset_lastoutputs(&base_addr, api_server_port, 0); + assert!(last_10_outputs.is_ok()); + let last_5_outputs = get_txhashset_lastoutputs(&base_addr, api_server_port, 5); + assert!(last_5_outputs.is_ok()); + let last_10_rangeproofs = get_txhashset_lastrangeproofs(&base_addr, api_server_port, 0); + assert!(last_10_rangeproofs.is_ok()); + let last_5_rangeproofs = get_txhashset_lastrangeproofs(&base_addr, api_server_port, 5); + assert!(last_5_rangeproofs.is_ok()); + let last_10_kernels = get_txhashset_lastkernels(&base_addr, api_server_port, 0); + assert!(last_10_kernels.is_ok()); + let last_5_kernels = get_txhashset_lastkernels(&base_addr, api_server_port, 5); + assert!(last_5_kernels.is_ok()); + + //let some more mining happen, make sure nothing pukes + thread::sleep(time::Duration::from_millis(5000)); +} + +/// Creates 2 servers and test P2P API +#[test] +fn test_p2p() { + init_test_logger(); + info!("starting test_p2p"); + global::set_local_chain_type(ChainTypes::AutomatedTesting); + + let _test_name_dir = "test_servers"; + + // Spawn server and let it run for a bit + let server_one_dir = "p2p_server_one"; + framework::clean_all_output(server_one_dir); + let mut server_config_one = LocalServerContainerConfig::default(); + server_config_one.name = String::from(server_one_dir); + server_config_one.p2p_server_port = 40002; + server_config_one.api_server_port = 40003; + server_config_one.start_miner = false; + server_config_one.start_wallet = false; + server_config_one.is_seeding = true; + let mut server_one = LocalServerContainer::new(server_config_one.clone()).unwrap(); + let _ = thread::spawn(move || server_one.run_server(120)); + + thread::sleep(time::Duration::from_millis(1000)); + + // Spawn server and let it run for a bit + let server_two_dir = "p2p_server_two"; + framework::clean_all_output(server_two_dir); + let mut server_config_two = LocalServerContainerConfig::default(); + server_config_two.name = String::from(server_two_dir); + server_config_two.p2p_server_port = 40004; + server_config_two.api_server_port = 40005; + server_config_two.start_miner = false; + server_config_two.start_wallet = false; + server_config_two.is_seeding = false; + let mut server_two = LocalServerContainer::new(server_config_two.clone()).unwrap(); + server_two.add_peer(format!( + "{}:{}", + server_config_one.base_addr, server_config_one.p2p_server_port + )); + let _ = thread::spawn(move || server_two.run_server(120)); + + // Let them do the handshake + thread::sleep(time::Duration::from_millis(2000)); + + // Starting tests + warn!("Starting P2P Tests"); + let base_addr = server_config_one.base_addr; + let api_server_port = server_config_one.api_server_port; + + // Check that peer all is also working + let mut peers_all = get_all_peers(&base_addr, api_server_port); + assert!(peers_all.is_ok()); + let pall = peers_all.unwrap(); + assert_eq!(pall.len(), 2); + + // Check that when we get peer connected the peer is here + let peers_connected = get_connected_peers(&base_addr, api_server_port); + assert!(peers_connected.is_ok()); + let pc = peers_connected.unwrap(); + assert_eq!(pc.len(), 1); + + // Check that the peer status is Healthy + let addr = format!( + "{}:{}", + server_config_two.base_addr, server_config_two.p2p_server_port + ); + let peer = get_peer(&base_addr, api_server_port, &addr); + assert!(peer.is_ok()); + assert_eq!(peer.unwrap().flags, p2p::State::Healthy); + + // Ban the peer + let ban_result = ban_peer(&base_addr, api_server_port, &addr); + assert!(ban_result.is_ok()); + thread::sleep(time::Duration::from_millis(2000)); + + // Check its status is banned with get peer + let peer = get_peer(&base_addr, api_server_port, &addr); + assert!(peer.is_ok()); + assert_eq!(peer.unwrap().flags, p2p::State::Banned); + + // Check from peer all + peers_all = get_all_peers(&base_addr, api_server_port); + assert!(peers_all.is_ok()); + assert_eq!(peers_all.unwrap().len(), 2); + + // Unban + let unban_result = unban_peer(&base_addr, api_server_port, &addr); + assert!(unban_result.is_ok()); + + // Check from peer connected + let peers_connected = get_connected_peers(&base_addr, api_server_port); + assert!(peers_connected.is_ok()); + assert_eq!(peers_connected.unwrap().len(), 0); + + // Check its status is healthy with get peer + let peer = get_peer(&base_addr, api_server_port, &addr); + assert!(peer.is_ok()); + assert_eq!(peer.unwrap().flags, p2p::State::Healthy); +} + +// Tip handler function +fn get_tip(base_addr: &String, api_server_port: u16) -> Result { + let url = format!("http://{}:{}/v1/chain", base_addr, api_server_port); + api::client::get::(url.as_str(), None).map_err(|e| Error::API(e)) +} + +// Status handler function +fn get_status(base_addr: &String, api_server_port: u16) -> Result { + let url = format!("http://{}:{}/v1/status", base_addr, api_server_port); + api::client::get::(url.as_str(), None).map_err(|e| Error::API(e)) +} + +// Block handler functions +fn get_block_by_height( + base_addr: &String, + api_server_port: u16, + height: u64, +) -> Result { + let url = format!( + "http://{}:{}/v1/blocks/{}", + base_addr, api_server_port, height + ); + api::client::get::(url.as_str(), None).map_err(|e| Error::API(e)) +} + +fn get_block_by_height_compact( + base_addr: &String, + api_server_port: u16, + height: u64, +) -> Result { + let url = format!( + "http://{}:{}/v1/blocks/{}?compact", + base_addr, api_server_port, height + ); + api::client::get::(url.as_str(), None).map_err(|e| Error::API(e)) +} + +fn get_block_by_hash( + base_addr: &String, + api_server_port: u16, + block_hash: &String, +) -> Result { + let url = format!( + "http://{}:{}/v1/blocks/{}", + base_addr, api_server_port, block_hash + ); + api::client::get::(url.as_str(), None).map_err(|e| Error::API(e)) +} + +fn get_block_by_hash_compact( + base_addr: &String, + api_server_port: u16, + block_hash: &String, +) -> Result { + let url = format!( + "http://{}:{}/v1/blocks/{}?compact", + base_addr, api_server_port, block_hash + ); + api::client::get::(url.as_str(), None).map_err(|e| Error::API(e)) +} + +// Chain output handler functions +fn get_outputs_by_ids1( + base_addr: &String, + api_server_port: u16, + ids: Vec, +) -> Result, Error> { + let url = format!( + "http://{}:{}/v1/chain/outputs/byids?id={}", + base_addr, + api_server_port, + ids.join(",") + ); + api::client::get::>(url.as_str(), None).map_err(|e| Error::API(e)) +} + +fn get_outputs_by_ids2( + base_addr: &String, + api_server_port: u16, + ids: Vec, +) -> Result, Error> { + let mut ids_string: String = String::from(""); + for id in ids { + ids_string = ids_string + "?id=" + &id; + } + let ids_string = String::from(&ids_string[1..ids_string.len()]); + let url = format!( + "http://{}:{}/v1/chain/outputs/byids?{}", + base_addr, api_server_port, ids_string + ); + api::client::get::>(url.as_str(), None).map_err(|e| Error::API(e)) +} + +fn get_outputs_by_height( + base_addr: &String, + api_server_port: u16, + start_height: u64, + end_height: u64, +) -> Result, Error> { + let url = format!( + "http://{}:{}/v1/chain/outputs/byheight?start_height={}&end_height={}", + base_addr, api_server_port, start_height, end_height + ); + api::client::get::>(url.as_str(), None).map_err(|e| Error::API(e)) +} + +// TxHashSet handler functions +fn get_txhashset_roots(base_addr: &String, api_server_port: u16) -> Result { + let url = format!( + "http://{}:{}/v1/txhashset/roots", + base_addr, api_server_port + ); + api::client::get::(url.as_str(), None).map_err(|e| Error::API(e)) +} + +fn get_txhashset_lastoutputs( + base_addr: &String, + api_server_port: u16, + n: u64, +) -> Result, Error> { + let url: String; + if n == 0 { + url = format!( + "http://{}:{}/v1/txhashset/lastoutputs", + base_addr, api_server_port + ); + } else { + url = format!( + "http://{}:{}/v1/txhashset/lastoutputs?n={}", + base_addr, api_server_port, n + ); + } + api::client::get::>(url.as_str(), None).map_err(|e| Error::API(e)) +} + +fn get_txhashset_lastrangeproofs( + base_addr: &String, + api_server_port: u16, + n: u64, +) -> Result, Error> { + let url: String; + if n == 0 { + url = format!( + "http://{}:{}/v1/txhashset/lastrangeproofs", + base_addr, api_server_port + ); + } else { + url = format!( + "http://{}:{}/v1/txhashset/lastrangeproofs?n={}", + base_addr, api_server_port, n + ); + } + api::client::get::>(url.as_str(), None).map_err(|e| Error::API(e)) +} + +fn get_txhashset_lastkernels( + base_addr: &String, + api_server_port: u16, + n: u64, +) -> Result, Error> { + let url: String; + if n == 0 { + url = format!( + "http://{}:{}/v1/txhashset/lastkernels", + base_addr, api_server_port + ); + } else { + url = format!( + "http://{}:{}/v1/txhashset/lastkernels?n={}", + base_addr, api_server_port, n + ); + } + api::client::get::>(url.as_str(), None).map_err(|e| Error::API(e)) +} + +// Helper function to get a vec of commitment output ids from a vec of block +// outputs +fn get_ids_from_block_outputs(block_outputs: Vec) -> Vec { + let mut ids: Vec = Vec::new(); + for block_output in block_outputs { + let outputs = &block_output.outputs; + for output in outputs { + ids.push(util::to_hex(output.clone().commit.0.to_vec())); + } + } + ids.into_iter().take(100).collect() +} + +pub fn ban_peer(base_addr: &String, api_server_port: u16, peer_addr: &String) -> Result<(), Error> { + let url = format!( + "http://{}:{}/v1/peers/{}/ban", + base_addr, api_server_port, peer_addr + ); + api::client::post_no_ret(url.as_str(), None, &"").map_err(|e| Error::API(e)) +} + +pub fn unban_peer( + base_addr: &String, + api_server_port: u16, + peer_addr: &String, +) -> Result<(), Error> { + let url = format!( + "http://{}:{}/v1/peers/{}/unban", + base_addr, api_server_port, peer_addr + ); + api::client::post_no_ret(url.as_str(), None, &"").map_err(|e| Error::API(e)) +} + +pub fn get_peer( + base_addr: &String, + api_server_port: u16, + peer_addr: &String, +) -> Result { + let url = format!( + "http://{}:{}/v1/peers/{}", + base_addr, api_server_port, peer_addr + ); + api::client::get::(url.as_str(), None).map_err(|e| Error::API(e)) +} + +pub fn get_connected_peers( + base_addr: &String, + api_server_port: u16, +) -> Result, Error> { + let url = format!( + "http://{}:{}/v1/peers/connected", + base_addr, api_server_port + ); + api::client::get::>(url.as_str(), None) + .map_err(|e| Error::API(e)) +} + +pub fn get_all_peers( + base_addr: &String, + api_server_port: u16, +) -> Result, Error> { + let url = format!("http://{}:{}/v1/peers/all", base_addr, api_server_port); + api::client::get::>(url.as_str(), None).map_err(|e| Error::API(e)) +} + +/// Error type wrapping underlying module errors. +#[derive(Debug)] +pub enum Error { + /// Error originating from HTTP API calls. + API(api::Error), +} diff --git a/integration/tests/dandelion.rs b/integration/tests/dandelion.rs new file mode 100644 index 0000000..c80e3b6 --- /dev/null +++ b/integration/tests/dandelion.rs @@ -0,0 +1,157 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#[macro_use] +extern crate log; + +mod framework; + +use self::util::Mutex; +use crate::framework::{LocalServerContainer, LocalServerContainerConfig}; +use grin_core as core; +use grin_util as util; +use std::sync::Arc; +use std::{thread, time}; + +/// Start 1 node mining, 1 non mining node and two wallets. +/// Then send a transaction from one wallet to another and propagate it a stem +/// transaction but without stem relay and check if the transaction is still +/// broadcasted. +#[test] +#[ignore] +fn test_dandelion_timeout() { + let test_name_dir = "test_dandelion_timeout"; + core::global::set_local_chain_type(core::global::ChainTypes::AutomatedTesting); + framework::clean_all_output(test_name_dir); + let mut log_config = util::LoggingConfig::default(); + //log_config.stdout_log_level = util::LogLevel::Trace; + log_config.stdout_log_level = util::LogLevel::Info; + //init_logger(Some(log_config)); + util::init_test_logger(); + + // Run a separate coinbase wallet for coinbase transactions + let mut coinbase_config = LocalServerContainerConfig::default(); + coinbase_config.name = String::from("coinbase_wallet"); + coinbase_config.wallet_validating_node_url = String::from("http://127.0.0.1:30001"); + coinbase_config.wallet_port = 10002; + let coinbase_wallet = Arc::new(Mutex::new( + LocalServerContainer::new(coinbase_config).unwrap(), + )); + let coinbase_wallet_config = { coinbase_wallet.lock().wallet_config.clone() }; + + let coinbase_seed = LocalServerContainer::get_wallet_seed(&coinbase_wallet_config); + + let _ = thread::spawn(move || { + let mut w = coinbase_wallet.lock(); + w.run_wallet(0); + }); + + let mut recp_config = LocalServerContainerConfig::default(); + recp_config.name = String::from("target_wallet"); + recp_config.wallet_validating_node_url = String::from("http://127.0.0.1:30001"); + recp_config.wallet_port = 20002; + let target_wallet = Arc::new(Mutex::new(LocalServerContainer::new(recp_config).unwrap())); + let target_wallet_cloned = target_wallet.clone(); + let recp_wallet_config = { target_wallet.lock().wallet_config.clone() }; + + let recp_seed = LocalServerContainer::get_wallet_seed(&recp_wallet_config); + //Start up a second wallet, to receive + let _ = thread::spawn(move || { + let mut w = target_wallet_cloned.lock(); + w.run_wallet(0); + }); + + // Spawn server and let it run for a bit + let mut server_one_config = LocalServerContainerConfig::default(); + server_one_config.name = String::from("server_one"); + server_one_config.p2p_server_port = 30000; + server_one_config.api_server_port = 30001; + server_one_config.start_miner = true; + server_one_config.start_wallet = false; + server_one_config.is_seeding = false; + server_one_config.coinbase_wallet_address = + String::from(format!("http://{}:{}", server_one_config.base_addr, 10002)); + let mut server_one = LocalServerContainer::new(server_one_config).unwrap(); + + let mut server_two_config = LocalServerContainerConfig::default(); + server_two_config.name = String::from("server_two"); + server_two_config.p2p_server_port = 40000; + server_two_config.api_server_port = 40001; + server_two_config.start_miner = false; + server_two_config.start_wallet = false; + server_two_config.is_seeding = true; + let mut server_two = LocalServerContainer::new(server_two_config.clone()).unwrap(); + + server_one.add_peer(format!( + "{}:{}", + server_two_config.base_addr, server_two_config.p2p_server_port + )); + + // Spawn servers and let them run for a bit + let _ = thread::spawn(move || { + server_two.run_server(120); + }); + + // Wait for the first server to start + thread::sleep(time::Duration::from_millis(5000)); + + let _ = thread::spawn(move || { + server_one.run_server(120); + }); + + // Let them do a handshake and properly update their peer relay + thread::sleep(time::Duration::from_millis(30000)); + + //Wait until we have some funds to send + let mut coinbase_info = + LocalServerContainer::get_wallet_info(&coinbase_wallet_config, &coinbase_seed); + let mut slept_time = 0; + while coinbase_info.amount_currently_spendable < 100000000000 { + thread::sleep(time::Duration::from_millis(500)); + slept_time += 500; + if slept_time > 10000 { + panic!("Coinbase not confirming in time"); + } + coinbase_info = + LocalServerContainer::get_wallet_info(&coinbase_wallet_config, &coinbase_seed); + } + + warn!("Sending 50 Grins to recipient wallet"); + + // Sending stem transaction + LocalServerContainer::send_amount_to( + &coinbase_wallet_config, + "50.00", + 1, + "not_all", + "http://127.0.0.1:20002", + false, + ); + + let coinbase_info = + LocalServerContainer::get_wallet_info(&coinbase_wallet_config, &coinbase_seed); + println!("Coinbase wallet info: {:?}", coinbase_info); + + let recipient_info = LocalServerContainer::get_wallet_info(&recp_wallet_config, &recp_seed); + + // The transaction should be waiting in the node stempool thus cannot be mined. + println!("Recipient wallet info: {:?}", recipient_info); + assert!(recipient_info.amount_awaiting_confirmation == 50000000000); + + // Wait for stem timeout + thread::sleep(time::Duration::from_millis(35000)); + println!("Recipient wallet info: {:?}", recipient_info); + let recipient_info = LocalServerContainer::get_wallet_info(&recp_wallet_config, &recp_seed); + assert!(recipient_info.amount_currently_spendable == 50000000000); +} diff --git a/integration/tests/framework.rs b/integration/tests/framework.rs new file mode 100644 index 0000000..ffade00 --- /dev/null +++ b/integration/tests/framework.rs @@ -0,0 +1,682 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +extern crate grin_apiwallet as apiwallet; +extern crate grin_libwallet as libwallet; +extern crate grin_refwallet as wallet; +extern crate grin_wallet_config as wallet_config; + +use self::keychain::Keychain; +use self::util::Mutex; +use self::wallet::{HTTPNodeClient, HTTPWalletCommAdapter, LMDBBackend}; +use self::wallet_config::WalletConfig; +use blake2_rfc as blake2; +use grin_api as api; +use grin_core as core; +use grin_keychain as keychain; +use grin_p2p as p2p; +use grin_servers as servers; +use grin_util as util; +use p2p::PeerAddr; +use std::default::Default; +use std::ops::Deref; +use std::sync::Arc; +use std::{fs, thread, time}; + +/// Just removes all results from previous runs +pub fn clean_all_output(test_name_dir: &str) { + let target_dir = format!("target/tmp/{}", test_name_dir); + if let Err(e) = remove_dir_all::remove_dir_all(target_dir) { + println!("can't remove output from previous test :{}, may be ok", e); + } +} + +/// Errors that can be returned by LocalServerContainer +#[derive(Debug)] +#[allow(dead_code)] +pub enum Error { + Internal(String), + Argument(String), + NotFound, +} + +/// All-in-one server configuration struct, for convenience +/// +#[derive(Clone)] +pub struct LocalServerContainerConfig { + // user friendly name for the server, also denotes what dir + // the data files will appear in + pub name: String, + + // Base IP address + pub base_addr: String, + + // Port the server (p2p) is running on + pub p2p_server_port: u16, + + // Port the API server is running on + pub api_server_port: u16, + + // Port the wallet server is running on + pub wallet_port: u16, + + // Port the wallet owner API is running on + pub owner_port: u16, + + // Whether to include the foreign API endpoints in the owner API + pub owner_api_include_foreign: bool, + + // Whether we're going to mine + pub start_miner: bool, + + // time in millis by which to artificially slow down the mining loop + // in this container + pub miner_slowdown_in_millis: u64, + + // Whether we're going to run a wallet as well, + // can use same server instance as a validating node for convenience + pub start_wallet: bool, + + // address of a server to use as a seed + pub seed_addr: String, + + // keep track of whether this server is supposed to be seeding + pub is_seeding: bool, + + // Whether to burn mining rewards + pub burn_mining_rewards: bool, + + // full address to send coinbase rewards to + pub coinbase_wallet_address: String, + + // When running a wallet, the address to check inputs and send + // finalised transactions to, + pub wallet_validating_node_url: String, +} + +/// Default server config +impl Default for LocalServerContainerConfig { + fn default() -> LocalServerContainerConfig { + LocalServerContainerConfig { + name: String::from("test_host"), + base_addr: String::from("127.0.0.1"), + api_server_port: 13413, + p2p_server_port: 13414, + wallet_port: 13415, + owner_port: 13420, + owner_api_include_foreign: false, + seed_addr: String::from(""), + is_seeding: false, + start_miner: false, + start_wallet: false, + burn_mining_rewards: false, + coinbase_wallet_address: String::from(""), + wallet_validating_node_url: String::from(""), + miner_slowdown_in_millis: 0, + } + } +} + +/// A top-level container to hold everything that might be running +/// on a server, i.e. server, wallet in send or receive mode + +#[allow(dead_code)] +pub struct LocalServerContainer { + // Configuration + config: LocalServerContainerConfig, + + // Structure of references to the + // internal server data + pub p2p_server_stats: Option, + + // The API server instance + api_server: Option, + + // whether the server is running + pub server_is_running: bool, + + // Whether the server is mining + pub server_is_mining: bool, + + // Whether the server is also running a wallet + // Not used if running wallet without server + pub wallet_is_running: bool, + + // the list of peers to connect to + pub peer_list: Vec, + + // base directory for the server instance + pub working_dir: String, + + // Wallet configuration + pub wallet_config: WalletConfig, +} + +impl LocalServerContainer { + /// Create a new local server container with defaults, with the given name + /// all related files will be created in the directory + /// target/tmp/{name} + + pub fn new(config: LocalServerContainerConfig) -> Result { + let working_dir = format!("target/tmp/{}", config.name); + let mut wallet_config = WalletConfig::default(); + + wallet_config.api_listen_port = config.wallet_port; + wallet_config.check_node_api_http_addr = config.wallet_validating_node_url.clone(); + wallet_config.owner_api_include_foreign = Some(config.owner_api_include_foreign); + wallet_config.data_file_dir = working_dir.clone(); + Ok(LocalServerContainer { + config: config, + p2p_server_stats: None, + api_server: None, + server_is_running: false, + server_is_mining: false, + wallet_is_running: false, + working_dir: working_dir, + peer_list: Vec::new(), + wallet_config: wallet_config, + }) + } + + pub fn run_server(&mut self, duration_in_seconds: u64) -> servers::Server { + let api_addr = format!("{}:{}", self.config.base_addr, self.config.api_server_port); + + let mut seeding_type = p2p::Seeding::None; + let mut seeds = Vec::new(); + + if self.config.seed_addr.len() > 0 { + seeding_type = p2p::Seeding::List; + seeds = vec![PeerAddr::from_ip( + self.config.seed_addr.to_string().parse().unwrap(), + )]; + } + + let s = servers::Server::new(servers::ServerConfig { + api_http_addr: api_addr, + api_secret_path: None, + db_root: format!("{}/.grin", self.working_dir), + p2p_config: p2p::P2PConfig { + port: self.config.p2p_server_port, + seeds: Some(seeds), + seeding_type: seeding_type, + ..p2p::P2PConfig::default() + }, + chain_type: core::global::ChainTypes::AutomatedTesting, + skip_sync_wait: Some(true), + stratum_mining_config: None, + ..Default::default() + }) + .unwrap(); + + self.p2p_server_stats = Some(s.get_server_stats().unwrap()); + + let mut wallet_url = None; + + if self.config.start_wallet == true { + self.run_wallet(duration_in_seconds + 5); + // give a second to start wallet before continuing + thread::sleep(time::Duration::from_millis(1000)); + wallet_url = Some(format!( + "http://{}:{}", + self.config.base_addr, self.config.wallet_port + )); + } + + if self.config.start_miner == true { + println!( + "starting test Miner on port {}", + self.config.p2p_server_port + ); + s.start_test_miner(wallet_url, s.stop_state.clone()); + } + + for p in &mut self.peer_list { + println!("{} connecting to peer: {}", self.config.p2p_server_port, p); + let _ = s.connect_peer(PeerAddr::from_ip(p.parse().unwrap())); + } + + if self.wallet_is_running { + self.stop_wallet(); + } + + s + } + + /// Make a wallet for use in test endpoints (run_wallet and run_owner). + fn make_wallet_for_tests( + &mut self, + ) -> Arc>> { + // URL on which to start the wallet listener (i.e. api server) + let _url = format!("{}:{}", self.config.base_addr, self.config.wallet_port); + + // Just use the name of the server for a seed for now + let seed = format!("{}", self.config.name); + + let _seed = blake2::blake2b::blake2b(32, &[], seed.as_bytes()); + + println!( + "Starting the Grin wallet receiving daemon on {} ", + self.config.wallet_port + ); + + self.wallet_config = WalletConfig::default(); + + self.wallet_config.api_listen_port = self.config.wallet_port; + self.wallet_config.check_node_api_http_addr = + self.config.wallet_validating_node_url.clone(); + self.wallet_config.data_file_dir = self.working_dir.clone(); + self.wallet_config.owner_api_include_foreign = Some(self.config.owner_api_include_foreign); + + let _ = fs::create_dir_all(self.wallet_config.clone().data_file_dir); + let r = wallet::WalletSeed::init_file(&self.wallet_config, 32, None, ""); + + let client_n = HTTPNodeClient::new(&self.wallet_config.check_node_api_http_addr, None); + + if let Err(_e) = r { + //panic!("Error initializing wallet seed: {}", e); + } + + let wallet: LMDBBackend = + LMDBBackend::new(self.wallet_config.clone(), "", client_n).unwrap_or_else(|e| { + panic!( + "Error creating wallet: {:?} Config: {:?}", + e, self.wallet_config + ) + }); + + Arc::new(Mutex::new(wallet)) + } + + /// Starts a wallet daemon to receive + pub fn run_wallet(&mut self, _duration_in_mills: u64) { + let wallet = self.make_wallet_for_tests(); + + wallet::controller::foreign_listener(wallet, &self.wallet_config.api_listen_addr(), None) + .unwrap_or_else(|e| { + panic!( + "Error creating wallet listener: {:?} Config: {:?}", + e, self.wallet_config + ) + }); + + self.wallet_is_running = true; + } + + /// Starts a wallet owner daemon + #[allow(dead_code)] + pub fn run_owner(&mut self) { + let wallet = self.make_wallet_for_tests(); + + // WalletConfig doesn't allow changing the owner API path, so we build + // the path ourselves + let owner_listen_addr = format!("127.0.0.1:{}", self.config.owner_port); + + wallet::controller::owner_listener( + wallet, + &owner_listen_addr, + None, + None, + self.wallet_config.owner_api_include_foreign.clone(), + ) + .unwrap_or_else(|e| { + panic!( + "Error creating wallet owner listener: {:?} Config: {:?}", + e, self.wallet_config + ) + }); + } + + #[allow(dead_code)] + pub fn get_wallet_seed(config: &WalletConfig) -> wallet::WalletSeed { + let _ = fs::create_dir_all(config.clone().data_file_dir); + wallet::WalletSeed::init_file(config, 32, None, "").unwrap(); + let wallet_seed = + wallet::WalletSeed::from_file(config, "").expect("Failed to read wallet seed file."); + wallet_seed + } + + #[allow(dead_code)] + pub fn get_wallet_info( + config: &WalletConfig, + wallet_seed: &wallet::WalletSeed, + ) -> wallet::WalletInfo { + let keychain: keychain::ExtKeychain = wallet_seed + .derive_keychain(false) + .expect("Failed to derive keychain from seed file and passphrase."); + let client_n = HTTPNodeClient::new(&config.check_node_api_http_addr, None); + let mut wallet = LMDBBackend::new(config.clone(), "", client_n) + .unwrap_or_else(|e| panic!("Error creating wallet: {:?} Config: {:?}", e, config)); + wallet.keychain = Some(keychain); + let parent_id = keychain::ExtKeychain::derive_key_id(2, 0, 0, 0, 0); + let _ = libwallet::internal::updater::refresh_outputs(&mut wallet, &parent_id, false); + libwallet::internal::updater::retrieve_info(&mut wallet, &parent_id, 1).unwrap() + } + + #[allow(dead_code)] + pub fn send_amount_to( + config: &WalletConfig, + amount: &str, + minimum_confirmations: u64, + selection_strategy: &str, + dest: &str, + _fluff: bool, + ) { + let amount = core::core::amount_from_hr_string(amount) + .expect("Could not parse amount as a number with optional decimal point."); + + let wallet_seed = + wallet::WalletSeed::from_file(config, "").expect("Failed to read wallet seed file."); + + let keychain: keychain::ExtKeychain = wallet_seed + .derive_keychain(false) + .expect("Failed to derive keychain from seed file and passphrase."); + + let client_n = HTTPNodeClient::new(&config.check_node_api_http_addr, None); + let client_w = HTTPWalletCommAdapter::new(); + + let max_outputs = 500; + let change_outputs = 1; + + let mut wallet = LMDBBackend::new(config.clone(), "", client_n) + .unwrap_or_else(|e| panic!("Error creating wallet: {:?} Config: {:?}", e, config)); + wallet.keychain = Some(keychain); + let _ = wallet::controller::owner_single_use(Arc::new(Mutex::new(wallet)), |api| { + let (mut slate, lock_fn) = api.initiate_tx( + None, + amount, + minimum_confirmations, + max_outputs, + change_outputs, + selection_strategy == "all", + None, + )?; + slate = client_w.send_tx_sync(dest, &slate)?; + slate = api.finalize_tx(&slate)?; + api.tx_lock_outputs(&slate, lock_fn)?; + println!( + "Tx sent: {} grin to {} (strategy '{}')", + core::core::amount_to_hr_string(amount, false), + dest, + selection_strategy, + ); + Ok(()) + }) + .unwrap_or_else(|e| panic!("Error creating wallet: {:?} Config: {:?}", e, config)); + } + + /// Stops the running wallet server + pub fn stop_wallet(&mut self) { + println!("Stop wallet!"); + let api_server = self.api_server.as_mut().unwrap(); + api_server.stop(); + } + + /// Adds a peer to this server to connect to upon running + + #[allow(dead_code)] + pub fn add_peer(&mut self, addr: String) { + self.peer_list.push(addr); + } +} + +/// Configuration values for container pool + +pub struct LocalServerContainerPoolConfig { + // Base name to append to all the servers in this pool + pub base_name: String, + + // Base http address for all of the servers in this pool + pub base_http_addr: String, + + // Base port server for all of the servers in this pool + // Increment the number by 1 for each new server + pub base_p2p_port: u16, + + // Base api port for all of the servers in this pool + // Increment this number by 1 for each new server + pub base_api_port: u16, + + // Base wallet port for this server + // + pub base_wallet_port: u16, + + // Base wallet owner port for this server + // + pub base_owner_port: u16, + + // How long the servers in the pool are going to run + pub run_length_in_seconds: u64, +} + +/// Default server config +/// +impl Default for LocalServerContainerPoolConfig { + fn default() -> LocalServerContainerPoolConfig { + LocalServerContainerPoolConfig { + base_name: String::from("test_pool"), + base_http_addr: String::from("127.0.0.1"), + base_p2p_port: 10000, + base_api_port: 11000, + base_wallet_port: 12000, + base_owner_port: 13000, + run_length_in_seconds: 30, + } + } +} + +/// A convenience pool for running many servers simultaneously +/// without necessarily having to configure each one manually + +#[allow(dead_code)] +pub struct LocalServerContainerPool { + // configuration + pub config: LocalServerContainerPoolConfig, + + // keep ahold of all the created servers thread-safely + server_containers: Vec, + + // Keep track of what the last ports a server was opened on + next_p2p_port: u16, + + next_api_port: u16, + + next_wallet_port: u16, + + next_owner_port: u16, + + // keep track of whether a seed exists, and pause a bit if so + is_seeding: bool, +} + +#[allow(dead_code)] +impl LocalServerContainerPool { + pub fn new(config: LocalServerContainerPoolConfig) -> LocalServerContainerPool { + (LocalServerContainerPool { + next_api_port: config.base_api_port, + next_p2p_port: config.base_p2p_port, + next_wallet_port: config.base_wallet_port, + next_owner_port: config.base_owner_port, + config: config, + server_containers: Vec::new(), + is_seeding: false, + }) + } + + /// adds a single server on the next available port + /// overriding passed-in values as necessary. Config object is an OUT value + /// with + /// ports/addresses filled in + /// + + #[allow(dead_code)] + pub fn create_server(&mut self, server_config: &mut LocalServerContainerConfig) { + // If we're calling it this way, need to override these + server_config.p2p_server_port = self.next_p2p_port; + server_config.api_server_port = self.next_api_port; + server_config.wallet_port = self.next_wallet_port; + server_config.owner_port = self.next_owner_port; + + server_config.name = String::from(format!( + "{}/{}-{}", + self.config.base_name, self.config.base_name, server_config.p2p_server_port + )); + + // Use self as coinbase wallet + server_config.coinbase_wallet_address = String::from(format!( + "http://{}:{}", + server_config.base_addr, server_config.wallet_port + )); + + self.next_p2p_port += 1; + self.next_api_port += 1; + self.next_wallet_port += 1; + self.next_owner_port += 1; + + if server_config.is_seeding { + self.is_seeding = true; + } + + let _server_address = format!( + "{}:{}", + server_config.base_addr, server_config.p2p_server_port + ); + + let server_container = LocalServerContainer::new(server_config.clone()).unwrap(); + // self.server_containers.push(server_arc); + + // Create a future that runs the server for however many seconds + // collect them all and run them in the run_all_servers + let _run_time = self.config.run_length_in_seconds; + + self.server_containers.push(server_container); + } + + /// adds n servers, ready to run + /// + /// + #[allow(dead_code)] + pub fn create_servers(&mut self, number: u16) { + for _ in 0..number { + // self.create_server(); + } + } + + /// runs all servers, and returns a vector of references to the servers + /// once they've all been run + /// + + #[allow(dead_code)] + pub fn run_all_servers(self) -> Arc>> { + let run_length = self.config.run_length_in_seconds; + let mut handles = vec![]; + + // return handles to all of the servers, wrapped in mutexes, handles, etc + let return_containers = Arc::new(Mutex::new(Vec::new())); + + let is_seeding = self.is_seeding.clone(); + + for mut s in self.server_containers { + let return_container_ref = return_containers.clone(); + let handle = thread::spawn(move || { + if is_seeding && !s.config.is_seeding { + // there's a seed and we're not it, so hang around longer and give the seed + // a chance to start + thread::sleep(time::Duration::from_millis(2000)); + } + let server_ref = s.run_server(run_length); + return_container_ref.lock().push(server_ref); + }); + // Not a big fan of sleeping hack here, but there appears to be a + // concurrency issue when creating files in rocksdb that causes + // failure if we don't pause a bit before starting the next server + thread::sleep(time::Duration::from_millis(500)); + handles.push(handle); + } + + for handle in handles { + match handle.join() { + Ok(_) => {} + Err(e) => { + println!("Error starting server thread: {:?}", e); + panic!(e); + } + } + } + + // return a much simplified version of the results + return_containers.clone() + } + + #[allow(dead_code)] + pub fn connect_all_peers(&mut self) { + // just pull out all currently active servers, build a list, + // and feed into all servers + let mut server_addresses: Vec = Vec::new(); + for s in &self.server_containers { + let server_address = format!("{}:{}", s.config.base_addr, s.config.p2p_server_port); + server_addresses.push(server_address); + } + + for a in server_addresses { + for s in &mut self.server_containers { + if format!("{}:{}", s.config.base_addr, s.config.p2p_server_port) != a { + s.add_peer(a.clone()); + } + } + } + } +} + +#[allow(dead_code)] +pub fn stop_all_servers(servers: Arc>>) { + let locked_servs = servers.lock(); + for s in locked_servs.deref() { + s.stop(); + } +} + +/// Create and return a ServerConfig +#[allow(dead_code)] +pub fn config(n: u16, test_name_dir: &str, seed_n: u16) -> servers::ServerConfig { + servers::ServerConfig { + api_http_addr: format!("127.0.0.1:{}", 20000 + n), + api_secret_path: None, + db_root: format!("target/tmp/{}/grin-sync-{}", test_name_dir, n), + p2p_config: p2p::P2PConfig { + port: 10000 + n, + seeding_type: p2p::Seeding::List, + seeds: Some(vec![PeerAddr::from_ip( + format!("127.0.0.1:{}", 10000 + seed_n).parse().unwrap(), + )]), + ..p2p::P2PConfig::default() + }, + chain_type: core::global::ChainTypes::AutomatedTesting, + archive_mode: Some(true), + skip_sync_wait: Some(true), + ..Default::default() + } +} + +/// return stratum mining config +#[allow(dead_code)] +pub fn stratum_config() -> servers::common::types::StratumServerConfig { + servers::common::types::StratumServerConfig { + enable_stratum_server: Some(true), + stratum_server_addr: Some(String::from("127.0.0.1:13416")), + attempt_time_per_block: 60, + minimum_share_difficulty: 1, + wallet_listener_url: String::from("http://127.0.0.1:13415"), + burn_reward: false, + } +} diff --git a/integration/tests/simulnet.rs b/integration/tests/simulnet.rs new file mode 100644 index 0000000..df407ec --- /dev/null +++ b/integration/tests/simulnet.rs @@ -0,0 +1,1007 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +extern crate grin_apiwallet as apiwallet; +extern crate grin_libwallet as libwallet; +extern crate grin_refwallet as wallet; +extern crate grin_wallet_config as wallet_config; +#[macro_use] +extern crate log; + +mod framework; + +use self::core::core::hash::Hashed; +use self::core::global::{self, ChainTypes}; +use self::libwallet::types::{WalletBackend, WalletInst}; +use self::util::{Mutex, StopState}; +use self::wallet::controller; +use self::wallet::lmdb_wallet::LMDBBackend; +use self::wallet::{HTTPNodeClient, HTTPWalletCommAdapter}; +use self::wallet_config::WalletConfig; +use grin_api as api; +use grin_core as core; +use grin_keychain as keychain; +use grin_p2p as p2p; +use grin_servers as servers; +use grin_util as util; +use p2p::PeerAddr; +use std::cmp; +use std::default::Default; +use std::process::exit; +use std::sync::Arc; +use std::{thread, time}; + +use crate::framework::{ + config, stop_all_servers, LocalServerContainerConfig, LocalServerContainerPool, + LocalServerContainerPoolConfig, +}; + +/// Testing the frameworks by starting a fresh server, creating a genesis +/// Block and mining into a wallet for a bit +#[test] +fn basic_genesis_mine() { + util::init_test_logger(); + global::set_local_chain_type(ChainTypes::AutomatedTesting); + + let test_name_dir = "genesis_mine"; + framework::clean_all_output(test_name_dir); + + // Create a server pool + let mut pool_config = LocalServerContainerPoolConfig::default(); + pool_config.base_name = String::from(test_name_dir); + pool_config.run_length_in_seconds = 10; + + pool_config.base_api_port = 30000; + pool_config.base_p2p_port = 31000; + pool_config.base_wallet_port = 32000; + + let mut pool = LocalServerContainerPool::new(pool_config); + + // Create a server to add into the pool + let mut server_config = LocalServerContainerConfig::default(); + server_config.start_miner = true; + server_config.start_wallet = false; + server_config.burn_mining_rewards = true; + + pool.create_server(&mut server_config); + let servers = pool.run_all_servers(); + stop_all_servers(servers); +} + +/// Creates 5 servers, first being a seed and check that through peer address +/// messages they all end up connected. +#[test] +fn simulate_seeding() { + util::init_test_logger(); + global::set_local_chain_type(ChainTypes::AutomatedTesting); + + let test_name_dir = "simulate_seeding"; + framework::clean_all_output(test_name_dir); + + // Create a server pool + let mut pool_config = LocalServerContainerPoolConfig::default(); + pool_config.base_name = test_name_dir.to_string(); + pool_config.run_length_in_seconds = 30; + + // have to use different ports because of tests being run in parallel + pool_config.base_api_port = 30020; + pool_config.base_p2p_port = 31020; + pool_config.base_wallet_port = 32020; + + let mut pool = LocalServerContainerPool::new(pool_config); + + // Create a first seed server to add into the pool + let mut server_config = LocalServerContainerConfig::default(); + // server_config.start_miner = true; + server_config.start_wallet = false; + server_config.burn_mining_rewards = true; + server_config.is_seeding = true; + + pool.create_server(&mut server_config); + + // wait the seed server fully start up before start remaining servers + thread::sleep(time::Duration::from_millis(1_000)); + + // point next servers at first seed + server_config.is_seeding = false; + server_config.seed_addr = format!( + "{}:{}", + server_config.base_addr, server_config.p2p_server_port + ); + + for _ in 0..4 { + pool.create_server(&mut server_config); + } + + let servers = pool.run_all_servers(); + thread::sleep(time::Duration::from_secs(5)); + + // Check they all end up connected. + let url = format!( + "http://{}:{}/v1/peers/connected", + &server_config.base_addr, 30020 + ); + let peers_all = api::client::get::>(url.as_str(), None); + assert!(peers_all.is_ok()); + assert_eq!(peers_all.unwrap().len(), 4); + + stop_all_servers(servers); + + // wait servers fully stop before start next automated test + thread::sleep(time::Duration::from_millis(1_000)); +} + +/// Create 1 server, start it mining, then connect 4 other peers mining and +/// using the first as a seed. Meant to test the evolution of mining difficulty with miners +/// running at different rates. +/// +/// TODO: Just going to comment this out as an automatically run test for the time +/// being, As it's more for actively testing and hurts CI a lot +#[ignore] +#[test] +fn simulate_parallel_mining() { + global::set_local_chain_type(ChainTypes::AutomatedTesting); + + let test_name_dir = "simulate_parallel_mining"; + // framework::clean_all_output(test_name_dir); + + // Create a server pool + let mut pool_config = LocalServerContainerPoolConfig::default(); + pool_config.base_name = test_name_dir.to_string(); + pool_config.run_length_in_seconds = 60; + // have to use different ports because of tests being run in parallel + pool_config.base_api_port = 30040; + pool_config.base_p2p_port = 31040; + pool_config.base_wallet_port = 32040; + + let mut pool = LocalServerContainerPool::new(pool_config); + + // Create a first seed server to add into the pool + let mut server_config = LocalServerContainerConfig::default(); + server_config.start_miner = true; + server_config.start_wallet = true; + server_config.is_seeding = true; + + pool.create_server(&mut server_config); + + // point next servers at first seed + server_config.is_seeding = false; + server_config.seed_addr = format!( + "{}:{}", + server_config.base_addr, server_config.p2p_server_port + ); + + // And create 4 more, then let them run for a while + for i in 1..4 { + // fudge in some slowdown + server_config.miner_slowdown_in_millis = i * 2; + pool.create_server(&mut server_config); + } + + // pool.connect_all_peers(); + + let servers = pool.run_all_servers(); + stop_all_servers(servers); + + // Check mining difficulty here?, though I'd think it's more valuable + // to simply output it. Can at least see the evolution of the difficulty target + // in the debug log output for now +} + +// TODO: Convert these tests to newer framework format +/// Create a network of 5 servers and mine a block, verifying that the block +/// gets propagated to all. +#[test] +fn simulate_block_propagation() { + util::init_test_logger(); + + // we actually set the chain_type in the ServerConfig below + // TODO - avoid needing to set it in two places? + global::set_local_chain_type(ChainTypes::AutomatedTesting); + + let test_name_dir = "grin-prop"; + framework::clean_all_output(test_name_dir); + + // instantiates 5 servers on different ports + let mut servers = vec![]; + for n in 0..5 { + let s = servers::Server::new(framework::config(10 * n, test_name_dir, 0)).unwrap(); + servers.push(s); + thread::sleep(time::Duration::from_millis(100)); + } + + // start mining + let stop = Arc::new(Mutex::new(StopState::new())); + servers[0].start_test_miner(None, stop.clone()); + + // monitor for a change of head on a different server and check whether + // chain height has changed + let mut success = false; + let mut time_spent = 0; + loop { + let mut count = 0; + for n in 0..5 { + if servers[n].head().height > 3 { + count += 1; + } + } + if count == 5 { + success = true; + break; + } + thread::sleep(time::Duration::from_millis(1_000)); + time_spent += 1; + if time_spent >= 30 { + info!("simulate_block_propagation - fail on timeout",); + break; + } + + // stop mining after 8s + if time_spent == 8 { + servers[0].stop_test_miner(stop.clone()); + } + } + for n in 0..5 { + servers[n].stop(); + } + assert_eq!(true, success); + + // wait servers fully stop before start next automated test + thread::sleep(time::Duration::from_millis(1_000)); +} + +/// Creates 2 different disconnected servers, mine a few blocks on one, connect +/// them and check that the 2nd gets all the blocks +#[test] +fn simulate_full_sync() { + util::init_test_logger(); + + // we actually set the chain_type in the ServerConfig below + global::set_local_chain_type(ChainTypes::AutomatedTesting); + + let test_name_dir = "grin-sync"; + framework::clean_all_output(test_name_dir); + + let s1 = servers::Server::new(framework::config(1000, "grin-sync", 1000)).unwrap(); + // mine a few blocks on server 1 + let stop = Arc::new(Mutex::new(StopState::new())); + s1.start_test_miner(None, stop.clone()); + thread::sleep(time::Duration::from_secs(8)); + s1.stop_test_miner(stop); + + let s2 = servers::Server::new(framework::config(1001, "grin-sync", 1000)).unwrap(); + + // Get the current header from s1. + let s1_header = s1.chain.head_header().unwrap(); + info!( + "simulate_full_sync - s1 header head: {} at {}", + s1_header.hash(), + s1_header.height + ); + + // Wait for s2 to sync up to and including the header from s1. + let mut time_spent = 0; + while s2.head().height < s1_header.height { + thread::sleep(time::Duration::from_millis(1_000)); + time_spent += 1; + if time_spent >= 30 { + info!( + "sync fail. s2.head().height: {}, s1_header.height: {}", + s2.head().height, + s1_header.height + ); + break; + } + } + + // Confirm both s1 and s2 see a consistent header at that height. + let s2_header = s2.chain.get_block_header(&s1_header.hash()).unwrap(); + assert_eq!(s1_header, s2_header); + + // Stop our servers cleanly. + s1.stop(); + s2.stop(); + + // wait servers fully stop before start next automated test + thread::sleep(time::Duration::from_millis(1_000)); +} + +/// Creates 2 different disconnected servers, mine a few blocks on one, connect +/// them and check that the 2nd gets all using fast sync algo +#[test] +fn simulate_fast_sync() { + util::init_test_logger(); + + // we actually set the chain_type in the ServerConfig below + global::set_local_chain_type(ChainTypes::AutomatedTesting); + + let test_name_dir = "grin-fast"; + framework::clean_all_output(test_name_dir); + + // start s1 and mine enough blocks to get beyond the fast sync horizon + let s1 = servers::Server::new(framework::config(2000, "grin-fast", 2000)).unwrap(); + let stop = Arc::new(Mutex::new(StopState::new())); + s1.start_test_miner(None, stop.clone()); + + while s1.head().height < 20 { + thread::sleep(time::Duration::from_millis(1_000)); + } + s1.stop_test_miner(stop); + + let mut conf = config(2001, "grin-fast", 2000); + conf.archive_mode = Some(false); + + let s2 = servers::Server::new(conf).unwrap(); + + // Get the current header from s1. + let s1_header = s1.chain.head_header().unwrap(); + + // Wait for s2 to sync up to and including the header from s1. + let mut total_wait = 0; + while s2.head().height < s1_header.height { + thread::sleep(time::Duration::from_millis(1_000)); + total_wait += 1; + if total_wait >= 30 { + error!( + "simulate_fast_sync test fail on timeout! s2 height: {}, s1 height: {}", + s2.head().height, + s1_header.height, + ); + break; + } + } + + // Confirm both s1 and s2 see a consistent header at that height. + let s2_header = s2.chain.get_block_header(&s1_header.hash()).unwrap(); + assert_eq!(s1_header, s2_header); + + // Stop our servers cleanly. + s1.stop(); + s2.stop(); + + // wait servers fully stop before start next automated test + thread::sleep(time::Duration::from_millis(1_000)); +} + +/// Preparation: +/// Creates 6 disconnected servers: A, B, C, D, E and F, mine 80 blocks on A, +/// Compact server A. +/// Connect all servers, check all get state_sync_threshold full blocks using fast sync. +/// Disconnect all servers from each other. +/// +/// Test case 1: nodes that just synced is able to handle forks of up to state_sync_threshold +/// Mine state_sync_threshold-7 blocks on A +/// Mine state_sync_threshold-1 blocks on C (long fork), connect C to server A +/// check server A can sync to C without txhashset download. +/// +/// Test case 2: nodes with history in between state_sync_threshold and cut_through_horizon will +/// be able to handle forks larger than state_sync_threshold but not as large as cut_through_horizon. +/// Mine 20 blocks on A (then A has 59 blocks in local chain) +/// Mine cut_through_horizon-1 blocks on D (longer fork), connect D to servers A, then fork point +/// is at A's body head.height - 39, and 20 < 39 < 70. +/// check server A can sync without txhashset download. +/// +/// Test case 3: nodes that have enough history is able to handle forks of up to cut_through_horizon +/// Mine cut_through_horizon+10 blocks on E, connect E to servers A and B +/// check server A can sync to E without txhashset download. +/// check server B can sync to E but need txhashset download. +/// +/// Test case 4: nodes which had a success state sync can have a new state sync if needed. +/// Mine cut_through_horizon+20 blocks on F (longer fork than E), connect F to servers B +/// check server B can sync to F with txhashset download. +/// +/// Test case 5: normal sync (not a fork) should not trigger a txhashset download +/// Mine cut_through_horizon-10 blocks on F, connect F to servers B +/// check server B can sync to F without txhashset download. +/// +/// Test case 6: far behind sync (not a fork) should trigger a txhashset download +/// Mine cut_through_horizon+1 blocks on F, connect F to servers B +/// check server B can sync to F with txhashset download. +/// +/// +#[ignore] +#[test] +fn simulate_long_fork() { + util::init_test_logger(); + println!("starting simulate_long_fork"); + + // we actually set the chain_type in the ServerConfig below + global::set_local_chain_type(ChainTypes::AutomatedTesting); + + let test_name_dir = "grin-long-fork"; + framework::clean_all_output(test_name_dir); + + let s = long_fork_test_preparation(); + for si in &s { + si.pause(); + } + thread::sleep(time::Duration::from_millis(1_000)); + + long_fork_test_case_1(&s); + thread::sleep(time::Duration::from_millis(1_000)); + + long_fork_test_case_2(&s); + thread::sleep(time::Duration::from_millis(1_000)); + + long_fork_test_case_3(&s); + thread::sleep(time::Duration::from_millis(1_000)); + + long_fork_test_case_4(&s); + thread::sleep(time::Duration::from_millis(1_000)); + + long_fork_test_case_5(&s); + + // Clean up + for si in &s { + si.stop(); + } + + // wait servers fully stop before start next automated test + thread::sleep(time::Duration::from_millis(1_000)); +} + +fn long_fork_test_preparation() -> Vec { + println!("preparation: mine 80 blocks, create 6 servers and sync all of them"); + + let mut s: Vec = vec![]; + + // start server A and mine 80 blocks to get beyond the fast sync horizon + let mut conf = framework::config(2100, "grin-long-fork", 2100); + conf.archive_mode = Some(false); + conf.api_secret_path = None; + let s0 = servers::Server::new(conf).unwrap(); + thread::sleep(time::Duration::from_millis(1_000)); + s.push(s0); + let stop = Arc::new(Mutex::new(StopState::new())); + s[0].start_test_miner(None, stop.clone()); + + while s[0].head().height < global::cut_through_horizon() as u64 + 10 { + thread::sleep(time::Duration::from_millis(1_000)); + } + s[0].stop_test_miner(stop); + thread::sleep(time::Duration::from_millis(1_000)); + + // Get the current header from s0. + let s0_header = s[0].chain.head().unwrap(); + + // check the tail after compacting + let _ = s[0].chain.compact(); + let s0_tail = s[0].chain.tail().unwrap(); + assert_eq!( + s0_header.height - global::cut_through_horizon() as u64, + s0_tail.height + ); + + for i in 1..6 { + let mut conf = config(2100 + i, "grin-long-fork", 2100); + conf.archive_mode = Some(false); + conf.api_secret_path = None; + let si = servers::Server::new(conf).unwrap(); + s.push(si); + } + thread::sleep(time::Duration::from_millis(1_000)); + + // Wait for s[1..5] to sync up to and including the header from s0. + let mut total_wait = 0; + let mut min_height = 0; + while min_height < s0_header.height { + thread::sleep(time::Duration::from_millis(1_000)); + total_wait += 1; + if total_wait >= 60 { + println!( + "simulate_long_fork (preparation) test fail on timeout! minimum height: {}, s0 height: {}", + min_height, + s0_header.height, + ); + exit(1); + } + min_height = s0_header.height; + for i in 1..6 { + min_height = cmp::min(s[i].head().height, min_height); + } + } + + // Confirm both s0 and s1 see a consistent header at that height. + let s1_header = s[1].chain.head().unwrap(); + assert_eq!(s0_header, s1_header); + println!( + "preparation done. all 5 servers head.height: {}", + s0_header.height + ); + + // Wait for peers fully connection + let mut total_wait = 0; + let mut min_peers = 0; + while min_peers < 4 { + thread::sleep(time::Duration::from_millis(1_000)); + total_wait += 1; + if total_wait >= 60 { + println!( + "simulate_long_fork (preparation) test fail on timeout! minimum connected peers: {}", + min_peers, + ); + exit(1); + } + min_peers = 4; + for i in 0..5 { + let peers_connected = get_connected_peers(&"127.0.0.1".to_owned(), 22100 + i); + min_peers = cmp::min(min_peers, peers_connected.len()); + } + } + + return s; +} + +fn long_fork_test_mining(blocks: u64, n: u16, s: &servers::Server) { + // Get the current header from node. + let sn_header = s.chain.head().unwrap(); + + // Mining + let stop = Arc::new(Mutex::new(StopState::new())); + s.start_test_miner(None, stop.clone()); + + while s.head().height < sn_header.height + blocks { + thread::sleep(time::Duration::from_millis(1)); + } + s.stop_test_miner(stop); + thread::sleep(time::Duration::from_millis(1_000)); + println!( + "{} blocks mined on s{}. s{}.height: {} (old height: {})", + s.head().height - sn_header.height, + n, + n, + s.head().height, + sn_header.height, + ); + + let _ = s.chain.compact(); + let sn_header = s.chain.head().unwrap(); + let sn_tail = s.chain.tail().unwrap(); + println!( + "after compacting, s{}.head().height: {}, s{}.tail().height: {}", + n, sn_header.height, n, sn_tail.height, + ); +} + +fn long_fork_test_case_1(s: &[servers::Server]) { + println!("\ntest case 1 start"); + + // Mine state_sync_threshold-7 blocks on s0 + long_fork_test_mining(global::state_sync_threshold() as u64 - 7, 0, &s[0]); + + // Mine state_sync_threshold-1 blocks on s2 (long fork), a fork with more work than s0 chain + long_fork_test_mining(global::state_sync_threshold() as u64 - 1, 2, &s[2]); + + let s2_header = s[2].chain.head().unwrap(); + let s0_header = s[0].chain.head().unwrap(); + let s0_tail = s[0].chain.tail().unwrap(); + println!( + "test case 1: s0 start syncing with s2... s0.head().height: {}, s2.head().height: {}", + s0_header.height, s2_header.height, + ); + s[0].resume(); + s[2].resume(); + + // Check server s0 can sync to s2 without txhashset download. + let mut total_wait = 0; + while s[0].head().height < s2_header.height { + thread::sleep(time::Duration::from_millis(1_000)); + total_wait += 1; + if total_wait >= 120 { + println!( + "test case 1: test fail on timeout! s0 height: {}, s2 height: {}", + s[0].head().height, + s2_header.height, + ); + exit(1); + } + } + let s0_tail_new = s[0].chain.tail().unwrap(); + assert_eq!(s0_tail_new.height, s0_tail.height); + println!( + "test case 1: s0.head().height: {}, s2_header.height: {}", + s[0].head().height, + s2_header.height, + ); + assert_eq!(s[0].head().last_block_h, s2_header.last_block_h); + + s[0].pause(); + s[2].stop(); + println!("test case 1 passed") +} + +fn long_fork_test_case_2(s: &[servers::Server]) { + println!("\ntest case 2 start"); + + // Mine 20 blocks on s0 + long_fork_test_mining(20, 0, &s[0]); + + // Mine cut_through_horizon-1 blocks on s3 (longer fork) + long_fork_test_mining(global::cut_through_horizon() as u64 - 1, 3, &s[3]); + let s3_header = s[3].chain.head().unwrap(); + let s0_header = s[0].chain.head().unwrap(); + let s0_tail = s[0].chain.tail().unwrap(); + println!( + "test case 2: s0 start syncing with s3. s0.head().height: {}, s3.head().height: {}", + s0_header.height, s3_header.height, + ); + s[0].resume(); + s[3].resume(); + + // Check server s0 can sync to s3 without txhashset download. + let mut total_wait = 0; + while s[0].head().height < s3_header.height { + thread::sleep(time::Duration::from_millis(1_000)); + total_wait += 1; + if total_wait >= 120 { + println!( + "test case 2: test fail on timeout! s0 height: {}, s3 height: {}", + s[0].head().height, + s3_header.height, + ); + exit(1); + } + } + let s0_tail_new = s[0].chain.tail().unwrap(); + assert_eq!(s0_tail_new.height, s0_tail.height); + assert_eq!(s[0].head().hash(), s3_header.hash()); + + let _ = s[0].chain.compact(); + let s0_header = s[0].chain.head().unwrap(); + let s0_tail = s[0].chain.tail().unwrap(); + println!( + "test case 2: after compacting, s0.head().height: {}, s0.tail().height: {}", + s0_header.height, s0_tail.height, + ); + + s[0].pause(); + s[3].stop(); + println!("test case 2 passed") +} + +fn long_fork_test_case_3(s: &[servers::Server]) { + println!("\ntest case 3 start"); + + // Mine cut_through_horizon+1 blocks on s4 + long_fork_test_mining(global::cut_through_horizon() as u64 + 10, 4, &s[4]); + + let s4_header = s[4].chain.head().unwrap(); + let s0_header = s[0].chain.head().unwrap(); + let s0_tail = s[0].chain.tail().unwrap(); + let s1_header = s[1].chain.head().unwrap(); + let s1_tail = s[1].chain.tail().unwrap(); + println!( + "test case 3: s0/1 start syncing with s4. s0.head().height: {}, s0.tail().height: {}, s1.head().height: {}, s1.tail().height: {}, s4.head().height: {}", + s0_header.height, s0_tail.height, + s1_header.height, s1_tail.height, + s4_header.height, + ); + s[0].resume(); + s[4].resume(); + + // Check server s0 can sync to s4. + let mut total_wait = 0; + while s[0].head().height < s4_header.height { + thread::sleep(time::Duration::from_millis(1_000)); + total_wait += 1; + if total_wait >= 120 { + println!( + "test case 3: test fail on timeout! s0 height: {}, s4 height: {}", + s[0].head().height, + s4_header.height, + ); + exit(1); + } + } + assert_eq!(s[0].head().hash(), s4_header.hash()); + + s[0].stop(); + s[1].resume(); + + // Check server s1 can sync to s4 but with txhashset download. + let mut total_wait = 0; + while s[1].head().height < s4_header.height { + thread::sleep(time::Duration::from_millis(1_000)); + total_wait += 1; + if total_wait >= 120 { + println!( + "test case 3: test fail on timeout! s1 height: {}, s4 height: {}", + s[1].head().height, + s4_header.height, + ); + exit(1); + } + } + let s1_tail_new = s[1].chain.tail().unwrap(); + println!( + "test case 3: s[1].tail().height: {}, old height: {}", + s1_tail_new.height, s1_tail.height + ); + assert_ne!(s1_tail_new.height, s1_tail.height); + assert_eq!(s[1].head().hash(), s4_header.hash()); + + s[1].pause(); + s[4].pause(); + println!("test case 3 passed") +} + +fn long_fork_test_case_4(s: &[servers::Server]) { + println!("\ntest case 4 start"); + + let _ = s[1].chain.compact(); + + // Mine cut_through_horizon+20 blocks on s5 (longer fork than s4) + long_fork_test_mining(global::cut_through_horizon() as u64 + 20, 5, &s[5]); + + let s5_header = s[5].chain.head().unwrap(); + let s1_header = s[1].chain.head().unwrap(); + let s1_tail = s[1].chain.tail().unwrap(); + println!( + "test case 4: s1 start syncing with s5. s1.head().height: {}, s1.tail().height: {}, s5.head().height: {}", + s1_header.height, s1_tail.height, + s5_header.height, + ); + s[1].resume(); + s[5].resume(); + + // Check server s1 can sync to s5 with a new txhashset download. + let mut total_wait = 0; + while s[1].head().height < s5_header.height { + thread::sleep(time::Duration::from_millis(1_000)); + total_wait += 1; + if total_wait >= 120 { + println!( + "test case 4: test fail on timeout! s1 height: {}, s5 height: {}", + s[1].head().height, + s5_header.height, + ); + exit(1); + } + } + let s1_tail_new = s[1].chain.tail().unwrap(); + println!( + "test case 4: s[1].tail().height: {}, old height: {}", + s1_tail_new.height, s1_tail.height + ); + assert_ne!(s1_tail_new.height, s1_tail.height); + assert_eq!(s[1].head().hash(), s5_header.hash()); + + s[1].pause(); + s[5].pause(); + + println!("test case 4 passed") +} + +fn long_fork_test_case_5(s: &[servers::Server]) { + println!("\ntest case 5 start"); + + let _ = s[1].chain.compact(); + + // Mine cut_through_horizon-10 blocks on s5 + long_fork_test_mining(global::cut_through_horizon() as u64 - 10, 5, &s[5]); + + let s5_header = s[5].chain.head().unwrap(); + let s1_header = s[1].chain.head().unwrap(); + let s1_tail = s[1].chain.tail().unwrap(); + println!( + "test case 5: s1 start syncing with s5. s1.head().height: {}, s1.tail().height: {}, s5.head().height: {}", + s1_header.height, s1_tail.height, + s5_header.height, + ); + s[1].resume(); + s[5].resume(); + + // Check server s1 can sync to s5 without a txhashset download (normal body sync) + let mut total_wait = 0; + while s[1].head().height < s5_header.height { + thread::sleep(time::Duration::from_millis(1_000)); + total_wait += 1; + if total_wait >= 120 { + println!( + "test case 5: test fail on timeout! s1 height: {}, s5 height: {}", + s[1].head().height, + s5_header.height, + ); + exit(1); + } + } + let s1_tail_new = s[1].chain.tail().unwrap(); + println!( + "test case 5: s[1].tail().height: {}, old height: {}", + s1_tail_new.height, s1_tail.height + ); + assert_eq!(s1_tail_new.height, s1_tail.height); + assert_eq!(s[1].head().hash(), s5_header.hash()); + + s[1].pause(); + s[5].pause(); + + println!("test case 5 passed") +} + +#[allow(dead_code)] +fn long_fork_test_case_6(s: &[servers::Server]) { + println!("\ntest case 6 start"); + + let _ = s[1].chain.compact(); + + // Mine cut_through_horizon+1 blocks on s5 + long_fork_test_mining(global::cut_through_horizon() as u64 + 1, 5, &s[5]); + + let s5_header = s[5].chain.head().unwrap(); + let s1_header = s[1].chain.head().unwrap(); + let s1_tail = s[1].chain.tail().unwrap(); + println!( + "test case 6: s1 start syncing with s5. s1.head().height: {}, s1.tail().height: {}, s5.head().height: {}", + s1_header.height, s1_tail.height, + s5_header.height, + ); + s[1].resume(); + s[5].resume(); + + // Check server s1 can sync to s5 without a txhashset download (normal body sync) + let mut total_wait = 0; + while s[1].head().height < s5_header.height { + thread::sleep(time::Duration::from_millis(1_000)); + total_wait += 1; + if total_wait >= 120 { + println!( + "test case 6: test fail on timeout! s1 height: {}, s5 height: {}", + s[1].head().height, + s5_header.height, + ); + exit(1); + } + } + let s1_tail_new = s[1].chain.tail().unwrap(); + println!( + "test case 6: s[1].tail().height: {}, old height: {}", + s1_tail_new.height, s1_tail.height + ); + assert_eq!(s1_tail_new.height, s1_tail.height); + assert_eq!(s[1].head().hash(), s5_header.hash()); + + s[1].pause(); + s[5].pause(); + + println!("test case 6 passed") +} + +pub fn create_wallet( + dir: &str, + client_n: HTTPNodeClient, +) -> Arc>> { + let mut wallet_config = WalletConfig::default(); + wallet_config.data_file_dir = String::from(dir); + let _ = wallet::WalletSeed::init_file(&wallet_config, 32, None, ""); + let mut wallet: LMDBBackend = + LMDBBackend::new(wallet_config.clone(), "", client_n).unwrap_or_else(|e| { + panic!("Error creating wallet: {:?} Config: {:?}", e, wallet_config) + }); + wallet.open_with_credentials().unwrap_or_else(|e| { + panic!( + "Error initializing wallet: {:?} Config: {:?}", + e, wallet_config + ) + }); + Arc::new(Mutex::new(wallet)) +} + +/// Intended to replicate https://github.com/mimblewimble/grin/issues/1325 +#[ignore] +#[test] +fn replicate_tx_fluff_failure() { + util::init_test_logger(); + global::set_local_chain_type(ChainTypes::UserTesting); + framework::clean_all_output("tx_fluff"); + + // Create Wallet 1 (Mining Input) and start it listening + // Wallet 1 post to another node, just for fun + let client1 = HTTPNodeClient::new("http://127.0.0.1:23003", None); + let client1_w = HTTPWalletCommAdapter::new(); + let wallet1 = create_wallet("target/tmp/tx_fluff/wallet1", client1.clone()); + let _wallet1_handle = thread::spawn(move || { + controller::foreign_listener(wallet1, "127.0.0.1:33000", None) + .unwrap_or_else(|e| panic!("Error creating wallet1 listener: {:?}", e,)); + }); + + // Create Wallet 2 (Recipient) and launch + let client2 = HTTPNodeClient::new("http://127.0.0.1:23001", None); + let wallet2 = create_wallet("target/tmp/tx_fluff/wallet2", client2.clone()); + let _wallet2_handle = thread::spawn(move || { + controller::foreign_listener(wallet2, "127.0.0.1:33001", None) + .unwrap_or_else(|e| panic!("Error creating wallet2 listener: {:?}", e,)); + }); + + // Server 1 (mines into wallet 1) + let mut s1_config = framework::config(3000, "tx_fluff", 3000); + s1_config.test_miner_wallet_url = Some("http://127.0.0.1:33000".to_owned()); + s1_config.dandelion_config.embargo_secs = Some(10); + s1_config.dandelion_config.patience_secs = Some(1); + s1_config.dandelion_config.relay_secs = Some(1); + let s1 = servers::Server::new(s1_config.clone()).unwrap(); + // Mine off of server 1 + s1.start_test_miner(s1_config.test_miner_wallet_url, s1.stop_state.clone()); + thread::sleep(time::Duration::from_secs(5)); + + // Server 2 (another node) + let mut s2_config = framework::config(3001, "tx_fluff", 3001); + s2_config.p2p_config.seeds = Some(vec![PeerAddr::from_ip("127.0.0.1:13000".parse().unwrap())]); + s2_config.dandelion_config.embargo_secs = Some(10); + s2_config.dandelion_config.patience_secs = Some(1); + s2_config.dandelion_config.relay_secs = Some(1); + let _s2 = servers::Server::new(s2_config.clone()).unwrap(); + + let dl_nodes = 5; + + for i in 0..dl_nodes { + // (create some stem nodes) + let mut s_config = framework::config(3002 + i, "tx_fluff", 3002 + i); + s_config.p2p_config.seeds = + Some(vec![PeerAddr::from_ip("127.0.0.1:13000".parse().unwrap())]); + s_config.dandelion_config.embargo_secs = Some(10); + s_config.dandelion_config.patience_secs = Some(1); + s_config.dandelion_config.relay_secs = Some(1); + let _ = servers::Server::new(s_config.clone()).unwrap(); + } + + thread::sleep(time::Duration::from_secs(10)); + + // get another instance of wallet1 (to update contents and perform a send) + let wallet1 = create_wallet("target/tmp/tx_fluff/wallet1", client1.clone()); + + let amount = 30_000_000_000; + let dest = "http://127.0.0.1:33001"; + + wallet::controller::owner_single_use(wallet1, |api| { + let (mut slate, lock_fn) = api.initiate_tx( + None, amount, // amount + 2, // minimum confirmations + 500, // max outputs + 1000, // num change outputs + true, // select all outputs + None, + )?; + slate = client1_w.send_tx_sync(dest, &slate)?; + slate = api.finalize_tx(&slate)?; + api.tx_lock_outputs(&slate, lock_fn)?; + api.post_tx(&slate.tx, false)?; + Ok(()) + }) + .unwrap(); + + // Give some time for propagation and mining + thread::sleep(time::Duration::from_secs(200)); + + // get another instance of wallet (to check contents) + let wallet2 = create_wallet("target/tmp/tx_fluff/wallet2", client2.clone()); + + wallet::controller::owner_single_use(wallet2, |api| { + let res = api.retrieve_summary_info(true, 1).unwrap(); + assert_eq!(res.1.amount_currently_spendable, amount); + Ok(()) + }) + .unwrap(); +} + +fn get_connected_peers( + base_addr: &String, + api_server_port: u16, +) -> Vec { + let url = format!( + "http://{}:{}/v1/peers/connected", + base_addr, api_server_port + ); + api::client::get::>(url.as_str(), None).unwrap() +} diff --git a/integration/tests/stratum.rs b/integration/tests/stratum.rs new file mode 100644 index 0000000..ac19e79 --- /dev/null +++ b/integration/tests/stratum.rs @@ -0,0 +1,177 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#[macro_use] +extern crate log; + +mod framework; + +use self::core::global::{self, ChainTypes}; +use crate::framework::{config, stratum_config}; +use bufstream::BufStream; +use grin_core as core; +use grin_servers as servers; +use grin_util as util; +use grin_util::{Mutex, StopState}; +use serde_json::Value; +use std::io::prelude::{BufRead, Write}; +use std::net::TcpStream; +use std::process; +use std::sync::Arc; +use std::{thread, time}; + +// Create a grin server, and a stratum server. +// Simulate a few JSONRpc requests and verify the results. +// Validate disconnected workers +// Validate broadcasting new jobs +#[test] +fn basic_stratum_server() { + util::init_test_logger(); + global::set_local_chain_type(ChainTypes::AutomatedTesting); + + let test_name_dir = "stratum_server"; + framework::clean_all_output(test_name_dir); + + // Create a server + let s = servers::Server::new(config(4000, test_name_dir, 0)).unwrap(); + + // Get mining config with stratumserver enabled + let mut stratum_cfg = stratum_config(); + stratum_cfg.burn_reward = true; + stratum_cfg.attempt_time_per_block = 999; + stratum_cfg.enable_stratum_server = Some(true); + stratum_cfg.stratum_server_addr = Some(String::from("127.0.0.1:11101")); + + // Start stratum server + s.start_stratum_server(stratum_cfg); + + // Wait for stratum server to start and + // Verify stratum server accepts connections + loop { + if let Ok(_stream) = TcpStream::connect("127.0.0.1:11101") { + break; + } else { + thread::sleep(time::Duration::from_millis(500)); + } + // As this stream falls out of scope it will be disconnected + } + info!("stratum server connected"); + + // Create a few new worker connections + let mut workers = vec![]; + for _n in 0..5 { + let w = TcpStream::connect("127.0.0.1:11101").unwrap(); + w.set_nonblocking(true) + .expect("Failed to set TcpStream to non-blocking"); + let stream = BufStream::new(w); + workers.push(stream); + } + assert!(workers.len() == 5); + info!("workers length verification ok"); + + // Simulate a worker lost connection + workers.remove(4); + + // Swallow the genesis block + thread::sleep(time::Duration::from_secs(5)); // Wait for the server to broadcast + let mut response = String::new(); + for n in 0..workers.len() { + let _result = workers[n].read_line(&mut response); + } + + // Verify a few stratum JSONRpc commands + // getjobtemplate - expected block template result + let mut response = String::new(); + let job_req = "{\"id\": \"Stratum\", \"jsonrpc\": \"2.0\", \"method\": \"getjobtemplate\"}\n"; + workers[2].write(job_req.as_bytes()).unwrap(); + workers[2].flush().unwrap(); + thread::sleep(time::Duration::from_secs(1)); // Wait for the server to reply + match workers[2].read_line(&mut response) { + Ok(_) => { + let r: Value = serde_json::from_str(&response).unwrap(); + assert_eq!(r["error"], serde_json::Value::Null); + assert_ne!(r["result"], serde_json::Value::Null); + } + Err(_e) => { + assert!(false); + } + } + info!("a few stratum JSONRpc commands verification ok"); + + // keepalive - expected "ok" result + let mut response = String::new(); + let job_req = "{\"id\":\"3\",\"jsonrpc\":\"2.0\",\"method\":\"keepalive\"}\n"; + let ok_resp = "{\"id\":\"3\",\"jsonrpc\":\"2.0\",\"method\":\"keepalive\",\"result\":\"ok\",\"error\":null}\n"; + workers[2].write(job_req.as_bytes()).unwrap(); + workers[2].flush().unwrap(); + thread::sleep(time::Duration::from_secs(1)); // Wait for the server to reply + let _st = workers[2].read_line(&mut response); + assert_eq!(response.as_str(), ok_resp); + info!("keepalive test ok"); + + // "doesnotexist" - error expected + let mut response = String::new(); + let job_req = "{\"id\":\"4\",\"jsonrpc\":\"2.0\",\"method\":\"doesnotexist\"}\n"; + let ok_resp = "{\"id\":\"4\",\"jsonrpc\":\"2.0\",\"method\":\"doesnotexist\",\"result\":null,\"error\":{\"code\":-32601,\"message\":\"Method not found\"}}\n"; + workers[3].write(job_req.as_bytes()).unwrap(); + workers[3].flush().unwrap(); + thread::sleep(time::Duration::from_secs(1)); // Wait for the server to reply + let _st = workers[3].read_line(&mut response); + assert_eq!(response.as_str(), ok_resp); + info!("worker doesnotexist test ok"); + + // Verify stratum server and worker stats + let stats = s.get_server_stats().unwrap(); + assert_eq!(stats.stratum_stats.block_height, 1); // just 1 genesis block + assert_eq!(stats.stratum_stats.num_workers, 4); // 5 - 1 = 4 + assert_eq!(stats.stratum_stats.worker_stats[5].is_connected, false); // worker was removed + assert_eq!(stats.stratum_stats.worker_stats[1].is_connected, true); + info!("stratum server and worker stats verification ok"); + + // Start mining blocks + let stop = Arc::new(Mutex::new(StopState::new())); + s.start_test_miner(None, stop.clone()); + info!("test miner started"); + + // This test is supposed to complete in 3 seconds, + // so let's set a timeout on 10s to avoid infinite waiting happened in Travis-CI. + let _handler = thread::spawn(|| { + thread::sleep(time::Duration::from_secs(10)); + error!("basic_stratum_server test fail on timeout!"); + thread::sleep(time::Duration::from_millis(100)); + process::exit(1); + }); + + // Simulate a worker lost connection + workers.remove(1); + + // Wait for a few mined blocks + thread::sleep(time::Duration::from_secs(3)); + s.stop_test_miner(stop); + + // Verify blocks are being broadcast to workers + let expected = String::from("job"); + let mut jobtemplate = String::new(); + let _st = workers[2].read_line(&mut jobtemplate); + let job_template: Value = serde_json::from_str(&jobtemplate).unwrap(); + assert_eq!(job_template["method"], expected); + info!("blocks broadcasting to workers test ok"); + + // Verify stratum server and worker stats + let stats = s.get_server_stats().unwrap(); + assert_eq!(stats.stratum_stats.num_workers, 3); // 5 - 2 = 3 + assert_eq!(stats.stratum_stats.worker_stats[2].is_connected, false); // worker was removed + assert_ne!(stats.stratum_stats.block_height, 1); + info!("basic_stratum_server test done and ok."); +} diff --git a/libwallet/Cargo.toml b/libwallet/Cargo.toml new file mode 100644 index 0000000..4041894 --- /dev/null +++ b/libwallet/Cargo.toml @@ -0,0 +1,73 @@ +[package] +name = "grin_wallet_libwallet" +version = "5.4.0-alpha.1" +authors = ["Grin Developers "] +description = "Simple, private and scalable cryptocurrency implementation based on the MimbleWimble chain format." +license = "Apache-2.0" +repository = "https://github.com/mimblewimble/grin-wallet" +keywords = [ "crypto", "grin", "mimblewimble" ] +exclude = ["**/*.grin", "**/*.grin2"] +#build = "src/build/build.rs" +edition = "2018" + +[dependencies] +blake2-rfc = "0.2" +rand = "0.6" +serde = "1" +serde_derive = "1" +serde_json = "1" +log = "0.4" +uuid = { version = "0.8", features = ["serde", "v4"] } +chrono = { version = "0.4.11", features = ["serde"] } +lazy_static = "1" +strum = "0.18" +strum_macros = "0.18" +thiserror = "1" +ed25519-dalek = "1.0.0-pre.4" +x25519-dalek = "0.6" +base64 = "0.9" +regex = "1.3" +sha2 = "0.10.0" +bs58 = "0.3" +age = "0.7" +curve25519-dalek = "2.1" +secrecy = "0.6" +bech32 = "0.7" +byteorder = "1.3" +num-bigint = "0.2" + +#mwixnet onion +chacha20 = "0.8.1" +hmac = { version = "0.12.0", features = ["std"]} + +grin_wallet_util = { path = "../util", version = "5.4.0-alpha.1" } +grin_wallet_config = { path = "../config", version = "5.4.0-alpha.1" } + +##### Grin Imports + +# For Release +grin_core = "5.3.3" +grin_keychain = "5.3.3" +grin_util = "5.3.3" +grin_store = "5.3.3" + +# For beta release + +# grin_core = { git = "https://github.com/mimblewimble/grin", tag = "v5.2.0-beta.3"} +# grin_keychain = { git = "https://github.com/mimblewimble/grin", tag = "v5.2.0-beta.3" } +# grin_util = { git = "https://github.com/mimblewimble/grin", tag = "v5.2.0-beta.3" } +# grin_store = { git = "https://github.com/mimblewimble/grin", tag = "v5.2.0-beta.3" } + +# For bleeding edge +# grin_core = { git = "https://github.com/mimblewimble/grin", branch = "master" } +# grin_keychain = { git = "https://github.com/mimblewimble/grin", branch = "master" } +# grin_util = { git = "https://github.com/mimblewimble/grin", branch = "master" } +# grin_store = { git = "https://github.com/mimblewimble/grin", branch = "master" } + +# For local testing +# grin_core = { path = "../../grin/core"} +# grin_keychain = { path = "../../grin/keychain"} +# grin_util = { path = "../../grin/util"} +# grin_store = { path = "../../grin/store"} + +##### diff --git a/libwallet/src/address.rs b/libwallet/src/address.rs new file mode 100644 index 0000000..8d55b39 --- /dev/null +++ b/libwallet/src/address.rs @@ -0,0 +1,49 @@ +// Copyright 2021 The Grin Develope; +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Functions defining wallet 'addresses', i.e. ed2559 keys based on +//! a derivation path + +use crate::grin_util::secp::key::SecretKey; +use crate::Error; +use grin_keychain::{ChildNumber, Identifier, Keychain, SwitchCommitmentType}; + +use crate::blake2::blake2b::blake2b; + +/// Derive a secret key given a derivation path and index +pub fn address_from_derivation_path( + keychain: &K, + parent_key_id: &Identifier, + index: u32, +) -> Result +where + K: Keychain, +{ + let mut key_path = parent_key_id.to_path(); + // An output derivation for acct m/0 + // is m/0/0/0, m/0/0/1 (for instance), m/1 is m/1/0/0, m/1/0/1 + // Address generation path should be + // for m/0: m/0/1/0, m/0/1/1 + // for m/1: m/1/1/0, m/1/1/1 + key_path.path[1] = ChildNumber::from(1); + key_path.depth += 1; + key_path.path[key_path.depth as usize - 1] = ChildNumber::from(index); + let key_id = Identifier::from_path(&key_path); + let sec_key = keychain.derive_key(0, &key_id, SwitchCommitmentType::None)?; + let hashed = blake2b(32, &[], &sec_key.0[..]); + Ok(SecretKey::from_slice( + &keychain.secp(), + &hashed.as_bytes()[..], + )?) +} diff --git a/libwallet/src/api_impl.rs b/libwallet/src/api_impl.rs new file mode 100644 index 0000000..88fa561 --- /dev/null +++ b/libwallet/src/api_impl.rs @@ -0,0 +1,27 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! lower-level wallet functions which build upon core::libtx to perform wallet +//! operations + +#![deny(non_upper_case_globals)] +#![deny(non_camel_case_types)] +#![deny(non_snake_case)] +#![deny(unused_mut)] +#![warn(missing_docs)] + +pub mod foreign; +pub mod owner; +pub mod owner_updater; +pub mod types; diff --git a/libwallet/src/api_impl/foreign.rs b/libwallet/src/api_impl/foreign.rs new file mode 100644 index 0000000..315091c --- /dev/null +++ b/libwallet/src/api_impl/foreign.rs @@ -0,0 +1,233 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Generic implementation of owner API functions +use strum::IntoEnumIterator; + +use crate::api_impl::owner::{check_ttl, post_tx}; +use crate::grin_core::core::FeeFields; +use crate::grin_keychain::Keychain; +use crate::grin_util::secp::key::SecretKey; +use crate::internal::{selection, tx, updater}; +use crate::slate_versions::SlateVersion; +use crate::{ + address, BlockFees, CbData, Error, NodeClient, Slate, SlateState, TxLogEntryType, VersionInfo, + WalletBackend, +}; + +use super::owner::tx_lock_outputs; + +const FOREIGN_API_VERSION: u16 = 2; + +/// Return the version info +pub fn check_version() -> VersionInfo { + VersionInfo { + foreign_api_version: FOREIGN_API_VERSION, + supported_slate_versions: SlateVersion::iter().collect(), + } +} + +/// Build a coinbase transaction +pub fn build_coinbase<'a, T: ?Sized, C, K>( + w: &mut T, + keychain_mask: Option<&SecretKey>, + block_fees: &BlockFees, + test_mode: bool, +) -> Result +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + updater::build_coinbase(&mut *w, keychain_mask, block_fees, test_mode) +} + +/// Receive a tx as recipient +pub fn receive_tx<'a, T: ?Sized, C, K>( + w: &mut T, + keychain_mask: Option<&SecretKey>, + slate: &Slate, + dest_acct_name: Option<&str>, + use_test_rng: bool, +) -> Result +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let mut ret_slate = slate.clone(); + check_ttl(w, &ret_slate)?; + let parent_key_id = match dest_acct_name { + Some(d) => { + let pm = w.get_acct_path(d.to_owned())?; + match pm { + Some(p) => p.path, + None => w.parent_key_id(), + } + } + None => w.parent_key_id(), + }; + // Don't do this multiple times + let tx = updater::retrieve_txs( + &mut *w, + None, + Some(ret_slate.id), + None, + Some(&parent_key_id), + use_test_rng, + )?; + for t in &tx { + if t.tx_type == TxLogEntryType::TxReceived { + return Err(Error::TransactionAlreadyReceived(ret_slate.id.to_string())); + } + } + + ret_slate.tx = Some(Slate::empty_transaction()); + + let height = w.last_confirmed_height()?; + let keychain = w.keychain(keychain_mask)?; + + let context = tx::add_output_to_slate( + &mut *w, + keychain_mask, + &mut ret_slate, + height, + &parent_key_id, + false, + use_test_rng, + )?; + + // Add our contribution to the offset + ret_slate.adjust_offset(&keychain, &context)?; + + let excess = ret_slate.calc_excess(keychain.secp())?; + + if let Some(ref mut p) = ret_slate.payment_proof { + let sig = tx::create_payment_proof_signature( + ret_slate.amount, + &excess, + p.sender_address, + address::address_from_derivation_path(&keychain, &parent_key_id, 0)?, + )?; + + p.receiver_signature = Some(sig); + } + + ret_slate.amount = 0; + ret_slate.fee_fields = FeeFields::zero(); + ret_slate.remove_other_sigdata(&keychain, &context.sec_nonce, &context.sec_key)?; + ret_slate.state = SlateState::Standard2; + + Ok(ret_slate) +} + +/// Receive a tx that this wallet has issued +pub fn finalize_tx<'a, T: ?Sized, C, K>( + w: &mut T, + keychain_mask: Option<&SecretKey>, + slate: &Slate, + post_automatically: bool, +) -> Result +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let mut sl = slate.clone(); + let mut context = w.get_private_context(keychain_mask, sl.id.as_bytes())?; + check_ttl(w, &sl)?; + if sl.state == SlateState::Invoice2 { + // Add our contribution to the offset + sl.adjust_offset(&w.keychain(keychain_mask)?, &context)?; + + let mut temp_ctx = context.clone(); + temp_ctx.sec_key = context.initial_sec_key.clone(); + temp_ctx.sec_nonce = context.initial_sec_nonce.clone(); + selection::repopulate_tx(&mut *w, keychain_mask, &mut sl, &temp_ctx, false)?; + + tx::complete_tx(&mut *w, keychain_mask, &mut sl, &context)?; + tx::update_stored_tx(&mut *w, keychain_mask, &context, &mut sl, true)?; + { + let mut batch = w.batch(keychain_mask)?; + batch.delete_private_context(sl.id.as_bytes())?; + batch.commit()?; + } + sl.state = SlateState::Invoice3; + sl.amount = 0; + } else if sl.state == SlateState::Standard2 { + let keychain = w.keychain(keychain_mask)?; + let parent_key_id = w.parent_key_id(); + + if let Some(args) = context.late_lock_args.take() { + // Transaction was late locked, select inputs+change now + // and insert into original context + + let current_height = w.w2n_client().get_chain_tip()?.0; + let mut temp_sl = + tx::new_tx_slate(&mut *w, context.amount, false, 2, false, args.ttl_blocks)?; + let temp_context = selection::build_send_tx( + w, + &keychain, + keychain_mask, + &mut temp_sl, + current_height, + args.minimum_confirmations, + args.max_outputs as usize, + args.num_change_outputs as usize, + args.selection_strategy_is_use_all, + Some(context.fee.map(|f| f.fee()).unwrap_or(0)), + parent_key_id.clone(), + false, + true, + false, + )?; + + // Add inputs and outputs to original context + context.input_ids = temp_context.input_ids; + context.output_ids = temp_context.output_ids; + + // Store the updated context + { + let mut batch = w.batch(keychain_mask)?; + batch.save_private_context(sl.id.as_bytes(), &context)?; + batch.commit()?; + } + + // Now do the actual locking + tx_lock_outputs(w, keychain_mask, &sl)?; + } + + // Add our contribution to the offset + sl.adjust_offset(&keychain, &context)?; + + selection::repopulate_tx(&mut *w, keychain_mask, &mut sl, &context, true)?; + + tx::complete_tx(&mut *w, keychain_mask, &mut sl, &context)?; + tx::verify_slate_payment_proof(&mut *w, keychain_mask, &parent_key_id, &context, &sl)?; + tx::update_stored_tx(&mut *w, keychain_mask, &context, &sl, false)?; + { + let mut batch = w.batch(keychain_mask)?; + batch.delete_private_context(sl.id.as_bytes())?; + batch.commit()?; + } + sl.state = SlateState::Standard3; + sl.amount = 0; + } else { + return Err(Error::SlateState); + } + if post_automatically { + post_tx(w.w2n_client(), sl.tx_or_err()?, true)?; + } + Ok(sl) +} diff --git a/libwallet/src/api_impl/owner.rs b/libwallet/src/api_impl/owner.rs new file mode 100644 index 0000000..017bf4e --- /dev/null +++ b/libwallet/src/api_impl/owner.rs @@ -0,0 +1,1478 @@ +// Copyright 2021 The Grin Develope; +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Generic implementation of owner API functions + +use uuid::Uuid; + +use crate::api_impl::foreign::finalize_tx as foreign_finalize; +use crate::grin_core::core::hash::Hashed; +use crate::grin_core::core::{FeeFields, Output, OutputFeatures, Transaction}; +use crate::grin_core::libtx::proof; +use crate::grin_keychain::ViewKey; +use crate::grin_util::secp::{key::SecretKey, pedersen::Commitment}; +use crate::grin_util::Mutex; +use crate::grin_util::ToHex; +use crate::util::{OnionV3Address, OnionV3AddressError}; + +use crate::api_impl::owner_updater::StatusMessage; +use crate::grin_keychain::{BlindingFactor, Identifier, Keychain, SwitchCommitmentType}; +use crate::internal::{keys, scan, selection, tx, updater}; +use crate::slate::{PaymentInfo, Slate, SlateState}; +use crate::types::{AcctPathMapping, NodeClient, TxLogEntry, WalletBackend, WalletInfo}; +use crate::Error; +use crate::{ + address, + mwixnet::{create_onion, ComSignature, Hop, MixnetReqCreationParams, SwapReq}, + wallet_lock, BuiltOutput, InitTxArgs, IssueInvoiceTxArgs, NodeHeightResult, + OutputCommitMapping, PaymentProof, RetrieveTxQueryArgs, ScannedBlockInfo, Slatepack, + SlatepackAddress, Slatepacker, SlatepackerArgs, TxLogEntryType, ViewWallet, WalletInitStatus, + WalletInst, WalletLCProvider, +}; + +use ed25519_dalek::PublicKey as DalekPublicKey; +use ed25519_dalek::SecretKey as DalekSecretKey; +use ed25519_dalek::Verifier; +use x25519_dalek::{PublicKey as xPublicKey, StaticSecret}; + +use std::convert::{TryFrom, TryInto}; +use std::sync::mpsc::Sender; +use std::sync::Arc; + +/// List of accounts +pub fn accounts<'a, T: ?Sized, C, K>(w: &mut T) -> Result, Error> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + keys::accounts(&mut *w) +} + +/// new account path +pub fn create_account_path<'a, T: ?Sized, C, K>( + w: &mut T, + keychain_mask: Option<&SecretKey>, + label: &str, +) -> Result +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + keys::new_acct_path(&mut *w, keychain_mask, label) +} + +/// set active account +pub fn set_active_account<'a, T: ?Sized, C, K>(w: &mut T, label: &str) -> Result<(), Error> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + w.set_parent_key_id_by_name(label) +} + +/// Hash of the wallet root public key +pub fn get_rewind_hash<'a, L, C, K>( + wallet_inst: Arc>>>, + keychain_mask: Option<&SecretKey>, +) -> Result +where + L: WalletLCProvider<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + wallet_lock!(wallet_inst, w); + let keychain = w.keychain(keychain_mask)?; + let root_public_key = keychain.public_root_key(); + let rewind_hash = ViewKey::rewind_hash(keychain.secp(), root_public_key).to_hex(); + Ok(rewind_hash) +} + +/// Retrieve the slatepack address for the current parent key at +/// the given index +pub fn get_slatepack_address<'a, L, C, K>( + wallet_inst: Arc>>>, + keychain_mask: Option<&SecretKey>, + index: u32, +) -> Result +where + L: WalletLCProvider<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + wallet_lock!(wallet_inst, w); + let parent_key_id = w.parent_key_id(); + let k = w.keychain(keychain_mask)?; + let sec_addr_key = address::address_from_derivation_path(&k, &parent_key_id, index)?; + SlatepackAddress::try_from(&sec_addr_key) +} + +/// Retrieve the decryption key for the current parent key +/// the given index +pub fn get_slatepack_secret_key<'a, L, C, K>( + wallet_inst: Arc>>>, + keychain_mask: Option<&SecretKey>, + index: u32, +) -> Result +where + L: WalletLCProvider<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + wallet_lock!(wallet_inst, w); + let parent_key_id = w.parent_key_id(); + let k = w.keychain(keychain_mask)?; + let sec_addr_key = address::address_from_derivation_path(&k, &parent_key_id, index)?; + let d_skey = match DalekSecretKey::from_bytes(&sec_addr_key.0) { + Ok(k) => k, + Err(e) => { + return Err(OnionV3AddressError::InvalidPrivateKey(format!( + "Unable to create secret key: {}", + e + )) + .into()); + } + }; + Ok(d_skey) +} + +/// Create a slatepack message from the given slate +pub fn create_slatepack_message<'a, L, C, K>( + wallet_inst: Arc>>>, + keychain_mask: Option<&SecretKey>, + slate: &Slate, + sender_index: Option, + recipients: Vec, +) -> Result +where + L: WalletLCProvider<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let sender = match sender_index { + Some(i) => Some(get_slatepack_address(wallet_inst, keychain_mask, i)?), + None => None, + }; + let packer = Slatepacker::new(SlatepackerArgs { + sender, + recipients, + dec_key: None, + }); + let slatepack = packer.create_slatepack(slate)?; + packer.armor_slatepack(&slatepack) +} + +/// Unpack a slate from the given slatepack message, +/// optionally decrypting +pub fn slate_from_slatepack_message<'a, L, C, K>( + wallet_inst: Arc>>>, + keychain_mask: Option<&SecretKey>, + slatepack: String, + secret_indices: Vec, +) -> Result +where + L: WalletLCProvider<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + if secret_indices.is_empty() { + let packer = Slatepacker::new(SlatepackerArgs { + sender: None, + recipients: vec![], + dec_key: None, + }); + let slatepack = packer.deser_slatepack(slatepack.as_bytes(), true)?; + return packer.get_slate(&slatepack); + } else { + for index in secret_indices { + let dec_key = Some(get_slatepack_secret_key( + wallet_inst.clone(), + keychain_mask, + index, + )?); + let packer = Slatepacker::new(SlatepackerArgs { + sender: None, + recipients: vec![], + dec_key: (&dec_key).as_ref(), + }); + let res = packer.deser_slatepack(slatepack.as_bytes(), true); + let slatepack = match res { + Ok(sp) => sp, + Err(_) => { + continue; + } + }; + return packer.get_slate(&slatepack); + } + return Err(Error::SlatepackDecryption( + "Could not decrypt slatepack with any provided index on the address derivation path" + .to_owned(), + ) + .into()); + } +} + +/// Decode a slatepack message, to allow viewing +/// Will decrypt if possible, otherwise will return +/// undecrypted slatepack +pub fn decode_slatepack_message<'a, L, C, K>( + wallet_inst: Arc>>>, + keychain_mask: Option<&SecretKey>, + slatepack: String, + secret_indices: Vec, +) -> Result +where + L: WalletLCProvider<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let packer = Slatepacker::new(SlatepackerArgs { + sender: None, + recipients: vec![], + dec_key: None, + }); + if secret_indices.is_empty() { + packer.deser_slatepack(slatepack.as_bytes(), false) + } else { + for index in secret_indices { + let dec_key = Some(get_slatepack_secret_key( + wallet_inst.clone(), + keychain_mask, + index, + )?); + let packer = Slatepacker::new(SlatepackerArgs { + sender: None, + recipients: vec![], + dec_key: (&dec_key).as_ref(), + }); + let res = packer.deser_slatepack(slatepack.as_bytes(), true); + let slatepack = match res { + Ok(sp) => sp, + Err(_) => { + continue; + } + }; + return Ok(slatepack); + } + packer.deser_slatepack(slatepack.as_bytes(), false) + } +} + +/// retrieve outputs +pub fn retrieve_outputs<'a, L, C, K>( + wallet_inst: Arc>>>, + keychain_mask: Option<&SecretKey>, + status_send_channel: &Option>, + include_spent: bool, + refresh_from_node: bool, + tx_id: Option, +) -> Result<(bool, Vec), Error> +where + L: WalletLCProvider<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let validated = if refresh_from_node { + update_wallet_state( + wallet_inst.clone(), + keychain_mask, + status_send_channel, + false, + )? + } else { + false + }; + + wallet_lock!(wallet_inst, w); + let parent_key_id = w.parent_key_id(); + + Ok(( + validated, + updater::retrieve_outputs( + &mut **w, + keychain_mask, + include_spent, + tx_id, + Some(&parent_key_id), + )?, + )) +} + +/// Retrieve txs +pub fn retrieve_txs<'a, L, C, K>( + wallet_inst: Arc>>>, + keychain_mask: Option<&SecretKey>, + status_send_channel: &Option>, + refresh_from_node: bool, + tx_id: Option, + tx_slate_id: Option, + query_args: Option, +) -> Result<(bool, Vec), Error> +where + L: WalletLCProvider<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let validated = if refresh_from_node { + update_wallet_state( + wallet_inst.clone(), + keychain_mask, + status_send_channel, + false, + )? + } else { + false + }; + + wallet_lock!(wallet_inst, w); + let parent_key_id = w.parent_key_id(); + let txs = updater::retrieve_txs( + &mut **w, + tx_id, + tx_slate_id, + query_args, + Some(&parent_key_id), + false, + )?; + + Ok((validated, txs)) +} + +/// Retrieve summary info +pub fn retrieve_summary_info<'a, L, C, K>( + wallet_inst: Arc>>>, + keychain_mask: Option<&SecretKey>, + status_send_channel: &Option>, + refresh_from_node: bool, + minimum_confirmations: u64, +) -> Result<(bool, WalletInfo), Error> +where + L: WalletLCProvider<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let validated = if refresh_from_node { + update_wallet_state( + wallet_inst.clone(), + keychain_mask, + status_send_channel, + false, + )? + } else { + false + }; + + wallet_lock!(wallet_inst, w); + let parent_key_id = w.parent_key_id(); + let wallet_info = updater::retrieve_info(&mut **w, &parent_key_id, minimum_confirmations)?; + Ok((validated, wallet_info)) +} + +/// Retrieve payment proof +pub fn retrieve_payment_proof<'a, L, C, K>( + wallet_inst: Arc>>>, + keychain_mask: Option<&SecretKey>, + status_send_channel: &Option>, + refresh_from_node: bool, + tx_id: Option, + tx_slate_id: Option, +) -> Result +where + L: WalletLCProvider<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + if tx_id.is_none() && tx_slate_id.is_none() { + return Err(Error::PaymentProofRetrieval( + "Transaction ID or Slate UUID must be specified".to_owned(), + )); + } + if refresh_from_node { + update_wallet_state( + wallet_inst.clone(), + keychain_mask, + status_send_channel, + false, + )? + } else { + false + }; + let txs = retrieve_txs( + wallet_inst.clone(), + keychain_mask, + status_send_channel, + refresh_from_node, + tx_id, + tx_slate_id, + None, + )?; + if txs.1.len() != 1 { + return Err(Error::PaymentProofRetrieval( + "Transaction doesn't exist".to_owned(), + )); + } + // Pull out all needed fields, returning an error if they're not present + let tx = txs.1[0].clone(); + let proof = match tx.payment_proof { + Some(p) => p, + None => { + return Err(Error::PaymentProofRetrieval( + "Transaction does not contain a payment proof".to_owned(), + )); + } + }; + let amount = if tx.amount_credited >= tx.amount_debited { + tx.amount_credited - tx.amount_debited + } else { + let fee = match tx.fee { + Some(f) => f.fee(), // apply fee mask past HF4 + None => 0, + }; + tx.amount_debited - tx.amount_credited - fee + }; + let excess = match tx.kernel_excess { + Some(e) => e, + None => { + return Err(Error::PaymentProofRetrieval( + "Transaction does not contain kernel excess".to_owned(), + )); + } + }; + let r_sig = match proof.receiver_signature { + Some(e) => e, + None => { + return Err(Error::PaymentProofRetrieval( + "Proof does not contain receiver signature ".to_owned(), + )); + } + }; + let s_sig = match proof.sender_signature { + Some(e) => e, + None => { + return Err(Error::PaymentProofRetrieval( + "Proof does not contain sender signature ".to_owned(), + )); + } + }; + Ok(PaymentProof { + amount: amount, + excess: excess, + recipient_address: SlatepackAddress::new(&proof.receiver_address), + recipient_sig: r_sig, + sender_address: SlatepackAddress::new(&proof.sender_address), + sender_sig: s_sig, + }) +} + +/// Initiate tx as sender +pub fn init_send_tx<'a, T: ?Sized, C, K>( + w: &mut T, + keychain_mask: Option<&SecretKey>, + args: InitTxArgs, + use_test_rng: bool, +) -> Result +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let parent_key_id = match &args.src_acct_name { + Some(d) => { + let pm = w.get_acct_path(d.clone())?; + match pm { + Some(p) => p.path, + None => w.parent_key_id(), + } + } + None => w.parent_key_id(), + }; + + let mut slate = tx::new_tx_slate( + &mut *w, + args.amount, + false, + 2, + use_test_rng, + args.ttl_blocks, + )?; + + if let Some(v) = args.target_slate_version { + slate.version_info.version = v; + }; + + // if we just want to estimate, don't save a context, just send the results + // back + if let Some(true) = args.estimate_only { + let (total, fee) = tx::estimate_send_tx( + &mut *w, + keychain_mask, + args.amount, + args.amount_includes_fee.unwrap_or(false), + args.minimum_confirmations, + args.max_outputs as usize, + args.num_change_outputs as usize, + args.selection_strategy_is_use_all, + &parent_key_id, + )?; + slate.amount = total; + slate.fee_fields = fee.try_into().unwrap(); + return Ok(slate); + } + + let height = w.w2n_client().get_chain_tip()?.0; + let mut context = if args.late_lock.unwrap_or(false) { + tx::create_late_lock_context( + &mut *w, + keychain_mask, + &mut slate, + height, + &args, + &parent_key_id, + use_test_rng, + )? + } else { + tx::add_inputs_to_slate( + &mut *w, + keychain_mask, + &mut slate, + height, + args.minimum_confirmations, + args.max_outputs as usize, + args.num_change_outputs as usize, + args.selection_strategy_is_use_all, + &parent_key_id, + true, + use_test_rng, + args.amount_includes_fee.unwrap_or(false), + )? + }; + + // Payment Proof, add addresses to slate and save address + // TODO: Note we only use single derivation path for now, + // probably want to allow sender to specify which one + let deriv_path = 0u32; + + if let Some(a) = args.payment_proof_recipient_address { + let k = w.keychain(keychain_mask)?; + + let sec_addr_key = address::address_from_derivation_path(&k, &parent_key_id, deriv_path)?; + let sender_address = OnionV3Address::from_private(&sec_addr_key.0)?; + + slate.payment_proof = Some(PaymentInfo { + sender_address: sender_address.to_ed25519()?, + receiver_address: a.pub_key, + receiver_signature: None, + }); + + context.payment_proof_derivation_index = Some(deriv_path); + } + + // Save the aggsig context in our DB for when we + // recieve the transaction back + { + let mut batch = w.batch(keychain_mask)?; + batch.save_private_context(slate.id.as_bytes(), &context)?; + batch.commit()?; + } + + slate.compact()?; + + Ok(slate) +} + +/// Initiate a transaction as the recipient (invoicing) +pub fn issue_invoice_tx<'a, T: ?Sized, C, K>( + w: &mut T, + keychain_mask: Option<&SecretKey>, + args: IssueInvoiceTxArgs, + use_test_rng: bool, +) -> Result +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let parent_key_id = match args.dest_acct_name { + Some(d) => { + let pm = w.get_acct_path(d)?; + match pm { + Some(p) => p.path, + None => w.parent_key_id(), + } + } + None => w.parent_key_id(), + }; + + let mut slate = tx::new_tx_slate(&mut *w, args.amount, true, 2, use_test_rng, None)?; + let height = w.w2n_client().get_chain_tip()?.0; + let context = tx::add_output_to_slate( + &mut *w, + keychain_mask, + &mut slate, + height, + &parent_key_id, + true, + use_test_rng, + )?; + + if let Some(v) = args.target_slate_version { + slate.version_info.version = v; + }; + + // Save the aggsig context in our DB for when we + // recieve the transaction back + { + let mut batch = w.batch(keychain_mask)?; + batch.save_private_context(slate.id.as_bytes(), &context)?; + batch.commit()?; + } + + slate.compact()?; + + Ok(slate) +} + +/// Receive an invoice tx, essentially adding inputs to whatever +/// output was specified +pub fn process_invoice_tx<'a, T: ?Sized, C, K>( + w: &mut T, + keychain_mask: Option<&SecretKey>, + slate: &Slate, + args: InitTxArgs, + use_test_rng: bool, +) -> Result +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let mut ret_slate = slate.clone(); + check_ttl(w, &ret_slate)?; + let parent_key_id = match args.src_acct_name { + Some(d) => { + let pm = w.get_acct_path(d)?; + match pm { + Some(p) => p.path, + None => w.parent_key_id(), + } + } + None => w.parent_key_id(), + }; + // Don't do this multiple times + let tx = updater::retrieve_txs( + &mut *w, + None, + Some(ret_slate.id), + None, + Some(&parent_key_id), + use_test_rng, + )?; + for t in &tx { + if t.tx_type == TxLogEntryType::TxSent { + return Err(Error::TransactionAlreadyReceived(ret_slate.id.to_string())); + } + if t.tx_type == TxLogEntryType::TxSentCancelled { + return Err(Error::TransactionWasCancelled(ret_slate.id.to_string())); + } + } + + let height = w.w2n_client().get_chain_tip()?.0; + + // update ttl if desired + if let Some(b) = args.ttl_blocks { + ret_slate.ttl_cutoff_height = height + b; + } + + // if this is compact mode, we need to create the transaction now + ret_slate.tx = Some(Slate::empty_transaction()); + + // if self sending, make sure to store 'initiator' keys + let context_res = w.get_private_context(keychain_mask, slate.id.as_bytes()); + + let mut context = tx::add_inputs_to_slate( + &mut *w, + keychain_mask, + &mut ret_slate, + height, + args.minimum_confirmations, + args.max_outputs as usize, + args.num_change_outputs as usize, + args.selection_strategy_is_use_all, + &parent_key_id, + false, + use_test_rng, + false, + )?; + + let keychain = w.keychain(keychain_mask)?; + + // Add our contribution to the offset + if context_res.is_ok() { + // Self sending: don't correct for inputs and outputs + // here, as we will do it during finalization. + let mut tmp_context = context.clone(); + tmp_context.input_ids.clear(); + tmp_context.output_ids.clear(); + ret_slate.adjust_offset(&keychain, &tmp_context)?; + } else { + ret_slate.adjust_offset(&keychain, &context)?; + } + + // needs to be stored as we're removing sig data for return trip. this needs to be present + // when locking transaction context and updating tx log with excess later + context.calculated_excess = Some(ret_slate.calc_excess(keychain.secp())?); + + // if self-sending, merge contexts + if let Ok(c) = context_res { + context.initial_sec_key = c.initial_sec_key; + context.initial_sec_nonce = c.initial_sec_nonce; + context.fee = c.fee; + context.amount = c.amount; + for o in c.output_ids.iter() { + context.output_ids.push(o.clone()); + } + for i in c.input_ids.iter() { + context.input_ids.push(i.clone()); + } + } + + selection::repopulate_tx(&mut *w, keychain_mask, &mut ret_slate, &context, false)?; + + // Save the aggsig context in our DB for when we + // recieve the transaction back + { + let mut batch = w.batch(keychain_mask)?; + batch.save_private_context(slate.id.as_bytes(), &context)?; + batch.commit()?; + } + + // Can remove amount as well as other sig data now + ret_slate.amount = 0; + ret_slate.remove_other_sigdata(&keychain, &context.sec_nonce, &context.sec_key)?; + + ret_slate.state = SlateState::Invoice2; + Ok(ret_slate) +} + +/// Lock sender outputs +pub fn tx_lock_outputs<'a, T: ?Sized, C, K>( + w: &mut T, + keychain_mask: Option<&SecretKey>, + slate: &Slate, +) -> Result<(), Error> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let context = w.get_private_context(keychain_mask, slate.id.as_bytes())?; + let mut excess_override = None; + + let mut sl = slate.clone(); + + if sl.tx == None { + sl.tx = Some(Slate::empty_transaction()); + selection::repopulate_tx(&mut *w, keychain_mask, &mut sl, &context, true)?; + } + + if slate.participant_data.len() == 1 { + // purely for invoice workflow, payer needs the excess back temporarily for storage + excess_override = context.calculated_excess; + } + + let height = w.w2n_client().get_chain_tip()?.0; + selection::lock_tx_context( + &mut *w, + keychain_mask, + &sl, + height, + &context, + excess_override, + ) +} + +/// Finalize slate +pub fn finalize_tx<'a, T: ?Sized, C, K>( + w: &mut T, + keychain_mask: Option<&SecretKey>, + slate: &Slate, +) -> Result +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + foreign_finalize(w, keychain_mask, slate, false) +} + +/// cancel tx +pub fn cancel_tx<'a, L, C, K>( + wallet_inst: Arc>>>, + keychain_mask: Option<&SecretKey>, + status_send_channel: &Option>, + tx_id: Option, + tx_slate_id: Option, +) -> Result<(), Error> +where + L: WalletLCProvider<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + if !update_wallet_state( + wallet_inst.clone(), + keychain_mask, + status_send_channel, + false, + )? { + return Err(Error::TransactionCancellationError( + "Can't contact running Grin node. Not Cancelling.", + )); + } + wallet_lock!(wallet_inst, w); + let parent_key_id = w.parent_key_id(); + tx::cancel_tx(&mut **w, keychain_mask, &parent_key_id, tx_id, tx_slate_id) +} + +/// get stored tx +/// crashes if stored tx has total fees exceeding 2^40 nanogrin +pub fn get_stored_tx<'a, T: ?Sized, C, K>( + w: &T, + tx_id: Option, + slate_id: Option<&Uuid>, +) -> Result, Error> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let mut uuid = None; + if let Some(i) = tx_id { + let tx = w.tx_log_iter().find(|t| t.id == i); + if let Some(t) = tx { + uuid = t.tx_slate_id; + } + } + if uuid.is_none() { + if let Some(sid) = slate_id { + uuid = Some(sid.to_owned()); + } + } + let id = match uuid { + Some(u) => u, + None => { + return Err(Error::StoredTx( + "Both the provided Transaction Id and Slate UUID are invalid.".to_owned(), + )); + } + }; + let tx_res = w.get_stored_tx(&format!("{}", id))?; + match tx_res { + Some(tx) => { + let mut slate = Slate::blank(2, false); + slate.tx = Some(tx.clone()); + slate.fee_fields = tx.aggregate_fee_fields().unwrap(); // apply fee mask past HF4 + slate.id = id; + slate.offset = tx.offset; + slate.state = SlateState::Standard3; + Ok(Some(slate)) + } + None => Ok(None), + } +} + +/// Posts a transaction to the chain +/// take a client impl instead of wallet so as not to have to lock the wallet +pub fn post_tx<'a, C>(client: &C, tx: &Transaction, fluff: bool) -> Result<(), Error> +where + C: NodeClient + 'a, +{ + let res = client.post_tx(tx, fluff); + if let Err(e) = res { + error!("api: post_tx: failed with error: {}", e); + Err(e) + } else { + debug!( + "api: post_tx: successfully posted tx: {}, fluff? {}", + tx.hash(), + fluff + ); + Ok(()) + } +} + +/// Scan outputs with the rewind hash of a third-party wallet. +/// Help to retrieve outputs information that belongs it +pub fn scan_rewind_hash<'a, L, C, K>( + wallet_inst: Arc>>>, + rewind_hash: String, + start_height: Option, + status_send_channel: &Option>, +) -> Result +where + L: WalletLCProvider<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let is_hex = rewind_hash.chars().all(|c| c.is_ascii_hexdigit()); + let rewind_hash = rewind_hash.to_lowercase(); + if !(is_hex && rewind_hash.len() == 64) { + let msg = format!("Invalid Rewind Hash"); + return Err(Error::RewindHash(msg)); + } + + let tip = { + wallet_lock!(wallet_inst, w); + w.w2n_client().get_chain_tip()? + }; + + let start_height = match start_height { + Some(h) => h, + None => 1, + }; + + let info = scan::scan_rewind_hash( + wallet_inst, + rewind_hash, + start_height, + tip.0, + status_send_channel, + )?; + Ok(info) +} + +/// check repair +/// Accepts a wallet inst instead of a raw wallet so it can +/// lock as little as possible +pub fn scan<'a, L, C, K>( + wallet_inst: Arc>>>, + keychain_mask: Option<&SecretKey>, + start_height: Option, + delete_unconfirmed: bool, + status_send_channel: &Option>, +) -> Result<(), Error> +where + L: WalletLCProvider<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + update_outputs(wallet_inst.clone(), keychain_mask, true)?; + let tip = { + wallet_lock!(wallet_inst, w); + w.w2n_client().get_chain_tip()? + }; + + let start_height = match start_height { + Some(h) => h, + None => 1, + }; + + let mut info = scan::scan( + wallet_inst.clone(), + keychain_mask, + delete_unconfirmed, + start_height, + tip.0, + status_send_channel, + )?; + info.hash = tip.1; + + wallet_lock!(wallet_inst, w); + let mut batch = w.batch(keychain_mask)?; + batch.save_last_scanned_block(info)?; + batch.commit()?; + + Ok(()) +} + +/// node height +pub fn node_height<'a, L, C, K>( + wallet_inst: Arc>>>, + keychain_mask: Option<&SecretKey>, +) -> Result +where + L: WalletLCProvider<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let res = { + wallet_lock!(wallet_inst, w); + w.w2n_client().get_chain_tip() + }; + match res { + Ok(r) => Ok(NodeHeightResult { + height: r.0, + header_hash: r.1, + updated_from_node: true, + }), + Err(_) => { + let outputs = retrieve_outputs(wallet_inst, keychain_mask, &None, true, false, None)?; + let height = match outputs.1.iter().map(|m| m.output.height).max() { + Some(height) => height, + None => 0, + }; + Ok(NodeHeightResult { + height, + header_hash: "".to_owned(), + updated_from_node: false, + }) + } + } +} + +/// Experimental, wrap the entire definition of how a wallet's state is updated +pub fn update_wallet_state<'a, L, C, K>( + wallet_inst: Arc>>>, + keychain_mask: Option<&SecretKey>, + status_send_channel: &Option>, + update_all: bool, +) -> Result +where + L: WalletLCProvider<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let parent_key_id = { + wallet_lock!(wallet_inst, w); + w.parent_key_id() + }; + let client = { + wallet_lock!(wallet_inst, w); + w.w2n_client().clone() + }; + + // Step 1: Update outputs and transactions purely based on UTXO state + if let Some(ref s) = status_send_channel { + let _ = s.send(StatusMessage::UpdatingOutputs( + "Updating outputs from node".to_owned(), + )); + } + let mut result = update_outputs(wallet_inst.clone(), keychain_mask, update_all)?; + + if !result { + if let Some(ref s) = status_send_channel { + let _ = s.send(StatusMessage::UpdateWarning( + "Updater Thread unable to contact node".to_owned(), + )); + } + return Ok(result); + } + + if let Some(ref s) = status_send_channel { + let _ = s.send(StatusMessage::UpdatingTransactions( + "Updating transactions".to_owned(), + )); + } + + // Step 2: Update outstanding transactions with no change outputs by kernel + let mut txs = { + wallet_lock!(wallet_inst, w); + updater::retrieve_txs(&mut **w, None, None, None, Some(&parent_key_id), true)? + }; + result = update_txs_via_kernel(wallet_inst.clone(), keychain_mask, &mut txs)?; + if !result { + if let Some(ref s) = status_send_channel { + let _ = s.send(StatusMessage::UpdateWarning( + "Updater Thread unable to contact node".to_owned(), + )); + } + return Ok(result); + } + + // Step 3: Scan back a bit on the chain + let res = client.get_chain_tip(); + // if we can't get the tip, don't continue + let tip = match res { + Ok(t) => t, + Err(_) => { + if let Some(ref s) = status_send_channel { + let _ = s.send(StatusMessage::UpdateWarning( + "Updater Thread unable to contact node".to_owned(), + )); + } + return Ok(false); + } + }; + + // Check if this is a restored wallet that needs a full scan + let last_scanned_block = { + wallet_lock!(wallet_inst, w); + match w.init_status()? { + WalletInitStatus::InitNeedsScanning => ScannedBlockInfo { + height: 0, + hash: "".to_owned(), + start_pmmr_index: 0, + last_pmmr_index: 0, + }, + WalletInitStatus::InitNoScanning => ScannedBlockInfo { + height: tip.clone().0, + hash: tip.clone().1, + start_pmmr_index: 0, + last_pmmr_index: 0, + }, + WalletInitStatus::InitComplete => w.last_scanned_block()?, + } + }; + + let start_index = last_scanned_block.height.saturating_sub(100); + + if last_scanned_block.height == 0 { + let msg = "This wallet has not been scanned against the current chain. Beginning full scan... (this first scan may take a while, but subsequent scans will be much quicker)".to_string(); + if let Some(ref s) = status_send_channel { + let _ = s.send(StatusMessage::FullScanWarn(msg)); + } + } + + let mut info = scan::scan( + wallet_inst.clone(), + keychain_mask, + false, + start_index, + tip.0, + status_send_channel, + )?; + + info.hash = tip.1; + + { + wallet_lock!(wallet_inst, w); + let mut batch = w.batch(keychain_mask)?; + batch.save_last_scanned_block(info)?; + // init considered complete after first successful update + batch.save_init_status(WalletInitStatus::InitComplete)?; + batch.commit()?; + } + + // Step 5: Cancel any transactions with an expired TTL + for tx in txs { + if let Some(e) = tx.ttl_cutoff_height { + if tip.0 >= e { + wallet_lock!(wallet_inst, w); + let parent_key_id = w.parent_key_id(); + tx::cancel_tx(&mut **w, keychain_mask, &parent_key_id, Some(tx.id), None)?; + } + } + } + + Ok(result) +} + +/// Check TTL +pub fn check_ttl<'a, T: ?Sized, C, K>(w: &mut T, slate: &Slate) -> Result<(), Error> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + // Refuse if TTL is expired + let last_confirmed_height = w.last_confirmed_height()?; + if slate.ttl_cutoff_height != 0 { + if last_confirmed_height >= slate.ttl_cutoff_height { + return Err(Error::TransactionExpired); + } + } + Ok(()) +} + +/// Verify/validate arbitrary payment proof +/// Returns (whether this wallet is the sender, whether this wallet is the recipient) +pub fn verify_payment_proof<'a, L, C, K>( + wallet_inst: Arc>>>, + keychain_mask: Option<&SecretKey>, + proof: &PaymentProof, +) -> Result<(bool, bool), Error> +where + L: WalletLCProvider<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let sender_pubkey = proof.sender_address.pub_key; + let msg = tx::payment_proof_message(proof.amount, &proof.excess, sender_pubkey)?; + + let (mut client, parent_key_id, keychain) = { + wallet_lock!(wallet_inst, w); + ( + w.w2n_client().clone(), + w.parent_key_id(), + w.keychain(keychain_mask)?, + ) + }; + + // Check kernel exists + match client.get_kernel(&proof.excess, None, None) { + Err(e) => { + return Err(Error::PaymentProof(format!( + "Error retrieving kernel from chain: {}", + e + ))); + } + Ok(None) => { + return Err(Error::PaymentProof(format!( + "Transaction kernel with excess {:?} not found on chain", + proof.excess + ))); + } + Ok(Some(_)) => {} + }; + + // Check Sigs + let recipient_pubkey = proof.recipient_address.pub_key; + if recipient_pubkey.verify(&msg, &proof.recipient_sig).is_err() { + return Err(Error::PaymentProof( + "Invalid recipient signature".to_owned(), + )); + }; + + let sender_pubkey = proof.sender_address.pub_key; + if sender_pubkey.verify(&msg, &proof.sender_sig).is_err() { + return Err(Error::PaymentProof("Invalid sender signature".to_owned())); + }; + + // for now, simple test as to whether one of the addresses belongs to this wallet + let sec_key = address::address_from_derivation_path(&keychain, &parent_key_id, 0)?; + let d_skey = match DalekSecretKey::from_bytes(&sec_key.0) { + Ok(k) => k, + Err(e) => { + return Err(Error::ED25519Key(format!("{}", e))); + } + }; + let my_address_pubkey: DalekPublicKey = (&d_skey).into(); + + let sender_mine = my_address_pubkey == sender_pubkey; + let recipient_mine = my_address_pubkey == recipient_pubkey; + + Ok((sender_mine, recipient_mine)) +} + +/// Attempt to update outputs in wallet, return whether it was successful +fn update_outputs<'a, L, C, K>( + wallet_inst: Arc>>>, + keychain_mask: Option<&SecretKey>, + update_all: bool, +) -> Result +where + L: WalletLCProvider<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + wallet_lock!(wallet_inst, w); + let parent_key_id = w.parent_key_id(); + match updater::refresh_outputs(&mut **w, keychain_mask, &parent_key_id, update_all) { + Ok(_) => Ok(true), + Err(e) => { + if let Error::InvalidKeychainMask = e { + return Err(e); + } + Ok(false) + } + } +} + +/// Update transactions that need to be validated via kernel lookup +fn update_txs_via_kernel<'a, L, C, K>( + wallet_inst: Arc>>>, + keychain_mask: Option<&SecretKey>, + txs: &mut Vec, +) -> Result +where + L: WalletLCProvider<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let parent_key_id = { + wallet_lock!(wallet_inst, w); + w.parent_key_id() + }; + + let mut client = { + wallet_lock!(wallet_inst, w); + w.w2n_client().clone() + }; + + let height = match client.get_chain_tip() { + Ok(h) => h.0, + Err(_) => return Ok(false), + }; + + for tx in txs.iter_mut() { + if tx.confirmed { + continue; + } + if tx.amount_debited != 0 && tx.amount_credited != 0 { + continue; + } + if let Some(e) = tx.kernel_excess { + let res = client.get_kernel(&e, tx.kernel_lookup_min_height, Some(height)); + let kernel = match res { + Ok(k) => k, + Err(_) => return Ok(false), + }; + if let Some(k) = kernel { + debug!("Kernel Retrieved: {:?}", k); + wallet_lock!(wallet_inst, w); + let mut batch = w.batch(keychain_mask)?; + tx.confirmed = true; + tx.update_confirmation_ts(); + batch.save_tx_log_entry(tx.clone(), &parent_key_id)?; + batch.commit()?; + } + } else { + warn!("Attempted to update via kernel excess for transaction {:?}, but kernel excess was not stored", tx.tx_slate_id); + } + } + Ok(true) +} + +/// Builds an output for the wallet's next available key +pub fn build_output<'a, T: ?Sized, C, K>( + w: &mut T, + keychain_mask: Option<&SecretKey>, + features: OutputFeatures, + amount: u64, +) -> Result +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let k = w.keychain(keychain_mask)?; + + let key_id = keys::next_available_key(&mut *w, keychain_mask)?; + + let blind = k.derive_key(amount, &key_id, SwitchCommitmentType::Regular)?; + let commit = k.secp().commit(amount, blind.clone())?; + + let proof_builder = proof::ProofBuilder::new(&k); + let proof = proof::create( + &k, + &proof_builder, + amount, + &key_id, + SwitchCommitmentType::Regular, + commit, + None, + )?; + + let output = Output::new(features, commit, proof); + + Ok(BuiltOutput { + blind: BlindingFactor::from_secret_key(blind), + key_id: key_id, + output: output, + }) +} + +/// Create MXMixnet request +pub fn create_mwixnet_req<'a, T: ?Sized, C, K>( + w: &mut T, + keychain_mask: Option<&SecretKey>, + params: &MixnetReqCreationParams, + commitment: &Commitment, + lock_output: bool, + use_test_rng: bool, +) -> Result +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let parent_key_id = w.parent_key_id(); + let keychain = w.keychain(keychain_mask)?; + let outputs = updater::retrieve_outputs(w, keychain_mask, false, None, Some(&parent_key_id))?; + + let mut output = None; + for o in &outputs { + if o.commit == *commitment { + output = Some(o.output.clone()); + break; + } + } + + if output.is_none() { + return Err(Error::GenericError(String::from("output not found"))); + } + + let amount = output.clone().unwrap().value; + let input_blind = keychain.derive_key( + amount, + &output.clone().unwrap().key_id, + SwitchCommitmentType::Regular, + )?; + + let mut server_pubkeys = vec![]; + for i in 0..params.server_keys.len() { + server_pubkeys.push(xPublicKey::from(&StaticSecret::from( + params.server_keys[i].0, + ))); + } + + let fee = grin_core::libtx::tx_fee(1, 1, 1); + let new_amount = amount - (fee * server_pubkeys.len() as u64); + let new_output = build_output(w, keychain_mask, OutputFeatures::Plain, new_amount)?; + let secp = keychain.secp(); + + let mut blind_sum = new_output + .blind + .split(&BlindingFactor::from_secret_key(input_blind.clone()), &secp)?; + + let hops = server_pubkeys + .iter() + .enumerate() + .map(|(i, &p)| { + if (i + 1) == server_pubkeys.len() { + Hop { + server_pubkey: p.clone(), + excess: blind_sum.secret_key(&secp).unwrap(), + fee: FeeFields::from(fee as u32), + rangeproof: Some(new_output.output.proof.clone()), + } + } else { + let hop_excess; + if use_test_rng { + hop_excess = BlindingFactor::zero(); + } else { + hop_excess = BlindingFactor::rand(&secp); + } + blind_sum = blind_sum.split(&hop_excess, &secp).unwrap(); + Hop { + server_pubkey: p.clone(), + excess: hop_excess.secret_key(&secp).unwrap(), + fee: FeeFields::from(fee as u32), + rangeproof: None, + } + } + }) + .collect(); + + let onion = create_onion(&commitment, &hops, use_test_rng).unwrap(); + let comsig = ComSignature::sign( + amount, + &input_blind, + &onion.serialize().unwrap(), + use_test_rng, + ) + .unwrap(); + + // Lock output if requested + if lock_output { + let mut batch = w.batch(keychain_mask)?; + let mut update_output = batch.get(&output.as_ref().unwrap().key_id, &None)?; + update_output.lock(); + batch.lock_output(&mut update_output)?; + batch.commit()?; + } + + Ok(SwapReq { comsig, onion }) +} diff --git a/libwallet/src/api_impl/owner_updater.rs b/libwallet/src/api_impl/owner_updater.rs new file mode 100644 index 0000000..d46cdaa --- /dev/null +++ b/libwallet/src/api_impl/owner_updater.rs @@ -0,0 +1,146 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! A threaded persistent Updater that can be controlled by a grin wallet +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::mpsc::{Receiver, Sender}; +use std::sync::Arc; +use std::thread; +use std::time::Duration; + +use crate::grin_keychain::Keychain; +use crate::grin_util::secp::key::SecretKey; +use crate::grin_util::Mutex; + +use crate::api_impl::owner; +use crate::types::NodeClient; +use crate::Error; +use crate::{WalletInst, WalletLCProvider}; + +const MESSAGE_QUEUE_MAX_LEN: usize = 10_000; + +/// Update status messages which can be returned to listening clients +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum StatusMessage { + /// Wallet is performing a regular update, matching the UTXO set against + /// current wallet outputs + UpdatingOutputs(String), + /// Wallet is updating transactions, potentially retrieving transactions + /// by kernel if needed + UpdatingTransactions(String), + /// Warning that the wallet is about to perform a full UTXO scan + FullScanWarn(String), + /// Status and percentage complete messages returned during the + /// scanning process + Scanning(String, u8), + /// UTXO scanning is complete + ScanningComplete(String), + /// Warning of issues that may have occured during an update + UpdateWarning(String), +} + +/// Helper function that starts a simple log thread for updater messages +pub fn start_updater_log_thread( + rx: Receiver, + queue: Arc>>, +) -> Result<(), Error> { + let _ = thread::Builder::new() + .name("wallet-updater-status".to_string()) + .spawn(move || { + while let Ok(m) = rx.recv() { + // save to our message queue to be read by other consumers + { + let mut q = queue.lock(); + q.insert(0, m.clone()); + while q.len() > MESSAGE_QUEUE_MAX_LEN { + q.pop(); + } + } + match m { + StatusMessage::UpdatingOutputs(s) => debug!("{}", s), + StatusMessage::UpdatingTransactions(s) => debug!("{}", s), + StatusMessage::FullScanWarn(s) => warn!("{}", s), + StatusMessage::Scanning(s, m) => { + debug!("{}", s); + warn!("Scanning - {}% complete", m); + } + StatusMessage::ScanningComplete(s) => warn!("{}", s), + StatusMessage::UpdateWarning(s) => warn!("{}", s), + } + } + })?; + + Ok(()) +} + +/// Handles and launches a background update thread +pub struct Updater<'a, L, C, K> +where + L: WalletLCProvider<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + wallet_inst: Arc>>>, + is_running: Arc, +} + +impl<'a, L, C, K> Updater<'a, L, C, K> +where + L: WalletLCProvider<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + /// create a new updater + pub fn new( + wallet_inst: Arc>>>, + is_running: Arc, + ) -> Self { + is_running.store(false, Ordering::Relaxed); + Updater { + wallet_inst, + is_running, + } + } + + /// Start the updater at the given frequency + pub fn run( + &self, + frequency: Duration, + keychain_mask: Option, + status_send_channel: &Option>, + ) -> Result<(), Error> { + self.is_running.store(true, Ordering::Relaxed); + loop { + let wallet_opened = { + let mut w_lock = self.wallet_inst.lock(); + let w_provider = w_lock.lc_provider()?; + w_provider.wallet_inst().is_ok() + }; + // Business goes here + if wallet_opened { + owner::update_wallet_state( + self.wallet_inst.clone(), + (&keychain_mask).as_ref(), + status_send_channel, + false, + )?; + } + if !self.is_running.load(Ordering::Relaxed) { + break; + } + thread::sleep(frequency); + } + Ok(()) + } +} diff --git a/libwallet/src/api_impl/types.rs b/libwallet/src/api_impl/types.rs new file mode 100644 index 0000000..d942cd6 --- /dev/null +++ b/libwallet/src/api_impl/types.rs @@ -0,0 +1,346 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Types specific to the wallet api, mostly argument serialization + +use crate::grin_core::core::Output; +use crate::grin_core::libtx::secp_ser; +use crate::grin_keychain::{BlindingFactor, Identifier}; +use crate::grin_util::secp::pedersen; +use crate::slate_versions::ser as dalek_ser; +use crate::slate_versions::SlateVersion; +use crate::types::OutputData; +use crate::SlatepackAddress; + +use chrono::prelude::*; +use ed25519_dalek::Signature as DalekSignature; + +pub use crate::mwixnet::{Hop, MixnetReqCreationParams, SwapReq}; + +/// Type for storing amounts (in nanogrins). +/// Serializes as a string but can deserialize from a string or u64. +#[derive(Serialize, Deserialize)] +pub struct Amount(#[serde(with = "secp_ser::string_or_u64")] pub u64); + +/// V2 Init / Send TX API Args +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct InitTxArgs { + /// The human readable account name from which to draw outputs + /// for the transaction, overriding whatever the active account is as set via the + /// [`set_active_account`](../grin_wallet_api/owner/struct.Owner.html#method.set_active_account) method. + pub src_acct_name: Option, + #[serde(with = "secp_ser::string_or_u64")] + /// The amount to send, in nanogrins. (`1 G = 1_000_000_000nG`) + pub amount: u64, + /// Does the amount include the fee, or will fees be spent in addition to the amount? + pub amount_includes_fee: Option, + #[serde(with = "secp_ser::string_or_u64")] + /// The minimum number of confirmations an output + /// should have in order to be included in the transaction. + pub minimum_confirmations: u64, + /// By default, the wallet selects as many inputs as possible in a + /// transaction, to reduce the Output set and the fees. The wallet will attempt to spend + /// include up to `max_outputs` in a transaction, however if this is not enough to cover + /// the whole amount, the wallet will include more outputs. This parameter should be considered + /// a soft limit. + pub max_outputs: u32, + /// The target number of change outputs to create in the transaction. + /// The actual number created will be `num_change_outputs` + whatever remainder is needed. + pub num_change_outputs: u32, + /// If `true`, attempt to use up as many outputs as + /// possible to create the transaction, up the 'soft limit' of `max_outputs`. This helps + /// to reduce the size of the UTXO set and the amount of data stored in the wallet, and + /// minimizes fees. This will generally result in many inputs and a large change output(s), + /// usually much larger than the amount being sent. If `false`, the transaction will include + /// as many outputs as are needed to meet the amount, (and no more) starting with the smallest + /// value outputs. + pub selection_strategy_is_use_all: bool, + /// Optionally set the output target slate version (acceptable + /// down to the minimum slate version compatible with the current. If `None` the slate + /// is generated with the latest version. + pub target_slate_version: Option, + /// Number of blocks from current after which TX should be ignored + #[serde(with = "secp_ser::opt_string_or_u64")] + #[serde(default)] + pub ttl_blocks: Option, + /// If set, require a payment proof for the particular recipient + #[serde(default)] + pub payment_proof_recipient_address: Option, + /// If true, just return an estimate of the resulting slate, containing fees and amounts + /// locked without actually locking outputs or creating the transaction. Note if this is set to + /// 'true', the amount field in the slate will contain the total amount locked, not the provided + /// transaction amount + pub estimate_only: Option, + /// EXPERIMENTAL: if flagged, create the transaction as late-locked, i.e. don't select actual + /// inputs until just before finalization + #[serde(default)] + pub late_lock: Option, + /// Sender arguments. If present, the underlying function will also attempt to send the + /// transaction to a destination and optionally finalize the result + pub send_args: Option, +} + +/// Send TX API Args, for convenience functionality that inits the transaction and sends +/// in one go +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct InitTxSendArgs { + /// The destination, contents will depend on the particular method + pub dest: String, + /// Whether to post the transaction if the send and finalize were successful + pub post_tx: bool, + /// Whether to use dandelion when posting. If false, skip the dandelion relay + pub fluff: bool, + /// If set, skip the Slatepack TOR send attempt + pub skip_tor: bool, +} + +impl Default for InitTxArgs { + fn default() -> InitTxArgs { + InitTxArgs { + src_acct_name: None, + amount: 0, + amount_includes_fee: None, + minimum_confirmations: 10, + max_outputs: 500, + num_change_outputs: 1, + selection_strategy_is_use_all: true, + target_slate_version: None, + ttl_blocks: None, + estimate_only: Some(false), + payment_proof_recipient_address: None, + late_lock: Some(false), + send_args: None, + } + } +} + +/// V2 Issue Invoice Tx Args +#[derive(Clone, Serialize, Deserialize)] +pub struct IssueInvoiceTxArgs { + /// The human readable account name to which the received funds should be added + /// overriding whatever the active account is as set via the + /// [`set_active_account`](../grin_wallet_api/owner/struct.Owner.html#method.set_active_account) method. + pub dest_acct_name: Option, + /// The invoice amount in nanogrins. (`1 G = 1_000_000_000nG`) + #[serde(with = "secp_ser::string_or_u64")] + pub amount: u64, + /// Optionally set the output target slate version (acceptable + /// down to the minimum slate version compatible with the current. If `None` the slate + /// is generated with the latest version. + pub target_slate_version: Option, +} + +impl Default for IssueInvoiceTxArgs { + fn default() -> IssueInvoiceTxArgs { + IssueInvoiceTxArgs { + dest_acct_name: None, + amount: 0, + target_slate_version: None, + } + } +} + +/// Sort tx retrieval order +#[derive(Clone, Serialize, Deserialize)] +pub enum RetrieveTxQuerySortOrder { + /// Ascending + Asc, + /// Descending + Desc, +} + +/// Valid sort fields for a transaction list retrieval query +#[derive(Clone, Serialize, Deserialize)] +pub enum RetrieveTxQuerySortField { + /// Transaction Id + Id, + /// Creation Timestamp + CreationTimestamp, + /// Confirmation Timestamp + ConfirmationTimestamp, + /// TotalAmount (AmountCredited-AmountDebited) + TotalAmount, + /// Amount Credited + AmountCredited, + /// Amount Debited + AmountDebited, +} + +/// Retrieve Transaction List Pagination Arguments +#[derive(Clone, Serialize, Deserialize)] +pub struct RetrieveTxQueryArgs { + /// Retrieve transactions with an id higher than or equal to the given + /// If None, consider items from the first transaction and later + pub min_id: Option, + /// Retrieve tranactions with an id less than or equal to the given + /// If None, consider items from the last transaction and earlier + pub max_id: Option, + /// The maximum number of transactions to return + /// if both `before_id_inc` and `after_id_inc` are supplied, this will apply + /// to the before and earlier set + pub limit: Option, + /// whether to exclude cancelled transactions in the returned set + pub exclude_cancelled: Option, + /// whether to only consider outstanding transactions + pub include_outstanding_only: Option, + /// whether to only consider confirmed-only transactions + pub include_confirmed_only: Option, + /// whether to only consider sent transactions + pub include_sent_only: Option, + /// whether to only consider received transactions + pub include_received_only: Option, + /// whether to only consider coinbase transactions + pub include_coinbase_only: Option, + /// whether to only consider reverted transactions + pub include_reverted_only: Option, + /// lower bound on the total amount (amount_credited - amount_debited), inclusive + #[serde(with = "secp_ser::opt_string_or_u64")] + #[serde(default)] + pub min_amount: Option, + /// higher bound on the total amount (amount_credited - amount_debited), inclusive + #[serde(with = "secp_ser::opt_string_or_u64")] + #[serde(default)] + pub max_amount: Option, + /// lower bound on the creation timestamp, inclusive + pub min_creation_timestamp: Option>, + /// higher bound on on the creation timestamp, inclusive + pub max_creation_timestamp: Option>, + /// lower bound on the confirmation timestamp, inclusive + pub min_confirmed_timestamp: Option>, + /// higher bound on the confirmation timestamp, inclusive + pub max_confirmed_timestamp: Option>, + /// Field within the tranasction list on which to sort + /// defaults to ID if not present + pub sort_field: Option, + /// Sort order, defaults to ASC if not present (earliest is first) + pub sort_order: Option, +} + +impl Default for RetrieveTxQueryArgs { + fn default() -> Self { + Self { + min_id: None, + max_id: None, + limit: None, + exclude_cancelled: Some(false), + include_outstanding_only: Some(false), + include_confirmed_only: Some(false), + include_sent_only: Some(false), + include_received_only: Some(false), + include_coinbase_only: Some(false), + include_reverted_only: Some(false), + min_amount: None, + max_amount: None, + min_creation_timestamp: None, + max_creation_timestamp: None, + min_confirmed_timestamp: None, + max_confirmed_timestamp: None, + sort_field: Some(RetrieveTxQuerySortField::Id), + sort_order: Some(RetrieveTxQuerySortOrder::Asc), + } + } +} + +/// Fees in block to use for coinbase amount calculation +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct BlockFees { + /// fees + #[serde(with = "secp_ser::string_or_u64")] + pub fees: u64, + /// height + #[serde(with = "secp_ser::string_or_u64")] + pub height: u64, + /// key id + pub key_id: Option, +} + +impl BlockFees { + /// return key id + pub fn key_id(&self) -> Option { + self.key_id.clone() + } +} + +/// Map Outputdata to commits +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct OutputCommitMapping { + /// Output Data + pub output: OutputData, + /// The commit + #[serde( + serialize_with = "secp_ser::as_hex", + deserialize_with = "secp_ser::commitment_from_hex" + )] + pub commit: pedersen::Commitment, +} + +/// Node height result +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct NodeHeightResult { + /// Last known height + #[serde(with = "secp_ser::string_or_u64")] + pub height: u64, + /// Hash + pub header_hash: String, + /// Whether this height was updated from the node + pub updated_from_node: bool, +} + +/// Version request result +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct VersionInfo { + /// API version + pub foreign_api_version: u16, + /// Slate version + pub supported_slate_versions: Vec, +} + +/// Packaged Payment Proof +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct PaymentProof { + /// Amount + #[serde(with = "secp_ser::string_or_u64")] + pub amount: u64, + /// Kernel Excess + #[serde( + serialize_with = "secp_ser::as_hex", + deserialize_with = "secp_ser::commitment_from_hex" + )] + pub excess: pedersen::Commitment, + /// Recipient Wallet Address + pub recipient_address: SlatepackAddress, + /// Recipient Signature + #[serde(with = "dalek_ser::dalek_sig_serde")] + pub recipient_sig: DalekSignature, + /// Sender Wallet Address + pub sender_address: SlatepackAddress, + /// Sender Signature + #[serde(with = "dalek_ser::dalek_sig_serde")] + pub sender_sig: DalekSignature, +} + +/// Build output result +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct BuiltOutput { + /// Blinding Factor + #[serde( + serialize_with = "secp_ser::as_hex", + deserialize_with = "secp_ser::blind_from_hex" + )] + pub blind: BlindingFactor, + /// Key Identifier + pub key_id: Identifier, + /// Output + pub output: Output, +} diff --git a/libwallet/src/error.rs b/libwallet/src/error.rs new file mode 100644 index 0000000..d5a9f9c --- /dev/null +++ b/libwallet/src/error.rs @@ -0,0 +1,363 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Error types for libwallet + +use crate::grin_core::core::{committed, transaction}; +use crate::grin_core::libtx; +use crate::grin_keychain; +use crate::grin_util::secp; +use crate::util; +use grin_store; + +/// Wallet errors, mostly wrappers around underlying crypto or I/O errors. +#[derive(Clone, Eq, PartialEq, Debug, thiserror::Error, Serialize, Deserialize)] +pub enum Error { + /// Not enough funds + #[error("Not enough funds. Required: {needed_disp:?}, Available: {available_disp:?}")] + NotEnoughFunds { + /// available funds + available: u64, + /// Display friendly + available_disp: String, + /// Needed funds + needed: u64, + /// Display friendly + needed_disp: String, + }, + + /// Fee error + #[error("Fee Error: {0}")] + Fee(String), + + /// LibTX Error + #[error("LibTx Error")] + LibTX(#[from] libtx::Error), + + /// Keychain error + #[error("Keychain error")] + Keychain(#[from] grin_keychain::Error), + + /// Transaction Error + #[error("Transaction error")] + Transaction(#[from] transaction::Error), + + /// API Error + #[error("Client Callback Error: {0}")] + ClientCallback(String), + + /// Error from underlying secp lib + #[error("Secp Lib Error")] + Secp(#[from] secp::Error), + + /// Onion V3 Address Error + #[error("Onion V3 Address Error: {0}")] + OnionV3Address(#[from] util::OnionV3AddressError), + + /// Callback implementation error conversion + #[error("Trait Implementation error")] + CallbackImpl(&'static str), + + /// Wallet backend error + #[error("Wallet store error: {0}")] + Backend(String), + + /// Callback implementation error conversion + #[error("Restore Error")] + Restore, + + /// An error in the format of the JSON structures exchanged by the wallet + #[error("JSON format error: {0}")] + Format(String), + + /// Other serialization errors + #[error("Ser/Deserialization error")] + Deser(crate::grin_core::ser::Error), + + /// IO Error + #[error("I/O error {0}")] + IO(String), + + /// Error when contacting a node through its API + #[error("Node API error")] + Node, + + /// Error contacting wallet API + #[error("Wallet Communication Error: {0}")] + WalletComms(String), + + /// Error originating from hyper. + #[error("Hyper error")] + Hyper, + + /// Error originating from hyper uri parsing. + #[error("Uri parsing error")] + Uri, + + /// Signature error + #[error("Signature error: {0}")] + Signature(String), + + /// OwnerAPIEncryption + #[error("{}", _0)] + APIEncryption(String), + + /// Attempt to use duplicate transaction id in separate transactions + #[error("Duplicate transaction ID error")] + DuplicateTransactionId, + + /// Wallet seed already exists + #[error("Wallet seed exists error: {0}")] + WalletSeedExists(String), + + /// Wallet seed doesn't exist + #[error("Wallet seed doesn't exist error")] + WalletSeedDoesntExist, + + /// Wallet seed doesn't exist + #[error("Wallet seed decryption error")] + WalletSeedDecryption, + + /// Transaction doesn't exist + #[error("Transaction {0} doesn't exist")] + TransactionDoesntExist(String), + + /// Transaction already rolled back + #[error("Transaction {0} cannot be cancelled")] + TransactionNotCancellable(String), + + /// Cancellation error + #[error("Cancellation Error: {0}")] + TransactionCancellationError(&'static str), + + /// Cancellation error + #[error("Tx dump Error: {0}")] + TransactionDumpError(&'static str), + + /// Attempt to repost a transaction that's already confirmed + #[error("Transaction already confirmed error")] + TransactionAlreadyConfirmed, + + /// Transaction has already been received + #[error("Transaction {0} has already been received")] + TransactionAlreadyReceived(String), + + /// Transaction has been cancelled + #[error("Transaction {0} has been cancelled")] + TransactionWasCancelled(String), + + /// Attempt to repost a transaction that's not completed and stored + #[error("Transaction building not completed: {0}")] + TransactionBuildingNotCompleted(u32), + + /// Invalid BIP-32 Depth + #[error("Invalid BIP32 Depth (must be 1 or greater)")] + InvalidBIP32Depth, + + /// Attempt to add an account that exists + #[error("Account Label '{0}' already exists")] + AccountLabelAlreadyExists(String), + + /// Reference unknown account label + #[error("Unknown Account Label '{0}'")] + UnknownAccountLabel(String), + + /// Error from summing commitments via committed trait. + #[error("Committed Error")] + Committed(committed::Error), + + /// Error from summing commitments + #[error("Committed Error: {0}")] + Commit(String), + + /// Error Deserializing commit + #[error("Commit Deserialize Error: {0}")] + CommitDeser(String), + + /// Error Deserializing key + #[error("Server Key Deserialize Error: {0}")] + ServerKeyDeser(String), + + /// Parsing integert + #[error("Can't parse as u64: {0}")] + U64Deser(String), + + /// Can't parse slate version + #[error("Can't parse slate version")] + SlateVersionParse, + + /// Can't serialize slate + #[error("Can't Serialize slate")] + SlateSer, + + /// Can't deserialize slate + #[error("Can't Deserialize slate")] + SlateDeser, + + /// Invalid slate state + #[error("Invalid slate state")] + SlateState, + + /// Can't serialize slate pack + #[error("Can't Serialize slatepack")] + SlatepackSer, + + /// Can't deserialize slate + #[error("Can't Deserialize slatepack: {0}")] + SlatepackDeser(String), + + /// Unknown slate version + #[error("Unknown Slate Version: {0}")] + SlateVersion(u16), + + /// Attempt to use slate transaction data that doesn't exists + #[error("Slate transaction required in this context")] + SlateTransactionRequired, + + /// Attempt to downgrade slate that can't be downgraded + #[error("Can't downgrade slate: {0}")] + SlateInvalidDowngrade(String), + + /// Compatibility error between incoming slate versions and what's expected + #[error("Compatibility Error: {0}")] + Compatibility(String), + + /// Keychain doesn't exist (wallet not openend) + #[error("Keychain doesn't exist (has wallet been opened?)")] + KeychainDoesntExist, + + /// Lifecycle Error + #[error("Lifecycle Error: {0}")] + Lifecycle(String), + + /// Invalid Keychain Mask Error + #[error("Supplied Keychain Mask Token is incorrect")] + InvalidKeychainMask, + + /// Tor Process error + #[error("Tor Process Error: {0}")] + TorProcess(String), + + /// Tor Configuration Error + #[error("Tor Config Error: {0}")] + TorConfig(String), + + /// Generating ED25519 Public Key + #[error("Error generating ed25519 secret key: {0}")] + ED25519Key(String), + + /// Generating Payment Proof + #[error("Payment Proof generation error: {0}")] + PaymentProof(String), + + /// Retrieving Payment Proof + #[error("Payment Proof retrieval error: {0}")] + PaymentProofRetrieval(String), + + /// Retrieving Payment Proof + #[error("Payment Proof parsing error: {0}")] + PaymentProofParsing(String), + + /// Decoding OnionV3 addresses to payment proof addresses + #[error("Proof Address decoding: {0}")] + AddressDecoding(String), + + /// Transaction has expired it's TTL + #[error("Transaction Expired")] + TransactionExpired, + + /// Kernel features args don't exist + #[error("Kernel Features Arg {0} missing")] + KernelFeaturesMissing(String), + + /// Unknown Kernel Feature + #[error("Unknown Kernel Feature: {0}")] + UnknownKernelFeatures(u8), + + /// Invalid Kernel Feature + #[error("Invalid Kernel Feature: {0}")] + InvalidKernelFeatures(u8), + + /// Invalid Slatepack Data + #[error("Invalid Slatepack Data: {0}")] + InvalidSlatepackData(String), + + /// Slatepack Encryption + #[error("Couldn't encrypt Slatepack: {0}")] + SlatepackEncryption(String), + + /// Slatepack Decryption + #[error("Couldn't decrypt Slatepack: {0}")] + SlatepackDecryption(String), + + /// age error + #[error("Age error: {0}")] + Age(String), + + /// Rewind Hash parsing error + #[error("Rewind Hash error: {0}")] + RewindHash(String), + + /// Nonce creation error + #[error("Nonce error: {0}")] + Nonce(String), + + /// Slatepack address parsing error + #[error("SlatepackAddress error: {0}")] + SlatepackAddress(String), + + /// Retrieving Stored Tx + #[error("Stored Tx error: {0}")] + StoredTx(String), + + /// Other + #[error("Generic error: {0}")] + GenericError(String), +} + +impl From for Error { + fn from(error: grin_store::Error) -> Error { + Error::Backend(format!("{}", error)) + } +} + +impl From for Error { + fn from(error: age::EncryptError) -> Error { + Error::Age(format!("{}", error)) + } +} + +impl From for Error { + fn from(error: age::DecryptError) -> Error { + Error::Age(format!("{}", error)) + } +} + +impl From for Error { + fn from(e: std::io::Error) -> Error { + Error::IO(e.to_string()) + } +} + +impl From<&str> for Error { + fn from(error: &str) -> Error { + Error::Age(format!("Bech32 Key Encoding - {}", error)) + } +} + +impl From for Error { + fn from(error: bech32::Error) -> Error { + Error::SlatepackAddress(format!("{}", error)) + } +} diff --git a/libwallet/src/internal.rs b/libwallet/src/internal.rs new file mode 100644 index 0000000..6c1b135 --- /dev/null +++ b/libwallet/src/internal.rs @@ -0,0 +1,28 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! lower-level wallet functions which build upon core::libtx to perform wallet +//! operations + +#![deny(non_upper_case_globals)] +#![deny(non_camel_case_types)] +#![deny(non_snake_case)] +#![deny(unused_mut)] +#![warn(missing_docs)] + +pub mod keys; +pub mod scan; +pub mod selection; +pub mod tx; +pub mod updater; diff --git a/libwallet/src/internal/keys.rs b/libwallet/src/internal/keys.rs new file mode 100644 index 0000000..9d8f4cb --- /dev/null +++ b/libwallet/src/internal/keys.rs @@ -0,0 +1,129 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Wallet key management functions +use crate::error::Error; +use crate::grin_keychain::{ChildNumber, ExtKeychain, Identifier, Keychain}; +use crate::grin_util::secp::key::SecretKey; +use crate::types::{AcctPathMapping, NodeClient, WalletBackend}; + +/// Get next available key in the wallet for a given parent +pub fn next_available_key<'a, T: ?Sized, C, K>( + wallet: &mut T, + keychain_mask: Option<&SecretKey>, +) -> Result +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let child = wallet.next_child(keychain_mask)?; + Ok(child) +} + +/// Retrieve an existing key from a wallet +pub fn retrieve_existing_key<'a, T: ?Sized, C, K>( + wallet: &T, + key_id: Identifier, + mmr_index: Option, +) -> Result<(Identifier, u32), Error> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let existing = wallet.get(&key_id, &mmr_index)?; + let key_id = existing.key_id.clone(); + let derivation = existing.n_child; + Ok((key_id, derivation)) +} + +/// Returns a list of account to BIP32 path mappings +pub fn accounts<'a, T: ?Sized, C, K>(wallet: &mut T) -> Result, Error> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + Ok(wallet.acct_path_iter().collect()) +} + +/// Adds an new parent account path with a given label +pub fn new_acct_path<'a, T: ?Sized, C, K>( + wallet: &mut T, + keychain_mask: Option<&SecretKey>, + label: &str, +) -> Result +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let label = label.to_owned(); + if wallet.acct_path_iter().any(|l| l.label == label) { + return Err(Error::AccountLabelAlreadyExists(label)); + } + + // We're always using paths at m/k/0 for parent keys for output derivations + // so find the highest of those, then increment (to conform with external/internal + // derivation chains in BIP32 spec) + + let highest_entry = wallet.acct_path_iter().max_by(|a, b| { + ::from(a.path.to_path().path[0]).cmp(&::from(b.path.to_path().path[0])) + }); + + let return_id = { + if let Some(e) = highest_entry { + let mut p = e.path.to_path(); + p.path[0] = ChildNumber::from(::from(p.path[0]) + 1); + p.to_identifier() + } else { + ExtKeychain::derive_key_id(2, 0, 0, 0, 0) + } + }; + + let save_path = AcctPathMapping { + label: label, + path: return_id.clone(), + }; + + let mut batch = wallet.batch(keychain_mask)?; + batch.save_acct_path(save_path)?; + batch.commit()?; + Ok(return_id) +} + +/// Adds/sets a particular account path with a given label +pub fn set_acct_path<'a, T: ?Sized, C, K>( + wallet: &mut T, + keychain_mask: Option<&SecretKey>, + label: &str, + path: &Identifier, +) -> Result<(), Error> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let label = label.to_owned(); + let save_path = AcctPathMapping { + label: label, + path: path.clone(), + }; + + let mut batch = wallet.batch(keychain_mask)?; + batch.save_acct_path(save_path)?; + batch.commit()?; + Ok(()) +} diff --git a/libwallet/src/internal/scan.rs b/libwallet/src/internal/scan.rs new file mode 100644 index 0000000..129e99c --- /dev/null +++ b/libwallet/src/internal/scan.rs @@ -0,0 +1,645 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//! Functions to restore a wallet's outputs from just the master seed + +use crate::api_impl::owner_updater::StatusMessage; +use crate::grin_core::consensus::{valid_header_version, WEEK_HEIGHT}; +use crate::grin_core::core::HeaderVersion; +use crate::grin_core::global; +use crate::grin_core::libtx::proof; +use crate::grin_keychain::{Identifier, Keychain, SwitchCommitmentType}; +use crate::grin_util::secp::key::SecretKey; +use crate::grin_util::secp::pedersen; +use crate::grin_util::secp::{ContextFlag, Secp256k1}; +use crate::grin_util::Mutex; +use crate::grin_util::{from_hex, ToHex}; +use crate::internal::{keys, updater}; +use crate::types::*; +use crate::{wallet_lock, Error, OutputCommitMapping}; +use blake2_rfc::blake2b::blake2b; +use std::cmp; +use std::collections::HashMap; +use std::sync::mpsc::Sender; +use std::sync::Arc; + +/// Utility struct for return values from below +#[derive(Debug, Clone)] +struct OutputResult { + /// + pub commit: pedersen::Commitment, + /// + pub key_id: Identifier, + /// + pub n_child: u32, + /// + pub mmr_index: u64, + /// + pub value: u64, + /// + pub height: u64, + /// + pub lock_height: u64, + /// + pub is_coinbase: bool, +} + +#[derive(Debug, Clone)] +/// Collect stats in case we want to just output a single tx log entry +/// for restored non-coinbase outputs +struct RestoredTxStats { + /// + pub log_id: u32, + /// + pub amount_credited: u64, + /// + pub num_outputs: usize, +} + +fn identify_utxo_outputs<'a, K>( + keychain: &K, + outputs: Vec<(pedersen::Commitment, pedersen::RangeProof, bool, u64, u64)>, + status_send_channel: &Option>, + percentage_complete: u8, +) -> Result, Error> +where + K: Keychain + 'a, +{ + let mut wallet_outputs: Vec = Vec::new(); + + let legacy_builder = proof::LegacyProofBuilder::new(keychain); + let builder = proof::ProofBuilder::new(keychain); + let legacy_version = HeaderVersion(1); + + for output in outputs.iter() { + let (commit, proof, is_coinbase, height, mmr_index) = output; + // attempt to unwind message from the RP and get a value + // will fail if it's not ours + let info = { + // Before HF+2wk, try legacy rewind first + let info_legacy = + if valid_header_version(height.saturating_sub(2 * WEEK_HEIGHT), legacy_version) { + proof::rewind(keychain.secp(), &legacy_builder, *commit, None, *proof)? + } else { + None + }; + + // If legacy didn't work, try new rewind + if info_legacy.is_none() { + proof::rewind(keychain.secp(), &builder, *commit, None, *proof)? + } else { + info_legacy + } + }; + + let (amount, key_id, switch) = match info { + Some(i) => i, + None => { + continue; + } + }; + + let lock_height = if *is_coinbase { + *height + global::coinbase_maturity() + } else { + *height + }; + + let msg = format!( + "Output found: {:?}, amount: {:?}, key_id: {:?}, mmr_index: {},", + commit, amount, key_id, mmr_index, + ); + + if let Some(ref s) = status_send_channel { + let _ = s.send(StatusMessage::Scanning(msg, percentage_complete)); + } + + if switch != SwitchCommitmentType::Regular { + let msg = format!("Unexpected switch commitment type {:?}", switch); + if let Some(ref s) = status_send_channel { + let _ = s.send(StatusMessage::UpdateWarning(msg)); + } + } + + wallet_outputs.push(OutputResult { + commit: *commit, + key_id: key_id.clone(), + n_child: key_id.to_path().last_path_index(), + value: amount, + height: *height, + lock_height: lock_height, + is_coinbase: *is_coinbase, + mmr_index: *mmr_index, + }); + } + Ok(wallet_outputs) +} + +fn collect_chain_outputs_rewind_hash<'a, C>( + client: C, + rewind_hash: String, + start_index: u64, + end_index: Option, + status_send_channel: &Option>, +) -> Result +where + C: NodeClient + 'a, +{ + let batch_size = 1000; + let start_index_stat = start_index; + let mut start_index = start_index; + let mut vw = ViewWallet { + rewind_hash: rewind_hash, + output_result: vec![], + total_balance: 0, + last_pmmr_index: 0, + }; + let secp = Secp256k1::with_caps(ContextFlag::VerifyOnly); + + loop { + let (highest_index, last_retrieved_index, outputs) = + client.get_outputs_by_pmmr_index(start_index, end_index, batch_size)?; + + let range = highest_index as f64 - start_index_stat as f64; + let progress = last_retrieved_index as f64 - start_index_stat as f64; + let percentage_complete = cmp::min(((progress / range) * 100.0) as u8, 99); + + let msg = format!( + "Checking {} outputs, up to index {}. (Highest index: {})", + outputs.len(), + highest_index, + last_retrieved_index, + ); + if let Some(ref s) = status_send_channel { + let _ = s.send(StatusMessage::Scanning(msg, percentage_complete)); + } + + // Scanning outputs + for output in outputs.iter() { + let (commit, proof, is_coinbase, height, mmr_index) = output; + let rewind_hash = from_hex(vw.rewind_hash.as_str()) + .map_err(|e| Error::RewindHash(format!("Unable to decode rewind hash: {}", e)))?; + let rewind_nonce = blake2b(32, &commit.0, &rewind_hash); + let nonce = SecretKey::from_slice(&secp, rewind_nonce.as_bytes()) + .map_err(|e| Error::Nonce(format!("Unable to create nonce: {}", e)))?; + let info = secp.rewind_bullet_proof(*commit, nonce.clone(), None, *proof); + + if info.is_err() { + continue; + } + + let info = info.unwrap(); + vw.total_balance += info.value; + let lock_height = if *is_coinbase { + *height + global::coinbase_maturity() + } else { + *height + }; + + let output_info = ViewWalletOutputResult { + commit: commit.to_hex(), + value: info.value, + height: *height, + mmr_index: *mmr_index, + is_coinbase: *is_coinbase, + lock_height: lock_height, + }; + + vw.output_result.push(output_info); + } + if highest_index <= last_retrieved_index { + vw.last_pmmr_index = last_retrieved_index; + break; + } + start_index = last_retrieved_index + 1; + } + Ok(vw) +} + +fn collect_chain_outputs<'a, C, K>( + keychain: &K, + client: C, + start_index: u64, + end_index: Option, + status_send_channel: &Option>, +) -> Result<(Vec, u64), Error> +where + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let batch_size = 1000; + let start_index_stat = start_index; + let mut start_index = start_index; + let mut result_vec: Vec = vec![]; + let last_retrieved_return_index; + loop { + let (highest_index, last_retrieved_index, outputs) = + client.get_outputs_by_pmmr_index(start_index, end_index, batch_size)?; + + let range = highest_index as f64 - start_index_stat as f64; + let progress = last_retrieved_index as f64 - start_index_stat as f64; + let perc_complete = cmp::min(((progress / range) * 100.0) as u8, 99); + + let msg = format!( + "Checking {} outputs, up to index {}. (Highest index: {})", + outputs.len(), + highest_index, + last_retrieved_index, + ); + if let Some(ref s) = status_send_channel { + let _ = s.send(StatusMessage::Scanning(msg, perc_complete)); + } + + result_vec.append(&mut identify_utxo_outputs( + keychain, + outputs.clone(), + status_send_channel, + perc_complete as u8, + )?); + + if highest_index <= last_retrieved_index { + last_retrieved_return_index = last_retrieved_index; + break; + } + start_index = last_retrieved_index + 1; + } + Ok((result_vec, last_retrieved_return_index)) +} + +/// +fn restore_missing_output<'a, L, C, K>( + wallet_inst: Arc>>>, + keychain_mask: Option<&SecretKey>, + output: OutputResult, + found_parents: &mut HashMap, + tx_stats: &mut Option<&mut HashMap>, +) -> Result<(), Error> +where + L: WalletLCProvider<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + wallet_lock!(wallet_inst, w); + + let commit = w.calc_commit_for_cache(keychain_mask, output.value, &output.key_id)?; + let mut batch = w.batch(keychain_mask)?; + + let parent_key_id = output.key_id.parent_path(); + if !found_parents.contains_key(&parent_key_id) { + found_parents.insert(parent_key_id.clone(), 0); + if let Some(ref mut s) = tx_stats { + s.insert( + parent_key_id.clone(), + RestoredTxStats { + log_id: batch.next_tx_log_id(&parent_key_id)?, + amount_credited: 0, + num_outputs: 0, + }, + ); + } + } + + let log_id = if tx_stats.is_none() || output.is_coinbase { + let log_id = batch.next_tx_log_id(&parent_key_id)?; + let entry_type = match output.is_coinbase { + true => TxLogEntryType::ConfirmedCoinbase, + false => TxLogEntryType::TxReceived, + }; + let mut t = TxLogEntry::new(parent_key_id.clone(), entry_type, log_id); + t.confirmed = true; + t.amount_credited = output.value; + t.num_outputs = 1; + t.update_confirmation_ts(); + batch.save_tx_log_entry(t, &parent_key_id)?; + log_id + } else if let Some(ref mut s) = tx_stats { + let ts = s.get(&parent_key_id).unwrap().clone(); + s.insert( + parent_key_id.clone(), + RestoredTxStats { + log_id: ts.log_id, + amount_credited: ts.amount_credited + output.value, + num_outputs: ts.num_outputs + 1, + }, + ); + ts.log_id + } else { + 0 + }; + + let _ = batch.save(OutputData { + root_key_id: parent_key_id.clone(), + key_id: output.key_id, + n_child: output.n_child, + mmr_index: Some(output.mmr_index), + commit: commit, + value: output.value, + status: OutputStatus::Unspent, + height: output.height, + lock_height: output.lock_height, + is_coinbase: output.is_coinbase, + tx_log_entry: Some(log_id), + }); + + let max_child_index = *found_parents.get(&parent_key_id).unwrap(); + if output.n_child >= max_child_index { + found_parents.insert(parent_key_id, output.n_child); + } + + batch.commit()?; + Ok(()) +} + +/// +fn cancel_tx_log_entry<'a, L, C, K>( + wallet_inst: Arc>>>, + keychain_mask: Option<&SecretKey>, + output: &OutputData, +) -> Result<(), Error> +where + L: WalletLCProvider<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let parent_key_id = output.key_id.parent_path(); + wallet_lock!(wallet_inst, w); + let updated_tx_entry = if output.tx_log_entry.is_some() { + let entries = updater::retrieve_txs( + &mut **w, + output.tx_log_entry, + None, + None, + Some(&parent_key_id), + false, + )?; + if !entries.is_empty() { + let mut entry = entries[0].clone(); + match entry.tx_type { + TxLogEntryType::TxSent => entry.tx_type = TxLogEntryType::TxSentCancelled, + TxLogEntryType::TxReceived => entry.tx_type = TxLogEntryType::TxReceivedCancelled, + _ => {} + } + Some(entry) + } else { + None + } + } else { + None + }; + let mut batch = w.batch(keychain_mask)?; + if let Some(t) = updated_tx_entry { + batch.save_tx_log_entry(t, &parent_key_id)?; + } + batch.commit()?; + Ok(()) +} + +/// Scan outputs with a given rewind hash view wallet. +/// Retrieve all outputs information that belongs to it. +pub fn scan_rewind_hash<'a, L, C, K>( + wallet_inst: Arc>>>, + rewind_hash: String, + start_height: u64, + end_height: u64, + status_send_channel: &Option>, +) -> Result +where + L: WalletLCProvider<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + if let Some(ref s) = status_send_channel { + let _ = s.send(StatusMessage::Scanning("Starting UTXO scan".to_owned(), 0)); + } + let client = { + wallet_lock!(wallet_inst, w); + w.w2n_client().clone() + }; + // Retrieve the actual PMMR index range we're looking for + let pmmr_range = client.height_range_to_pmmr_indices(start_height, Some(end_height))?; + + let chain_outs = collect_chain_outputs_rewind_hash( + client, + rewind_hash, + pmmr_range.0, + Some(pmmr_range.1), + status_send_channel, + )?; + + let msg = format!( + "Identified {} wallet_outputs as belonging to this wallet", + chain_outs.output_result.len(), + ); + if let Some(ref s) = status_send_channel { + let _ = s.send(StatusMessage::Scanning(msg, 99)); + } + if let Some(ref s) = status_send_channel { + let _ = s.send(StatusMessage::ScanningComplete( + "Scanning Complete".to_owned(), + )); + } + Ok(chain_outs) +} + +/// Check / repair wallet contents by scanning against chain +/// assume wallet contents have been freshly updated with contents +/// of latest block +pub fn scan<'a, L, C, K>( + wallet_inst: Arc>>>, + keychain_mask: Option<&SecretKey>, + delete_unconfirmed: bool, + start_height: u64, + end_height: u64, + status_send_channel: &Option>, +) -> Result +where + L: WalletLCProvider<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + // First, get a definitive list of outputs we own from the chain + if let Some(ref s) = status_send_channel { + let _ = s.send(StatusMessage::Scanning("Starting UTXO scan".to_owned(), 0)); + } + let (client, keychain) = { + wallet_lock!(wallet_inst, w); + (w.w2n_client().clone(), w.keychain(keychain_mask)?) + }; + + // Retrieve the actual PMMR index range we're looking for + let pmmr_range = client.height_range_to_pmmr_indices(start_height, Some(end_height))?; + + let (chain_outs, last_index) = collect_chain_outputs( + &keychain, + client, + pmmr_range.0, + Some(pmmr_range.1), + status_send_channel, + )?; + let msg = format!( + "Identified {} wallet_outputs as belonging to this wallet", + chain_outs.len(), + ); + + if let Some(ref s) = status_send_channel { + let _ = s.send(StatusMessage::Scanning(msg, 99)); + } + + // Now, get all outputs owned by this wallet (regardless of account) + let wallet_outputs = { + wallet_lock!(wallet_inst, w); + updater::retrieve_outputs(&mut **w, keychain_mask, true, None, None)? + }; + + let mut missing_outs = vec![]; + let mut accidental_spend_outs = vec![]; + let mut locked_outs = vec![]; + + // check all definitive outputs exist in the wallet outputs + for deffo in chain_outs.into_iter() { + let matched_out = wallet_outputs.iter().find(|wo| wo.commit == deffo.commit); + match matched_out { + Some(s) => { + if s.output.status == OutputStatus::Spent { + accidental_spend_outs.push((s.output.clone(), deffo.clone())); + } + if s.output.status == OutputStatus::Locked { + locked_outs.push((s.output.clone(), deffo.clone())); + } + } + None => missing_outs.push(deffo), + } + } + + // mark problem spent outputs as unspent (confirmed against a short-lived fork, for example) + for m in accidental_spend_outs.into_iter() { + let mut o = m.0; + let msg = format!( + "Output for {} with ID {} ({:?}) marked as spent but exists in UTXO set. \ + Marking unspent and cancelling any associated transaction log entries.", + o.value, o.key_id, m.1.commit, + ); + if let Some(ref s) = status_send_channel { + let _ = s.send(StatusMessage::Scanning(msg, 99)); + } + o.status = OutputStatus::Unspent; + // any transactions associated with this should be cancelled + cancel_tx_log_entry(wallet_inst.clone(), keychain_mask, &o)?; + wallet_lock!(wallet_inst, w); + let mut batch = w.batch(keychain_mask)?; + batch.save(o)?; + batch.commit()?; + } + + let mut found_parents: HashMap = HashMap::new(); + + // Restore missing outputs, adding transaction for it back to the log + for m in missing_outs.into_iter() { + let msg = format!( + "Confirmed output for {} with ID {} ({:?}, index {}) exists in UTXO set but not in wallet. \ + Restoring.", + m.value, m.key_id, m.commit, m.mmr_index + ); + if let Some(ref s) = status_send_channel { + let _ = s.send(StatusMessage::Scanning(msg, 99)); + } + restore_missing_output( + wallet_inst.clone(), + keychain_mask, + m, + &mut found_parents, + &mut None, + )?; + } + + if delete_unconfirmed { + // Unlock locked outputs + for m in locked_outs.into_iter() { + let mut o = m.0; + let msg = format!( + "Confirmed output for {} with ID {} ({:?}) exists in UTXO set and is locked. \ + Unlocking and cancelling associated transaction log entries.", + o.value, o.key_id, m.1.commit, + ); + if let Some(ref s) = status_send_channel { + let _ = s.send(StatusMessage::Scanning(msg, 99)); + } + o.status = OutputStatus::Unspent; + cancel_tx_log_entry(wallet_inst.clone(), keychain_mask, &o)?; + wallet_lock!(wallet_inst, w); + let mut batch = w.batch(keychain_mask)?; + batch.save(o)?; + batch.commit()?; + } + + let unconfirmed_outs: Vec<&OutputCommitMapping> = wallet_outputs + .iter() + .filter(|o| o.output.status == OutputStatus::Unconfirmed) + .collect(); + // Delete unconfirmed outputs + for m in unconfirmed_outs.into_iter() { + let o = m.output.clone(); + let msg = format!( + "Unconfirmed output for {} with ID {} ({:?}) not in UTXO set. \ + Deleting and cancelling associated transaction log entries.", + o.value, o.key_id, m.commit, + ); + if let Some(ref s) = status_send_channel { + let _ = s.send(StatusMessage::Scanning(msg, 99)); + } + cancel_tx_log_entry(wallet_inst.clone(), keychain_mask, &o)?; + wallet_lock!(wallet_inst, w); + let mut batch = w.batch(keychain_mask)?; + batch.delete(&o.key_id, &o.mmr_index)?; + batch.commit()?; + } + } + + // restore labels, account paths and child derivation indices + wallet_lock!(wallet_inst, w); + let label_base = "account"; + let accounts: Vec = w.acct_path_iter().map(|m| m.path).collect(); + let mut acct_index = accounts.len(); + for (path, max_child_index) in found_parents.iter() { + // Only restore paths that don't exist + if !accounts.contains(path) { + let label = format!("{}_{}", label_base, acct_index); + let msg = format!("Setting account {} at path {}", label, path); + if let Some(ref s) = status_send_channel { + let _ = s.send(StatusMessage::Scanning(msg, 99)); + } + keys::set_acct_path(&mut **w, keychain_mask, &label, path)?; + acct_index += 1; + } + let current_child_index = w.current_child_index(&path)?; + if *max_child_index >= current_child_index { + let mut batch = w.batch(keychain_mask)?; + debug!("Next child for account {} is {}", path, max_child_index + 1); + batch.save_child_index(path, max_child_index + 1)?; + batch.commit()?; + } + } + + if let Some(ref s) = status_send_channel { + let _ = s.send(StatusMessage::ScanningComplete( + "Scanning Complete".to_owned(), + )); + } + + Ok(ScannedBlockInfo { + height: end_height, + hash: "".to_owned(), + start_pmmr_index: pmmr_range.0, + last_pmmr_index: last_index, + }) +} diff --git a/libwallet/src/internal/selection.rs b/libwallet/src/internal/selection.rs new file mode 100644 index 0000000..5bc4500 --- /dev/null +++ b/libwallet/src/internal/selection.rs @@ -0,0 +1,716 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Selection of inputs for building transactions + +use crate::address; +use crate::error::Error; +use crate::grin_core::core::amount_to_hr_string; +use crate::grin_core::libtx::{ + build, + proof::{ProofBuild, ProofBuilder}, + tx_fee, +}; +use crate::grin_keychain::{Identifier, Keychain}; +use crate::grin_util::secp::key::SecretKey; +use crate::grin_util::secp::pedersen; +use crate::internal::keys; +use crate::slate::Slate; +use crate::types::*; +use crate::util::OnionV3Address; +use std::collections::HashMap; +use std::convert::TryInto; + +/// Initialize a transaction on the sender side, returns a corresponding +/// libwallet transaction slate with the appropriate inputs selected, +/// and saves the private wallet identifiers of our selected outputs +/// into our transaction context + +pub fn build_send_tx<'a, T: ?Sized, C, K>( + wallet: &mut T, + keychain: &K, + keychain_mask: Option<&SecretKey>, + slate: &mut Slate, + current_height: u64, + minimum_confirmations: u64, + max_outputs: usize, + change_outputs: usize, + selection_strategy_is_use_all: bool, + fixed_fee: Option, + parent_key_id: Identifier, + use_test_nonce: bool, + is_initiator: bool, + amount_includes_fee: bool, +) -> Result +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let (elems, inputs, change_amounts_derivations, fee) = select_send_tx( + wallet, + keychain_mask, + slate.amount, + amount_includes_fee, + current_height, + minimum_confirmations, + max_outputs, + change_outputs, + selection_strategy_is_use_all, + &parent_key_id, + false, + )?; + if amount_includes_fee { + slate.amount = slate.amount.checked_sub(fee).ok_or(Error::GenericError( + format!("Transaction amount is too small to include fee").into(), + ))?; + }; + + if fixed_fee.map(|f| fee != f).unwrap_or(false) { + return Err(Error::Fee( + "The initially selected fee is not sufficient".into(), + )); + } + + // Update the fee on the slate so we account for this when building the tx. + slate.fee_fields = fee.try_into().unwrap(); + slate.add_transaction_elements(keychain, &ProofBuilder::new(keychain), elems)?; + + // Create our own private context + let mut context = Context::new( + keychain.secp(), + &parent_key_id, + use_test_nonce, + is_initiator, + ); + + context.fee = Some(slate.fee_fields); + context.amount = slate.amount; + + // Store our private identifiers for each input + for input in inputs { + context.add_input(&input.key_id, &input.mmr_index, input.value); + } + + let mut commits: HashMap> = HashMap::new(); + + // Store change output(s) and cached commits + for (change_amount, id, mmr_index) in &change_amounts_derivations { + context.add_output(&id, &mmr_index, *change_amount); + commits.insert( + id.clone(), + wallet.calc_commit_for_cache(keychain_mask, *change_amount, &id)?, + ); + } + + Ok(context) +} + +/// Locks all corresponding outputs in the context, creates +/// change outputs and tx log entry +pub fn lock_tx_context<'a, T: ?Sized, C, K>( + wallet: &mut T, + keychain_mask: Option<&SecretKey>, + slate: &Slate, + current_height: u64, + context: &Context, + excess_override: Option, +) -> Result<(), Error> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let mut output_commits: HashMap, u64)> = HashMap::new(); + // Store cached commits before locking wallet + let mut total_change = 0; + for (id, _, change_amount) in &context.get_outputs() { + output_commits.insert( + id.clone(), + ( + wallet.calc_commit_for_cache(keychain_mask, *change_amount, &id)?, + *change_amount, + ), + ); + total_change += change_amount; + } + + debug!("Change amount is: {}", total_change); + + let keychain = wallet.keychain(keychain_mask)?; + + let tx_entry = { + let lock_inputs = context.get_inputs(); + let slate_id = slate.id; + let height = current_height; + let parent_key_id = context.parent_key_id.clone(); + let mut batch = wallet.batch(keychain_mask)?; + let log_id = batch.next_tx_log_id(&parent_key_id)?; + let mut t = TxLogEntry::new(parent_key_id.clone(), TxLogEntryType::TxSent, log_id); + t.tx_slate_id = Some(slate_id); + let filename = format!("{}.grintx", slate_id); + t.stored_tx = Some(filename); + t.fee = context.fee; + t.ttl_cutoff_height = match slate.ttl_cutoff_height { + 0 => None, + n => Some(n), + }; + + if let Ok(e) = slate.calc_excess(keychain.secp()) { + t.kernel_excess = Some(e) + } + if let Some(e) = excess_override { + t.kernel_excess = Some(e) + } + t.kernel_lookup_min_height = Some(current_height); + + let mut amount_debited = 0; + t.num_inputs = lock_inputs.len(); + for id in lock_inputs { + let mut coin = batch.get(&id.0, &id.1).unwrap(); + coin.tx_log_entry = Some(log_id); + amount_debited += coin.value; + batch.lock_output(&mut coin)?; + } + + t.amount_debited = amount_debited; + + // store extra payment proof info, if required + if let Some(ref p) = slate.payment_proof { + let sender_address_path = match context.payment_proof_derivation_index { + Some(p) => p, + None => { + return Err(Error::PaymentProof( + "Payment proof derivation index required".to_owned(), + ) + .into()); + } + }; + let sender_key = address::address_from_derivation_path( + &keychain, + &parent_key_id, + sender_address_path, + )?; + let sender_address = OnionV3Address::from_private(&sender_key.0)?; + t.payment_proof = Some(StoredProofInfo { + receiver_address: p.receiver_address, + receiver_signature: p.receiver_signature, + sender_address: sender_address.to_ed25519()?, + sender_address_path, + sender_signature: None, + }); + }; + + // write the output representing our change + for (id, _, _) in &context.get_outputs() { + t.num_outputs += 1; + let (commit, change_amount) = output_commits.get(&id).unwrap().clone(); + t.amount_credited += change_amount; + batch.save(OutputData { + root_key_id: parent_key_id.clone(), + key_id: id.clone(), + n_child: id.to_path().last_path_index(), + commit: commit, + mmr_index: None, + value: change_amount, + status: OutputStatus::Unconfirmed, + height: height, + lock_height: 0, + is_coinbase: false, + tx_log_entry: Some(log_id), + })?; + } + batch.save_tx_log_entry(t.clone(), &parent_key_id)?; + batch.commit()?; + t + }; + wallet.store_tx( + &format!("{}", tx_entry.tx_slate_id.unwrap()), + slate.tx_or_err()?, + )?; + Ok(()) +} + +/// Creates a new output in the wallet for the recipient, +/// returning the key of the fresh output +/// Also creates a new transaction containing the output +pub fn build_recipient_output<'a, T: ?Sized, C, K>( + wallet: &mut T, + keychain_mask: Option<&SecretKey>, + slate: &mut Slate, + current_height: u64, + parent_key_id: Identifier, + use_test_rng: bool, + is_initiator: bool, +) -> Result<(Identifier, Context, TxLogEntry), Error> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + // Create a potential output for this transaction + let key_id = keys::next_available_key(wallet, keychain_mask).unwrap(); + let keychain = wallet.keychain(keychain_mask)?; + let key_id_inner = key_id.clone(); + let amount = slate.amount; + let height = current_height; + + let slate_id = slate.id; + slate.add_transaction_elements( + &keychain, + &ProofBuilder::new(&keychain), + vec![build::output(amount, key_id.clone())], + )?; + + // Add blinding sum to our context + let mut context = Context::new(keychain.secp(), &parent_key_id, use_test_rng, is_initiator); + + context.add_output(&key_id, &None, amount); + context.amount = amount; + context.fee = slate.fee_fields.as_opt(); + let commit = wallet.calc_commit_for_cache(keychain_mask, amount, &key_id_inner)?; + let mut batch = wallet.batch(keychain_mask)?; + let log_id = batch.next_tx_log_id(&parent_key_id)?; + let mut t = TxLogEntry::new(parent_key_id.clone(), TxLogEntryType::TxReceived, log_id); + t.tx_slate_id = Some(slate_id); + t.amount_credited = amount; + t.num_outputs = 1; + t.ttl_cutoff_height = match slate.ttl_cutoff_height { + 0 => None, + n => Some(n), + }; + // when invoicing, this will be invalid + if let Ok(e) = slate.calc_excess(keychain.secp()) { + t.kernel_excess = Some(e) + } + t.kernel_lookup_min_height = Some(current_height); + batch.save(OutputData { + root_key_id: parent_key_id.clone(), + key_id: key_id_inner.clone(), + mmr_index: None, + n_child: key_id_inner.to_path().last_path_index(), + commit: commit, + value: amount, + status: OutputStatus::Unconfirmed, + height: height, + lock_height: 0, + is_coinbase: false, + tx_log_entry: Some(log_id), + })?; + batch.save_tx_log_entry(t.clone(), &parent_key_id)?; + batch.commit()?; + + Ok((key_id, context, t)) +} + +/// Builds a transaction to send to someone from the HD seed associated with the +/// wallet and the amount to send. Handles reading through the wallet data file, +/// selecting outputs to spend and building the change. +pub fn select_send_tx<'a, T: ?Sized, C, K, B>( + wallet: &mut T, + keychain_mask: Option<&SecretKey>, + amount: u64, + amount_includes_fee: bool, + current_height: u64, + minimum_confirmations: u64, + max_outputs: usize, + change_outputs: usize, + selection_strategy_is_use_all: bool, + parent_key_id: &Identifier, + include_inputs_in_sum: bool, +) -> Result< + ( + Vec>>, + Vec, + Vec<(u64, Identifier, Option)>, // change amounts and derivations + u64, // fee + ), + Error, +> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, + B: ProofBuild, +{ + let (coins, _total, amount, fee) = select_coins_and_fee( + wallet, + amount, + amount_includes_fee, + current_height, + minimum_confirmations, + max_outputs, + change_outputs, + selection_strategy_is_use_all, + &parent_key_id, + )?; + + // build transaction skeleton with inputs and change + let (parts, change_amounts_derivations) = inputs_and_change( + &coins, + wallet, + keychain_mask, + amount, + fee, + change_outputs, + include_inputs_in_sum, + )?; + + Ok((parts, coins, change_amounts_derivations, fee)) +} + +/// Select outputs and calculating fee. +pub fn select_coins_and_fee<'a, T: ?Sized, C, K>( + wallet: &mut T, + amount: u64, + amount_includes_fee: bool, + current_height: u64, + minimum_confirmations: u64, + max_outputs: usize, + change_outputs: usize, + selection_strategy_is_use_all: bool, + parent_key_id: &Identifier, +) -> Result< + ( + Vec, + u64, // total + u64, // amount + u64, // fee + ), + Error, +> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + // select some spendable coins from the wallet + let (max_outputs, mut coins) = select_coins( + wallet, + amount, + current_height, + minimum_confirmations, + max_outputs, + selection_strategy_is_use_all, + parent_key_id, + ); + + // sender is responsible for setting the fee on the partial tx + // recipient should double check the fee calculation and not blindly trust the + // sender + + // First attempt to spend without change + let mut fee = tx_fee(coins.len(), 1, 1); + let mut total: u64 = coins.iter().map(|c| c.value).sum(); + let mut amount_with_fee = match amount_includes_fee { + true => amount, + false => amount + fee, + }; + + if total == 0 { + return Err(Error::NotEnoughFunds { + available: 0, + available_disp: amount_to_hr_string(0, false), + needed: amount_with_fee as u64, + needed_disp: amount_to_hr_string(amount_with_fee as u64, false), + }); + } + + // The amount with fee is more than the total values of our max outputs + if total < amount_with_fee && coins.len() == max_outputs { + return Err(Error::NotEnoughFunds { + available: total, + available_disp: amount_to_hr_string(total, false), + needed: amount_with_fee as u64, + needed_disp: amount_to_hr_string(amount_with_fee as u64, false), + }); + } + + let num_outputs = change_outputs + 1; + + // We need to add a change address or amount with fee is more than total + if total != amount_with_fee { + fee = tx_fee(coins.len(), num_outputs, 1); + amount_with_fee = match amount_includes_fee { + true => amount, + false => amount + fee, + }; + + // Here check if we have enough outputs for the amount including fee otherwise + // look for other outputs and check again + while total < amount_with_fee { + // End the loop if we have selected all the outputs and still not enough funds + if coins.len() == max_outputs { + return Err(Error::NotEnoughFunds { + available: total as u64, + available_disp: amount_to_hr_string(total, false), + needed: amount_with_fee as u64, + needed_disp: amount_to_hr_string(amount_with_fee as u64, false), + }); + } + + // select some spendable coins from the wallet + coins = select_coins( + wallet, + amount_with_fee, + current_height, + minimum_confirmations, + max_outputs, + selection_strategy_is_use_all, + parent_key_id, + ) + .1; + fee = tx_fee(coins.len(), num_outputs, 1); + total = coins.iter().map(|c| c.value).sum(); + amount_with_fee = match amount_includes_fee { + true => amount, + false => amount + fee, + }; + } + } + // If original amount includes fee, the new amount should + // be reduced, to accommodate the fee. + let new_amount = match amount_includes_fee { + true => amount.checked_sub(fee).ok_or(Error::GenericError( + format!("Transaction amount is too small to include fee").into(), + ))?, + false => amount, + }; + Ok((coins, total, new_amount, fee)) +} + +/// Selects inputs and change for a transaction +pub fn inputs_and_change<'a, T: ?Sized, C, K, B>( + coins: &[OutputData], + wallet: &mut T, + keychain_mask: Option<&SecretKey>, + amount: u64, + fee: u64, + num_change_outputs: usize, + include_inputs_in_sum: bool, +) -> Result< + ( + Vec>>, + Vec<(u64, Identifier, Option)>, + ), + Error, +> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, + B: ProofBuild, +{ + let mut parts = vec![]; + + // calculate the total across all inputs, and how much is left + let total: u64 = coins.iter().map(|c| c.value).sum(); + + // if we are spending 10,000 coins to send 1,000 then our change will be 9,000 + // if the fee is 80 then the recipient will receive 1000 and our change will be + // 8,920 + let change = total - amount - fee; + + // build inputs using the appropriate derived key_ids + if include_inputs_in_sum { + for coin in coins { + if coin.is_coinbase { + parts.push(build::coinbase_input(coin.value, coin.key_id.clone())); + } else { + parts.push(build::input(coin.value, coin.key_id.clone())); + } + } + } + + let mut change_amounts_derivations = vec![]; + + if change == 0 { + debug!("No change (sending exactly amount + fee), no change outputs to build"); + } else { + debug!( + "Building change outputs: total change: {} ({} outputs)", + change, num_change_outputs + ); + + let part_change = change / num_change_outputs as u64; + let remainder_change = change % part_change; + + for x in 0..num_change_outputs { + // n-1 equal change_outputs and a final one accounting for any remainder + let change_amount = if x == (num_change_outputs - 1) { + part_change + remainder_change + } else { + part_change + }; + + let change_key = wallet.next_child(keychain_mask).unwrap(); + + change_amounts_derivations.push((change_amount, change_key.clone(), None)); + parts.push(build::output(change_amount, change_key)); + } + } + + Ok((parts, change_amounts_derivations)) +} + +/// Select spendable coins from a wallet. +/// Default strategy is to spend the maximum number of outputs (up to +/// max_outputs). Alternative strategy is to spend smallest outputs first +/// but only as many as necessary. When we introduce additional strategies +/// we should pass something other than a bool in. +/// TODO: Possibly move this into another trait to be owned by a wallet? + +pub fn select_coins<'a, T: ?Sized, C, K>( + wallet: &mut T, + amount: u64, + current_height: u64, + minimum_confirmations: u64, + max_outputs: usize, + select_all: bool, + parent_key_id: &Identifier, +) -> (usize, Vec) +// max_outputs_available, Outputs +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + // first find all eligible outputs based on number of confirmations + let mut eligible = wallet + .iter() + .filter(|out| { + out.root_key_id == *parent_key_id + && out.eligible_to_spend(current_height, minimum_confirmations) + }) + .collect::>(); + + let max_available = eligible.len(); + + // sort eligible outputs by increasing value + eligible.sort_by_key(|out| out.value); + + // use a sliding window to identify potential sets of possible outputs to spend + // Case of amount > total amount of max_outputs(500): + // The limit exists because by default, we always select as many inputs as + // possible in a transaction, to reduce both the Output set and the fees. + // But that only makes sense up to a point, hence the limit to avoid being too + // greedy. But if max_outputs(500) is actually not enough to cover the whole + // amount, the wallet should allow going over it to satisfy what the user + // wants to send. So the wallet considers max_outputs more of a soft limit. + if eligible.len() > max_outputs { + for window in eligible.windows(max_outputs) { + let windowed_eligibles = window.to_vec(); + if let Some(outputs) = select_from(amount, select_all, windowed_eligibles) { + return (max_available, outputs); + } + } + // Not exist in any window of which total amount >= amount. + // Then take coins from the smallest one up to the total amount of selected + // coins = the amount. + if let Some(outputs) = select_from(amount, false, eligible.clone()) { + debug!( + "Extending maximum number of outputs. {} outputs selected.", + outputs.len() + ); + return (max_available, outputs); + } + } else if let Some(outputs) = select_from(amount, select_all, eligible.clone()) { + return (max_available, outputs); + } + + // we failed to find a suitable set of outputs to spend, + // so return the largest amount we can so we can provide guidance on what is + // possible + eligible.reverse(); + ( + max_available, + eligible.iter().take(max_outputs).cloned().collect(), + ) +} + +fn select_from(amount: u64, select_all: bool, outputs: Vec) -> Option> { + let total = outputs.iter().fold(0, |acc, x| acc + x.value); + if total >= amount { + if select_all { + Some(outputs.to_vec()) + } else { + let mut selected_amount = 0; + Some( + outputs + .iter() + .take_while(|out| { + let res = selected_amount < amount; + selected_amount += out.value; + res + }) + .cloned() + .collect(), + ) + } + } else { + None + } +} + +/// Repopulates output in the slate's tranacstion +/// with outputs from the stored context +/// change outputs and tx log entry +/// Remove the explicitly stored excess +pub fn repopulate_tx<'a, T: ?Sized, C, K>( + wallet: &mut T, + keychain_mask: Option<&SecretKey>, + slate: &mut Slate, + context: &Context, + update_fee: bool, +) -> Result<(), Error> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + // restore the original amount, fee + slate.amount = context.amount; + if update_fee { + slate.fee_fields = context + .fee + .ok_or_else(|| Error::Fee("Missing fee fields".into()))?; + } + + let keychain = wallet.keychain(keychain_mask)?; + + // restore my signature data + slate.add_participant_info(&keychain, &context, None)?; + + let mut parts = vec![]; + for (id, _, value) in &context.get_inputs() { + let input = wallet.iter().find(|out| out.key_id == *id); + if let Some(i) = input { + if i.is_coinbase { + parts.push(build::coinbase_input(*value, i.key_id.clone())); + } else { + parts.push(build::input(*value, i.key_id.clone())); + } + } + } + for (id, _, value) in &context.get_outputs() { + let output = wallet.iter().find(|out| out.key_id == *id); + if let Some(i) = output { + parts.push(build::output(*value, i.key_id.clone())); + } + } + let _ = slate.add_transaction_elements(&keychain, &ProofBuilder::new(&keychain), parts)?; + // restore the original offset + slate.tx_or_err_mut()?.offset = slate.offset.clone(); + Ok(()) +} diff --git a/libwallet/src/internal/tx.rs b/libwallet/src/internal/tx.rs new file mode 100644 index 0000000..a5dde1a --- /dev/null +++ b/libwallet/src/internal/tx.rs @@ -0,0 +1,683 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Transaction building functions + +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; +use std::io::Cursor; +use uuid::Uuid; + +use crate::grin_core::consensus::valid_header_version; +use crate::grin_core::core::HeaderVersion; +use crate::grin_keychain::{Identifier, Keychain}; +use crate::grin_util::secp::key::SecretKey; +use crate::grin_util::secp::pedersen; +use crate::grin_util::Mutex; +use crate::internal::{selection, updater}; +use crate::slate::Slate; +use crate::types::{Context, NodeClient, StoredProofInfo, TxLogEntryType, WalletBackend}; +use crate::util::OnionV3Address; +use crate::InitTxArgs; +use crate::{address, Error}; +use ed25519_dalek::Keypair as DalekKeypair; +use ed25519_dalek::PublicKey as DalekPublicKey; +use ed25519_dalek::SecretKey as DalekSecretKey; +use ed25519_dalek::Signature as DalekSignature; +use ed25519_dalek::{Signer, Verifier}; +use grin_core::core::FeeFields; + +// static for incrementing test UUIDs +lazy_static! { + static ref SLATE_COUNTER: Mutex = Mutex::new(0); +} + +/// Creates a new slate for a transaction, can be called by anyone involved in +/// the transaction (sender(s), receiver(s)) +pub fn new_tx_slate<'a, T: ?Sized, C, K>( + wallet: &mut T, + amount: u64, + is_invoice: bool, + num_participants: u8, + use_test_rng: bool, + ttl_blocks: Option, +) -> Result +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let current_height = wallet.w2n_client().get_chain_tip()?.0; + let mut slate = Slate::blank(num_participants, is_invoice); + if let Some(b) = ttl_blocks { + slate.ttl_cutoff_height = current_height + b; + } + if use_test_rng { + { + let sc = SLATE_COUNTER.lock(); + let bytes = [4, 54, 67, 12, 43, 2, 98, 76, 32, 50, 87, 5, 1, 33, 43, *sc]; + slate.id = Uuid::from_slice(&bytes).unwrap(); + } + *SLATE_COUNTER.lock() += 1; + } + slate.amount = amount; + + if valid_header_version(current_height, HeaderVersion(1)) { + slate.version_info.block_header_version = 1; + } + + if valid_header_version(current_height, HeaderVersion(2)) { + slate.version_info.block_header_version = 2; + } + + if valid_header_version(current_height, HeaderVersion(3)) { + slate.version_info.block_header_version = 3; + } + + // Set the features explicitly to 0 here. + // This will generate a Plain kernel (rather than a HeightLocked kernel). + slate.kernel_features = 0; + + Ok(slate) +} + +/// Estimates locked amount and fee for the transaction without creating one +pub fn estimate_send_tx<'a, T: ?Sized, C, K>( + wallet: &mut T, + keychain_mask: Option<&SecretKey>, + amount: u64, + amount_includes_fee: bool, + minimum_confirmations: u64, + max_outputs: usize, + num_change_outputs: usize, + selection_strategy_is_use_all: bool, + parent_key_id: &Identifier, +) -> Result< + ( + u64, // total + u64, // fee + ), + Error, +> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + // Get lock height + let current_height = wallet.w2n_client().get_chain_tip()?.0; + // ensure outputs we're selecting are up to date + updater::refresh_outputs(wallet, keychain_mask, parent_key_id, false)?; + + // Sender selects outputs into a new slate and save our corresponding keys in + // a transaction context. The secret key in our transaction context will be + // randomly selected. This returns the public slate, and a closure that locks + // our inputs and outputs once we're convinced the transaction exchange went + // according to plan + // This function is just a big helper to do all of that, in theory + // this process can be split up in any way + let (_coins, total, _amount, fee) = selection::select_coins_and_fee( + wallet, + amount, + amount_includes_fee, + current_height, + minimum_confirmations, + max_outputs, + num_change_outputs, + selection_strategy_is_use_all, + parent_key_id, + )?; + Ok((total, fee)) +} + +/// Add inputs to the slate (effectively becoming the sender) +pub fn add_inputs_to_slate<'a, T: ?Sized, C, K>( + wallet: &mut T, + keychain_mask: Option<&SecretKey>, + slate: &mut Slate, + current_height: u64, + minimum_confirmations: u64, + max_outputs: usize, + num_change_outputs: usize, + selection_strategy_is_use_all: bool, + parent_key_id: &Identifier, + is_initiator: bool, + use_test_rng: bool, + amount_includes_fee: bool, +) -> Result +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + // sender should always refresh outputs + updater::refresh_outputs(wallet, keychain_mask, parent_key_id, false)?; + + // Sender selects outputs into a new slate and save our corresponding keys in + // a transaction context. The secret key in our transaction context will be + // randomly selected. This returns the public slate, and a closure that locks + // our inputs and outputs once we're convinced the transaction exchange went + // according to plan + // This function is just a big helper to do all of that, in theory + // this process can be split up in any way + let mut context = selection::build_send_tx( + wallet, + &wallet.keychain(keychain_mask)?, + keychain_mask, + slate, + current_height, + minimum_confirmations, + max_outputs, + num_change_outputs, + selection_strategy_is_use_all, + None, + parent_key_id.clone(), + use_test_rng, + is_initiator, + amount_includes_fee, + )?; + + // Generate a kernel offset and subtract from our context's secret key. Store + // the offset in the slate's transaction kernel, and adds our public key + // information to the slate + slate.fill_round_1(&wallet.keychain(keychain_mask)?, &mut context)?; + + context.initial_sec_key = context.sec_key.clone(); + + if !is_initiator { + // perform partial sig + slate.fill_round_2( + &wallet.keychain(keychain_mask)?, + &context.sec_key, + &context.sec_nonce, + )?; + } + + Ok(context) +} + +/// Add receiver output to the slate +pub fn add_output_to_slate<'a, T: ?Sized, C, K>( + wallet: &mut T, + keychain_mask: Option<&SecretKey>, + slate: &mut Slate, + current_height: u64, + parent_key_id: &Identifier, + is_initiator: bool, + use_test_rng: bool, +) -> Result +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let keychain = wallet.keychain(keychain_mask)?; + // create an output using the amount in the slate + let (_, mut context, mut tx) = selection::build_recipient_output( + wallet, + keychain_mask, + slate, + current_height, + parent_key_id.clone(), + use_test_rng, + is_initiator, + )?; + + // fill public keys + slate.fill_round_1(&keychain, &mut context)?; + + context.initial_sec_key = context.sec_key.clone(); + + if !is_initiator { + // perform partial sig + slate.fill_round_2(&keychain, &context.sec_key, &context.sec_nonce)?; + // update excess in stored transaction + let mut batch = wallet.batch(keychain_mask)?; + tx.kernel_excess = Some(slate.calc_excess(keychain.secp())?); + batch.save_tx_log_entry(tx.clone(), &parent_key_id)?; + batch.commit()?; + } + + Ok(context) +} + +/// Create context, without adding inputs to slate +pub fn create_late_lock_context<'a, T: ?Sized, C, K>( + wallet: &mut T, + keychain_mask: Option<&SecretKey>, + slate: &mut Slate, + current_height: u64, + init_tx_args: &InitTxArgs, + parent_key_id: &Identifier, + use_test_rng: bool, +) -> Result +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + // sender should always refresh outputs + updater::refresh_outputs(wallet, keychain_mask, parent_key_id, false)?; + + // we're just going to run a selection to get the potential fee, + // but this won't be locked + let (_coins, _total, _amount, fee) = selection::select_coins_and_fee( + wallet, + init_tx_args.amount, + init_tx_args.amount_includes_fee.unwrap_or(false), + current_height, + init_tx_args.minimum_confirmations, + init_tx_args.max_outputs as usize, + init_tx_args.num_change_outputs as usize, + init_tx_args.selection_strategy_is_use_all, + &parent_key_id, + )?; + slate.fee_fields = FeeFields::new(0, fee)?; + + let keychain = wallet.keychain(keychain_mask)?; + + // Create our own private context + let mut context = Context::new(keychain.secp(), &parent_key_id, use_test_rng, true); + context.fee = Some(slate.fee_fields); + context.amount = slate.amount; + context.late_lock_args = Some(init_tx_args.clone()); + + // Generate a blinding factor for the tx and add + // public key info to the slate + slate.fill_round_1(&wallet.keychain(keychain_mask)?, &mut context)?; + + Ok(context) +} + +/// Complete a transaction +pub fn complete_tx<'a, T: ?Sized, C, K>( + wallet: &mut T, + keychain_mask: Option<&SecretKey>, + slate: &mut Slate, + context: &Context, +) -> Result<(), Error> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + // when self sending invoice tx, use initiator nonce to finalize + let (sec_key, sec_nonce) = { + if context.initial_sec_key != context.sec_key + && context.initial_sec_nonce != context.sec_nonce + { + ( + context.initial_sec_key.clone(), + context.initial_sec_nonce.clone(), + ) + } else { + (context.sec_key.clone(), context.sec_nonce.clone()) + } + }; + slate.fill_round_2(&wallet.keychain(keychain_mask)?, &sec_key, &sec_nonce)?; + + // Final transaction can be built by anyone at this stage + trace!("Slate to finalize is: {}", slate); + slate.finalize(&wallet.keychain(keychain_mask)?)?; + Ok(()) +} + +/// Rollback outputs associated with a transaction in the wallet +pub fn cancel_tx<'a, T: ?Sized, C, K>( + wallet: &mut T, + keychain_mask: Option<&SecretKey>, + parent_key_id: &Identifier, + tx_id: Option, + tx_slate_id: Option, +) -> Result<(), Error> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let mut tx_id_string = String::new(); + if let Some(tx_id) = tx_id { + tx_id_string = tx_id.to_string(); + } else if let Some(tx_slate_id) = tx_slate_id { + tx_id_string = tx_slate_id.to_string(); + } + let tx_vec = updater::retrieve_txs( + wallet, + tx_id, + tx_slate_id, + None, + Some(&parent_key_id), + false, + )?; + if tx_vec.len() != 1 { + return Err(Error::TransactionDoesntExist(tx_id_string)); + } + let tx = tx_vec[0].clone(); + match tx.tx_type { + TxLogEntryType::TxSent | TxLogEntryType::TxReceived | TxLogEntryType::TxReverted => {} + _ => return Err(Error::TransactionNotCancellable(tx_id_string)), + } + if tx.confirmed { + return Err(Error::TransactionNotCancellable(tx_id_string)); + } + // get outputs associated with tx + let res = updater::retrieve_outputs( + wallet, + keychain_mask, + false, + Some(tx.id), + Some(&parent_key_id), + )?; + let outputs = res.iter().map(|m| m.output.clone()).collect(); + updater::cancel_tx_and_outputs(wallet, keychain_mask, tx, outputs, parent_key_id)?; + Ok(()) +} + +/// Update the stored transaction (this update needs to happen when the TX is finalised) +pub fn update_stored_tx<'a, T: ?Sized, C, K>( + wallet: &mut T, + keychain_mask: Option<&SecretKey>, + context: &Context, + slate: &Slate, + is_invoiced: bool, +) -> Result<(), Error> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + // finalize command + let tx_vec = updater::retrieve_txs(wallet, None, Some(slate.id), None, None, false)?; + let mut tx = None; + // don't want to assume this is the right tx, in case of self-sending + for t in tx_vec { + if t.tx_type == TxLogEntryType::TxSent && !is_invoiced { + tx = Some(t); + break; + } + if t.tx_type == TxLogEntryType::TxReceived && is_invoiced { + tx = Some(t); + break; + } + } + let mut tx = match tx { + Some(t) => t, + None => return Err(Error::TransactionDoesntExist(slate.id.to_string())), + }; + let parent_key = tx.parent_key_id.clone(); + { + let keychain = wallet.keychain(keychain_mask)?; + tx.kernel_excess = Some(slate.calc_excess(keychain.secp())?); + } + + if let Some(ref p) = slate.clone().payment_proof { + let derivation_index = match context.payment_proof_derivation_index { + Some(i) => i, + None => 0, + }; + let keychain = wallet.keychain(keychain_mask)?; + let parent_key_id = wallet.parent_key_id(); + let excess = slate.calc_excess(keychain.secp())?; + let sender_key = + address::address_from_derivation_path(&keychain, &parent_key_id, derivation_index)?; + let sender_address = OnionV3Address::from_private(&sender_key.0)?; + let sig = + create_payment_proof_signature(slate.amount, &excess, p.sender_address, sender_key)?; + tx.payment_proof = Some(StoredProofInfo { + receiver_address: p.receiver_address, + receiver_signature: p.receiver_signature, + sender_address_path: derivation_index, + sender_address: sender_address.to_ed25519()?, + sender_signature: Some(sig), + }) + } + + wallet.store_tx(&format!("{}", tx.tx_slate_id.unwrap()), slate.tx_or_err()?)?; + + let mut batch = wallet.batch(keychain_mask)?; + batch.save_tx_log_entry(tx, &parent_key)?; + batch.commit()?; + Ok(()) +} + +pub fn payment_proof_message( + amount: u64, + kernel_commitment: &pedersen::Commitment, + sender_address: DalekPublicKey, +) -> Result, Error> { + let mut msg = Vec::new(); + msg.write_u64::(amount)?; + msg.append(&mut kernel_commitment.0.to_vec()); + msg.append(&mut sender_address.to_bytes().to_vec()); + Ok(msg) +} + +pub fn _decode_payment_proof_message( + msg: &[u8], +) -> Result<(u64, pedersen::Commitment, DalekPublicKey), Error> { + let mut rdr = Cursor::new(msg); + let amount = rdr.read_u64::()?; + let mut commit_bytes = [0u8; 33]; + for i in 0..33 { + commit_bytes[i] = rdr.read_u8()?; + } + let mut sender_address_bytes = [0u8; 32]; + for i in 0..32 { + sender_address_bytes[i] = rdr.read_u8()?; + } + + Ok(( + amount, + pedersen::Commitment::from_vec(commit_bytes.to_vec()), + DalekPublicKey::from_bytes(&sender_address_bytes).unwrap(), + )) +} + +/// create a payment proof +pub fn create_payment_proof_signature( + amount: u64, + kernel_commitment: &pedersen::Commitment, + sender_address: DalekPublicKey, + sec_key: SecretKey, +) -> Result { + let msg = payment_proof_message(amount, kernel_commitment, sender_address)?; + let d_skey = match DalekSecretKey::from_bytes(&sec_key.0) { + Ok(k) => k, + Err(e) => { + return Err(Error::ED25519Key(format!("{}", e))); + } + }; + let pub_key: DalekPublicKey = (&d_skey).into(); + let keypair = DalekKeypair { + public: pub_key, + secret: d_skey, + }; + Ok(keypair.sign(&msg)) +} + +/// Verify all aspects of a completed payment proof on the current slate +pub fn verify_slate_payment_proof<'a, T: ?Sized, C, K>( + wallet: &mut T, + keychain_mask: Option<&SecretKey>, + parent_key_id: &Identifier, + context: &Context, + slate: &Slate, +) -> Result<(), Error> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let tx_vec = updater::retrieve_txs( + wallet, + None, + Some(slate.id), + None, + Some(parent_key_id), + false, + )?; + if tx_vec.is_empty() { + return Err(Error::PaymentProof( + "TxLogEntry with original proof info not found (is account correct?)".to_owned(), + )); + } + + let orig_proof_info = tx_vec[0].clone().payment_proof; + + if orig_proof_info.is_some() && slate.payment_proof.is_none() { + return Err(Error::PaymentProof( + "Expected Payment Proof for this Transaction is not present".to_owned(), + )); + } + + if let Some(ref p) = slate.clone().payment_proof { + let orig_proof_info = match orig_proof_info { + Some(p) => p.clone(), + None => { + return Err(Error::PaymentProof( + "Original proof info not stored in tx".to_owned(), + )); + } + }; + let keychain = wallet.keychain(keychain_mask)?; + let index = match context.payment_proof_derivation_index { + Some(i) => i, + None => { + return Err(Error::PaymentProof( + "Payment proof derivation index required".to_owned(), + )); + } + }; + let orig_sender_sk = + address::address_from_derivation_path(&keychain, parent_key_id, index)?; + let orig_sender_address = OnionV3Address::from_private(&orig_sender_sk.0)?; + if p.sender_address != orig_sender_address.to_ed25519()? { + return Err(Error::PaymentProof( + "Sender address on slate does not match original sender address".to_owned(), + )); + } + + if orig_proof_info.receiver_address != p.receiver_address { + return Err(Error::PaymentProof( + "Recipient address on slate does not match original recipient address".to_owned(), + )); + } + let msg = payment_proof_message( + slate.amount, + &slate.calc_excess(&keychain.secp())?, + orig_sender_address.to_ed25519()?, + )?; + let sig = match p.receiver_signature { + Some(s) => s, + None => { + return Err(Error::PaymentProof( + "Recipient did not provide requested proof signature".to_owned(), + )); + } + }; + + if p.receiver_address.verify(&msg, &sig).is_err() { + return Err(Error::PaymentProof("Invalid proof signature".to_owned())); + }; + } + Ok(()) +} + +#[cfg(test)] +mod test { + use super::*; + use rand::rngs::mock::StepRng; + + use crate::grin_core::core::{FeeFields, KernelFeatures}; + use crate::grin_core::libtx::{build, ProofBuilder}; + use crate::grin_keychain::{ + BlindSum, BlindingFactor, ExtKeychain, ExtKeychainPath, Keychain, SwitchCommitmentType, + }; + use crate::grin_util::{secp, static_secp_instance}; + + #[test] + // demonstrate that input.commitment == referenced output.commitment + // based on the public key and amount begin spent + fn output_commitment_equals_input_commitment_on_spend() { + let keychain = ExtKeychain::from_random_seed(false).unwrap(); + let builder = ProofBuilder::new(&keychain); + let key_id1 = ExtKeychainPath::new(1, 1, 0, 0, 0).to_identifier(); + + let tx1 = build::transaction( + KernelFeatures::Plain { + fee: FeeFields::zero(), + }, + &[build::output(105, key_id1.clone())], + &keychain, + &builder, + ) + .unwrap(); + let tx2 = build::transaction( + KernelFeatures::Plain { + fee: FeeFields::zero(), + }, + &[build::input(105, key_id1.clone())], + &keychain, + &builder, + ) + .unwrap(); + + let inputs: Vec<_> = tx2.inputs().into(); + assert_eq!(tx1.outputs()[0].commitment(), inputs[0].commitment()); + } + + #[test] + fn payment_proof_construction() { + let secp_inst = static_secp_instance(); + let secp = secp_inst.lock(); + let mut test_rng = StepRng::new(1_234_567_890_u64, 1); + let sec_key = secp::key::SecretKey::new(&secp, &mut test_rng); + let d_skey = DalekSecretKey::from_bytes(&sec_key.0).unwrap(); + + let address: DalekPublicKey = (&d_skey).into(); + + let kernel_excess = { + ExtKeychainPath::new(1, 1, 0, 0, 0).to_identifier(); + let keychain = ExtKeychain::from_random_seed(true).unwrap(); + let switch = SwitchCommitmentType::Regular; + let id1 = ExtKeychain::derive_key_id(1, 1, 0, 0, 0); + let id2 = ExtKeychain::derive_key_id(1, 2, 0, 0, 0); + let skey1 = keychain.derive_key(0, &id1, switch).unwrap(); + let skey2 = keychain.derive_key(0, &id2, switch).unwrap(); + let blinding_factor = keychain + .blind_sum( + &BlindSum::new() + .sub_blinding_factor(BlindingFactor::from_secret_key(skey1)) + .add_blinding_factor(BlindingFactor::from_secret_key(skey2)), + ) + .unwrap(); + keychain + .secp() + .commit(0, blinding_factor.secret_key(&keychain.secp()).unwrap()) + .unwrap() + }; + + let amount = 1_234_567_890_u64; + let msg = payment_proof_message(amount, &kernel_excess, address).unwrap(); + println!("payment proof message is (len {}): {:?}", msg.len(), msg); + + let decoded = _decode_payment_proof_message(&msg).unwrap(); + assert_eq!(decoded.0, amount); + assert_eq!(decoded.1, kernel_excess); + assert_eq!(decoded.2, address); + + let sig = create_payment_proof_signature(amount, &kernel_excess, address, sec_key).unwrap(); + + assert!(address.verify(&msg, &sig).is_ok()); + } +} diff --git a/libwallet/src/internal/updater.rs b/libwallet/src/internal/updater.rs new file mode 100644 index 0000000..a9958de --- /dev/null +++ b/libwallet/src/internal/updater.rs @@ -0,0 +1,885 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Utilities to check the status of all the outputs we have stored in +//! the wallet storage and update them. + +use std::collections::{HashMap, HashSet}; +use uuid::Uuid; + +use crate::error::Error; +use crate::grin_core::consensus::reward; +use crate::grin_core::core::{Output, TxKernel}; +use crate::grin_core::global; +use crate::grin_core::libtx::proof::ProofBuilder; +use crate::grin_core::libtx::reward; +use crate::grin_keychain::{Identifier, Keychain, SwitchCommitmentType}; +use crate::grin_util as util; +use crate::grin_util::secp::key::SecretKey; +use crate::grin_util::secp::pedersen; +use crate::grin_util::static_secp_instance; +use crate::internal::keys; +use crate::types::{ + NodeClient, OutputData, OutputStatus, TxLogEntry, TxLogEntryType, WalletBackend, WalletInfo, +}; +use crate::{ + BlockFees, CbData, OutputCommitMapping, RetrieveTxQueryArgs, RetrieveTxQuerySortField, + RetrieveTxQuerySortOrder, +}; + +use num_bigint::BigInt; + +/// Retrieve all of the outputs (doesn't attempt to update from node) +pub fn retrieve_outputs<'a, T: ?Sized, C, K>( + wallet: &mut T, + keychain_mask: Option<&SecretKey>, + show_spent: bool, + tx_id: Option, + parent_key_id: Option<&Identifier>, +) -> Result, Error> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + // just read the wallet here, no need for a write lock + let mut outputs = wallet + .iter() + .filter(|out| show_spent || out.status != OutputStatus::Spent) + .collect::>(); + + // only include outputs with a given tx_id if provided + if let Some(id) = tx_id { + outputs = outputs + .into_iter() + .filter(|out| out.tx_log_entry == Some(id)) + .collect::>(); + } + + if let Some(k) = parent_key_id { + outputs = outputs + .iter() + .filter(|o| o.root_key_id == *k) + .cloned() + .collect() + } + + outputs.sort_by_key(|out| out.n_child); + let keychain = wallet.keychain(keychain_mask)?; + + let res = outputs + .into_iter() + .map(|output| { + let commit = match output.commit.clone() { + Some(c) => pedersen::Commitment::from_vec(util::from_hex(&c).unwrap()), + None => keychain + .commit(output.value, &output.key_id, SwitchCommitmentType::Regular) + .unwrap(), // TODO: proper support for different switch commitment schemes + }; + OutputCommitMapping { output, commit } + }) + .collect(); + Ok(res) +} + +/// Apply advanced filtering to resultset from retrieve_txs below +pub fn apply_advanced_tx_list_filtering<'a, T: ?Sized, C, K>( + wallet: &mut T, + query_args: &RetrieveTxQueryArgs, +) -> Vec +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + // Apply simple bool, GTE or LTE fields + let txs_iter: Box> = Box::new( + wallet + .tx_log_iter() + .filter(|tx_entry| { + if let Some(v) = query_args.exclude_cancelled { + if v { + tx_entry.tx_type != TxLogEntryType::TxReceivedCancelled + && tx_entry.tx_type != TxLogEntryType::TxSentCancelled + } else { + true + } + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.include_outstanding_only { + if v { + !tx_entry.confirmed + } else { + true + } + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.include_confirmed_only { + if v { + tx_entry.confirmed + } else { + true + } + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.include_sent_only { + if v { + tx_entry.tx_type == TxLogEntryType::TxSent + || tx_entry.tx_type == TxLogEntryType::TxSentCancelled + } else { + true + } + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.include_received_only { + if v { + tx_entry.tx_type == TxLogEntryType::TxReceived + || tx_entry.tx_type == TxLogEntryType::TxReceivedCancelled + } else { + true + } + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.include_coinbase_only { + if v { + tx_entry.tx_type == TxLogEntryType::ConfirmedCoinbase + } else { + true + } + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.include_reverted_only { + if v { + tx_entry.tx_type == TxLogEntryType::TxReverted + } else { + true + } + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.min_id { + tx_entry.id >= v + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.max_id { + tx_entry.id <= v + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.min_amount { + if tx_entry.tx_type == TxLogEntryType::TxSent + || tx_entry.tx_type == TxLogEntryType::TxSentCancelled + { + BigInt::from(tx_entry.amount_debited) + - BigInt::from(tx_entry.amount_credited) + >= BigInt::from(v) + } else { + BigInt::from(tx_entry.amount_credited) + - BigInt::from(tx_entry.amount_debited) + >= BigInt::from(v) + } + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.max_amount { + if tx_entry.tx_type == TxLogEntryType::TxSent + || tx_entry.tx_type == TxLogEntryType::TxSentCancelled + { + BigInt::from(tx_entry.amount_debited) + - BigInt::from(tx_entry.amount_credited) + <= BigInt::from(v) + } else { + BigInt::from(tx_entry.amount_credited) + - BigInt::from(tx_entry.amount_debited) + <= BigInt::from(v) + } + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.min_creation_timestamp { + tx_entry.creation_ts >= v + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.min_confirmed_timestamp { + tx_entry.creation_ts <= v + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.min_confirmed_timestamp { + if let Some(t) = tx_entry.confirmation_ts { + t >= v + } else { + true + } + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.max_confirmed_timestamp { + if let Some(t) = tx_entry.confirmation_ts { + t <= v + } else { + true + } + } else { + true + } + }), + ); + + let mut return_txs: Vec = txs_iter.collect(); + + // Now apply requested sorting + if let Some(ref s) = query_args.sort_field { + match s { + RetrieveTxQuerySortField::Id => { + return_txs.sort_by_key(|tx| tx.id); + } + RetrieveTxQuerySortField::CreationTimestamp => { + return_txs.sort_by_key(|tx| tx.creation_ts); + } + RetrieveTxQuerySortField::ConfirmationTimestamp => { + return_txs.sort_by_key(|tx| tx.confirmation_ts); + } + RetrieveTxQuerySortField::TotalAmount => { + return_txs.sort_by_key(|tx| { + if tx.tx_type == TxLogEntryType::TxSent + || tx.tx_type == TxLogEntryType::TxSentCancelled + { + BigInt::from(tx.amount_debited) - BigInt::from(tx.amount_credited) + } else { + BigInt::from(tx.amount_credited) - BigInt::from(tx.amount_debited) + } + }); + } + RetrieveTxQuerySortField::AmountCredited => { + return_txs.sort_by_key(|tx| tx.amount_credited); + } + RetrieveTxQuerySortField::AmountDebited => { + return_txs.sort_by_key(|tx| tx.amount_debited); + } + } + } else { + return_txs.sort_by_key(|tx| tx.id); + } + + if let Some(ref s) = query_args.sort_order { + match s { + RetrieveTxQuerySortOrder::Desc => return_txs.reverse(), + _ => {} + } + } + + // Apply limit if requested + if let Some(l) = query_args.limit { + return_txs = return_txs.into_iter().take(l as usize).collect() + } + + return_txs +} + +/// Retrieve all of the transaction entries, or a particular entry +/// if `parent_key_id` is set, only return entries from that key +pub fn retrieve_txs<'a, T: ?Sized, C, K>( + wallet: &mut T, + tx_id: Option, + tx_slate_id: Option, + query_args: Option, + parent_key_id: Option<&Identifier>, + outstanding_only: bool, +) -> Result, Error> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let mut txs; + // Adding in new transaction list query logic. If `tx_id` or `tx_slate_id` + // is provided, then `query_args` is ignored and old logic is followed. + if query_args.is_some() && tx_id.is_none() && tx_slate_id.is_none() { + txs = apply_advanced_tx_list_filtering(wallet, &query_args.unwrap()) + } else { + txs = wallet + .tx_log_iter() + .filter(|tx_entry| { + let f_pk = match parent_key_id { + Some(k) => tx_entry.parent_key_id == *k, + None => true, + }; + let f_tx_id = match tx_id { + Some(i) => tx_entry.id == i, + None => true, + }; + let f_txs = match tx_slate_id { + Some(t) => tx_entry.tx_slate_id == Some(t), + None => true, + }; + let f_outstanding = match outstanding_only { + true => { + !tx_entry.confirmed + && (tx_entry.tx_type == TxLogEntryType::TxReceived + || tx_entry.tx_type == TxLogEntryType::TxSent + || tx_entry.tx_type == TxLogEntryType::TxReverted) + } + false => true, + }; + f_pk && f_tx_id && f_txs && f_outstanding + }) + .collect(); + txs.sort_by_key(|tx| tx.creation_ts); + } + Ok(txs) +} + +/// Refreshes the outputs in a wallet with the latest information +/// from a node +pub fn refresh_outputs<'a, T: ?Sized, C, K>( + wallet: &mut T, + keychain_mask: Option<&SecretKey>, + parent_key_id: &Identifier, + update_all: bool, +) -> Result<(), Error> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let height = wallet.w2n_client().get_chain_tip()?.0; + refresh_output_state(wallet, keychain_mask, height, parent_key_id, update_all)?; + Ok(()) +} + +/// build a local map of wallet outputs keyed by commit +/// and a list of outputs we want to query the node for +pub fn map_wallet_outputs<'a, T: ?Sized, C, K>( + wallet: &mut T, + keychain_mask: Option<&SecretKey>, + parent_key_id: &Identifier, + update_all: bool, +) -> Result, Option, bool)>, Error> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let mut wallet_outputs = HashMap::new(); + let keychain = wallet.keychain(keychain_mask)?; + let unspents: Vec = wallet + .iter() + .filter(|x| x.root_key_id == *parent_key_id && x.status != OutputStatus::Spent) + .collect(); + + let tx_entries = retrieve_txs(wallet, None, None, None, Some(&parent_key_id), true)?; + + // Only select outputs that are actually involved in an outstanding transaction + let unspents = match update_all { + false => unspents + .into_iter() + .filter(|x| match x.tx_log_entry.as_ref() { + Some(t) => tx_entries.iter().any(|te| te.id == *t), + None => true, + }) + .collect(), + true => unspents, + }; + + for out in unspents { + let commit = match out.commit.clone() { + Some(c) => pedersen::Commitment::from_vec(util::from_hex(&c).unwrap()), + None => keychain + .commit(out.value, &out.key_id, SwitchCommitmentType::Regular) + .unwrap(), // TODO: proper support for different switch commitment schemes + }; + let val = ( + out.key_id.clone(), + out.mmr_index, + out.tx_log_entry, + out.status == OutputStatus::Unspent, + ); + wallet_outputs.insert(commit, val); + } + Ok(wallet_outputs) +} + +/// Cancel transaction and associated outputs +pub fn cancel_tx_and_outputs<'a, T: ?Sized, C, K>( + wallet: &mut T, + keychain_mask: Option<&SecretKey>, + mut tx: TxLogEntry, + outputs: Vec, + parent_key_id: &Identifier, +) -> Result<(), Error> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let mut batch = wallet.batch(keychain_mask)?; + + for mut o in outputs { + // unlock locked outputs + if o.status == OutputStatus::Unconfirmed || o.status == OutputStatus::Reverted { + batch.delete(&o.key_id, &o.mmr_index)?; + } + if o.status == OutputStatus::Locked { + o.status = OutputStatus::Unspent; + batch.save(o)?; + } + } + match tx.tx_type { + TxLogEntryType::TxSent => tx.tx_type = TxLogEntryType::TxSentCancelled, + TxLogEntryType::TxReceived | TxLogEntryType::TxReverted => { + tx.tx_type = TxLogEntryType::TxReceivedCancelled + } + _ => {} + } + batch.save_tx_log_entry(tx, parent_key_id)?; + batch.commit()?; + Ok(()) +} + +/// Apply refreshed API output data to the wallet +pub fn apply_api_outputs<'a, T: ?Sized, C, K>( + wallet: &mut T, + keychain_mask: Option<&SecretKey>, + wallet_outputs: &HashMap, Option, bool)>, + api_outputs: &HashMap, + reverted_kernels: HashSet, + height: u64, + parent_key_id: &Identifier, +) -> Result<(), Error> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + // now for each commit, find the output in the wallet and the corresponding + // api output (if it exists) and refresh it in-place in the wallet. + // Note: minimizing the time we spend holding the wallet lock. + { + let last_confirmed_height = wallet.last_confirmed_height()?; + // If the server height is less than our confirmed height, don't apply + // these changes as the chain is syncing, incorrect or forking + if height < last_confirmed_height { + warn!( + "Not updating outputs as the height of the node's chain \ + is less than the last reported wallet update height." + ); + warn!("Please wait for sync on node to complete or fork to resolve and try again."); + return Ok(()); + } + let mut batch = wallet.batch(keychain_mask)?; + for (commit, (id, mmr_index, _, _)) in wallet_outputs.iter() { + if let Ok(mut output) = batch.get(id, mmr_index) { + match api_outputs.get(&commit) { + Some(o) => { + // if this is a coinbase tx being confirmed, it's recordable in tx log + if output.is_coinbase && output.status == OutputStatus::Unconfirmed { + let log_id = batch.next_tx_log_id(parent_key_id)?; + let mut t = TxLogEntry::new( + parent_key_id.clone(), + TxLogEntryType::ConfirmedCoinbase, + log_id, + ); + t.confirmed = true; + t.amount_credited = output.value; + t.amount_debited = 0; + t.num_outputs = 1; + // calculate kernel excess for coinbase + { + let secp = static_secp_instance(); + let secp = secp.lock(); + let over_commit = secp.commit_value(output.value)?; + let excess = secp.commit_sum(vec![*commit], vec![over_commit])?; + t.kernel_excess = Some(excess); + t.kernel_lookup_min_height = Some(height); + } + t.update_confirmation_ts(); + output.tx_log_entry = Some(log_id); + batch.save_tx_log_entry(t, &parent_key_id)?; + } + // also mark the transaction in which this output is involved as confirmed + // note that one involved input/output confirmation SHOULD be enough + // to reliably confirm the tx + if !output.is_coinbase + && (output.status == OutputStatus::Unconfirmed + || output.status == OutputStatus::Reverted) + { + let tx = batch.tx_log_iter().find(|t| { + Some(t.id) == output.tx_log_entry + && t.parent_key_id == *parent_key_id + }); + if let Some(mut t) = tx { + if t.tx_type == TxLogEntryType::TxReverted { + t.tx_type = TxLogEntryType::TxReceived; + t.reverted_after = None; + } + t.update_confirmation_ts(); + t.confirmed = true; + batch.save_tx_log_entry(t, &parent_key_id)?; + } + } + output.height = o.1; + output.mark_unspent(); + } + None => { + if !output.is_coinbase + && output + .tx_log_entry + .map(|i| reverted_kernels.contains(&i)) + .unwrap_or(false) + { + output.mark_reverted(); + } else { + output.mark_spent(); + } + } + } + batch.save(output)?; + } + } + + for mut tx in batch.tx_log_iter() { + if reverted_kernels.contains(&tx.id) && tx.parent_key_id == *parent_key_id { + tx.tx_type = TxLogEntryType::TxReverted; + tx.reverted_after = tx.confirmation_ts.clone().and_then(|t| { + let now = chrono::Utc::now(); + (now - t).to_std().ok() + }); + tx.confirmed = false; + batch.save_tx_log_entry(tx, &parent_key_id)?; + } + } + + { + batch.save_last_confirmed_height(parent_key_id, height)?; + } + batch.commit()?; + } + Ok(()) +} + +/// Builds a single api query to retrieve the latest output data from the node. +/// So we can refresh the local wallet outputs. +fn refresh_output_state<'a, T: ?Sized, C, K>( + wallet: &mut T, + keychain_mask: Option<&SecretKey>, + height: u64, + parent_key_id: &Identifier, + update_all: bool, +) -> Result<(), Error> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + debug!("Refreshing wallet outputs"); + + // build a local map of wallet outputs keyed by commit + // and a list of outputs we want to query the node for + let wallet_outputs = map_wallet_outputs(wallet, keychain_mask, parent_key_id, update_all)?; + + let wallet_output_keys = wallet_outputs.keys().copied().collect(); + + let api_outputs = wallet + .w2n_client() + .get_outputs_from_node(wallet_output_keys)?; + + // For any disappeared output, check the on-chain status of the corresponding transaction kernel + // If it is no longer present, the transaction was reverted due to a re-org + let reverted_kernels = + find_reverted_kernels(wallet, &wallet_outputs, &api_outputs, parent_key_id)?; + + apply_api_outputs( + wallet, + keychain_mask, + &wallet_outputs, + &api_outputs, + reverted_kernels, + height, + parent_key_id, + )?; + clean_old_unconfirmed(wallet, keychain_mask, height)?; + Ok(()) +} + +fn find_reverted_kernels<'a, T: ?Sized, C, K>( + wallet: &mut T, + wallet_outputs: &HashMap, Option, bool)>, + api_outputs: &HashMap, + parent_key_id: &Identifier, +) -> Result, Error> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let mut client = wallet.w2n_client().clone(); + let mut ids = HashSet::new(); + + // Get transaction IDs for outputs that are no longer unspent + for (commit, (_, _, tx_id, was_unspent)) in wallet_outputs { + if let Some(tx_id) = *tx_id { + if *was_unspent && !api_outputs.contains_key(commit) { + ids.insert(tx_id); + } + } + } + + // Get corresponding kernels + let kernels = wallet + .tx_log_iter() + .filter(|t| { + ids.contains(&t.id) + && t.parent_key_id == *parent_key_id + && t.tx_type == TxLogEntryType::TxReceived + }) + .filter_map(|t| { + t.kernel_excess + .map(|e| (t.id, e, t.kernel_lookup_min_height)) + }); + + // Check each of the kernels on-chain + let mut reverted = HashSet::new(); + for (id, excess, min_height) in kernels { + if client.get_kernel(&excess, min_height, None)?.is_none() { + reverted.insert(id); + } + } + + Ok(reverted) +} + +fn clean_old_unconfirmed<'a, T: ?Sized, C, K>( + wallet: &mut T, + keychain_mask: Option<&SecretKey>, + height: u64, +) -> Result<(), Error> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + if height < 50 { + return Ok(()); + } + let mut ids_to_del = vec![]; + for out in wallet.iter() { + if out.status == OutputStatus::Unconfirmed + && out.height > 0 + && out.height < height - 50 + && out.is_coinbase + { + ids_to_del.push(out.key_id.clone()) + } + } + let mut batch = wallet.batch(keychain_mask)?; + for id in ids_to_del { + batch.delete(&id, &None)?; + } + batch.commit()?; + Ok(()) +} + +/// Retrieve summary info about the wallet +/// caller should refresh first if desired +pub fn retrieve_info<'a, T: ?Sized, C, K>( + wallet: &mut T, + parent_key_id: &Identifier, + minimum_confirmations: u64, +) -> Result +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let current_height = wallet.last_confirmed_height()?; + let outputs = wallet + .iter() + .filter(|out| out.root_key_id == *parent_key_id); + + let mut unspent_total = 0; + let mut immature_total = 0; + let mut awaiting_finalization_total = 0; + let mut unconfirmed_total = 0; + let mut locked_total = 0; + let mut reverted_total = 0; + + for out in outputs { + match out.status { + OutputStatus::Unspent => { + if out.is_coinbase && out.lock_height > current_height { + immature_total += out.value; + } else if out.num_confirmations(current_height) < minimum_confirmations { + // Treat anything less than minimum confirmations as "unconfirmed". + unconfirmed_total += out.value; + } else { + unspent_total += out.value; + } + } + OutputStatus::Unconfirmed => { + // We ignore unconfirmed coinbase outputs completely. + if !out.is_coinbase { + if minimum_confirmations == 0 { + unconfirmed_total += out.value; + } else { + awaiting_finalization_total += out.value; + } + } + } + OutputStatus::Locked => { + locked_total += out.value; + } + OutputStatus::Reverted => reverted_total += out.value, + OutputStatus::Spent => {} + } + } + + Ok(WalletInfo { + last_confirmed_height: current_height, + minimum_confirmations, + total: unspent_total + unconfirmed_total + immature_total, + amount_awaiting_finalization: awaiting_finalization_total, + amount_awaiting_confirmation: unconfirmed_total, + amount_immature: immature_total, + amount_locked: locked_total, + amount_currently_spendable: unspent_total, + amount_reverted: reverted_total, + }) +} + +/// Build a coinbase output and insert into wallet +pub fn build_coinbase<'a, T: ?Sized, C, K>( + wallet: &mut T, + keychain_mask: Option<&SecretKey>, + block_fees: &BlockFees, + test_mode: bool, +) -> Result +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let (out, kern, block_fees) = receive_coinbase(wallet, keychain_mask, block_fees, test_mode)?; + + Ok(CbData { + output: out, + kernel: kern, + key_id: block_fees.key_id, + }) +} + +//TODO: Split up the output creation and the wallet insertion +/// Build a coinbase output and the corresponding kernel +pub fn receive_coinbase<'a, T: ?Sized, C, K>( + wallet: &mut T, + keychain_mask: Option<&SecretKey>, + block_fees: &BlockFees, + test_mode: bool, +) -> Result<(Output, TxKernel, BlockFees), Error> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let height = block_fees.height; + let lock_height = height + global::coinbase_maturity(); + let key_id = block_fees.key_id(); + let parent_key_id = wallet.parent_key_id(); + + let key_id = match key_id { + Some(key_id) => match keys::retrieve_existing_key(wallet, key_id, None) { + Ok(k) => k.0, + Err(_) => keys::next_available_key(wallet, keychain_mask)?, + }, + None => keys::next_available_key(wallet, keychain_mask)?, + }; + + { + // Now acquire the wallet lock and write the new output. + let amount = reward(block_fees.fees); + let commit = wallet.calc_commit_for_cache(keychain_mask, amount, &key_id)?; + let mut batch = wallet.batch(keychain_mask)?; + batch.save(OutputData { + root_key_id: parent_key_id, + key_id: key_id.clone(), + n_child: key_id.to_path().last_path_index(), + mmr_index: None, + commit: commit, + value: amount, + status: OutputStatus::Unconfirmed, + height: height, + lock_height: lock_height, + is_coinbase: true, + tx_log_entry: None, + })?; + batch.commit()?; + } + + debug!( + "receive_coinbase: built candidate output - {:?}, {}", + key_id.clone(), + key_id, + ); + + let mut block_fees = block_fees.clone(); + block_fees.key_id = Some(key_id.clone()); + + debug!("receive_coinbase: {:?}", block_fees); + + let keychain = wallet.keychain(keychain_mask)?; + let (out, kern) = reward::output( + &keychain, + &ProofBuilder::new(&keychain), + &key_id, + block_fees.fees, + test_mode, + )?; + Ok((out, kern, block_fees)) +} diff --git a/libwallet/src/lib.rs b/libwallet/src/lib.rs new file mode 100644 index 0000000..95185c5 --- /dev/null +++ b/libwallet/src/lib.rs @@ -0,0 +1,90 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Higher level wallet functions which can be used by callers to operate +//! on the wallet, as well as helpers to invoke and instantiate wallets +//! and listeners + +#![deny(non_upper_case_globals)] +#![deny(non_camel_case_types)] +#![deny(non_snake_case)] +#![deny(unused_mut)] +#![warn(missing_docs)] + +use grin_wallet_config as config; + +use grin_core; +use grin_keychain; +use grin_util; + +use grin_wallet_util as util; + +use blake2_rfc as blake2; + +#[macro_use] +extern crate serde_derive; +#[macro_use] +extern crate log; +#[macro_use] +extern crate lazy_static; + +extern crate strum; +#[macro_use] +extern crate strum_macros; + +pub mod address; +pub mod api_impl; +mod error; +mod internal; +pub mod mwixnet; +mod slate; +pub mod slate_versions; +pub mod slatepack; +mod types; + +pub use crate::error::Error; +pub use crate::slate::{ParticipantData, Slate, SlateState}; +pub use crate::slate_versions::v4::sig_is_blank; +pub use crate::slate_versions::{ + SlateVersion, VersionedBinSlate, VersionedCoinbase, VersionedSlate, CURRENT_SLATE_VERSION, + GRIN_BLOCK_HEADER_VERSION, +}; +pub use crate::slatepack::{ + Slatepack, SlatepackAddress, SlatepackArmor, SlatepackBin, Slatepacker, SlatepackerArgs, +}; +pub use api_impl::owner_updater::StatusMessage; +pub use api_impl::types::{ + Amount, BlockFees, BuiltOutput, InitTxArgs, InitTxSendArgs, IssueInvoiceTxArgs, + NodeHeightResult, OutputCommitMapping, PaymentProof, RetrieveTxQueryArgs, + RetrieveTxQuerySortField, RetrieveTxQuerySortOrder, VersionInfo, +}; +pub use internal::scan::scan; +pub use slate_versions::ser as dalek_ser; +pub use types::{ + AcctPathMapping, BlockIdentifier, CbData, Context, NodeClient, NodeVersionInfo, OutputData, + OutputStatus, ScannedBlockInfo, StoredProofInfo, TxLogEntry, TxLogEntryType, TxWrapper, + ViewWallet, WalletBackend, WalletInfo, WalletInitStatus, WalletInst, WalletLCProvider, + WalletOutputBatch, +}; + +/// Helper for taking a lock on the wallet instance +#[macro_export] +macro_rules! wallet_lock { + ($wallet_inst: expr, $wallet: ident) => { + let inst = $wallet_inst.clone(); + let mut w_lock = inst.lock(); + let w_provider = w_lock.lc_provider()?; + let $wallet = w_provider.wallet_inst()?; + }; +} diff --git a/libwallet/src/mwixnet/mod.rs b/libwallet/src/mwixnet/mod.rs new file mode 100644 index 0000000..0605265 --- /dev/null +++ b/libwallet/src/mwixnet/mod.rs @@ -0,0 +1,24 @@ +// Copyright 2023 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Onion modules for mxmixnet +mod onion; +mod types; + +pub use onion::{ + create_onion, onion::Onion, onion::OnionError, util as onion_util, ComSigError, ComSignature, + MwixnetPublicKey, +}; + +pub use types::{Hop, MixnetReqCreationParams, SwapReq}; diff --git a/libwallet/src/mwixnet/onion/crypto/comsig.rs b/libwallet/src/mwixnet/onion/crypto/comsig.rs new file mode 100644 index 0000000..bf9e347 --- /dev/null +++ b/libwallet/src/mwixnet/onion/crypto/comsig.rs @@ -0,0 +1,230 @@ +// Copyright 2023 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Comsig modules for mxmixnet + +use grin_util::secp::{ + self as secp256k1zkp, pedersen::Commitment, rand::thread_rng, ContextFlag, Secp256k1, SecretKey, +}; + +use blake2_rfc::blake2b::Blake2b; +use byteorder::{BigEndian, ByteOrder}; +use grin_core::ser::{self, Readable, Reader, Writeable, Writer}; +use rand::rngs::mock::StepRng; +use thiserror::Error; + +/// A generalized Schnorr signature with a pedersen commitment value & blinding factors as the keys +#[derive(Clone, Debug)] +pub struct ComSignature { + pub_nonce: Commitment, + s: SecretKey, + t: SecretKey, +} + +/// Error types for Commitment Signatures +#[derive(Error, Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] +pub enum ComSigError { + /// Invalid commitment signature + #[error("Commitment signature is invalid")] + InvalidSig, + /// Secp256k1zkp error + #[error("Secp256k1zkp error: {0:?}")] + Secp256k1zkp(secp256k1zkp::Error), +} + +impl From for ComSigError { + fn from(err: secp256k1zkp::Error) -> ComSigError { + ComSigError::Secp256k1zkp(err) + } +} + +impl ComSignature { + /// Create a new ComSignature + pub fn new(pub_nonce: &Commitment, s: &SecretKey, t: &SecretKey) -> ComSignature { + ComSignature { + pub_nonce: pub_nonce.to_owned(), + s: s.to_owned(), + t: t.to_owned(), + } + } + + /// Sign commitment with amount, blinding factor, and message + pub fn sign( + amount: u64, + blind: &SecretKey, + msg: &Vec, + use_test_rng: bool, + ) -> Result { + let secp = Secp256k1::with_caps(ContextFlag::Commit); + + let mut amt_bytes = [0; 32]; + BigEndian::write_u64(&mut amt_bytes[24..32], amount); + let k_amt = SecretKey::from_slice(&secp, &amt_bytes)?; + let k_1; + let k_2; + + if use_test_rng { + // allow for consistent test results + let mut test_rng = StepRng::new(1_234_567_890_u64, 1); + k_1 = SecretKey::new(&secp, &mut test_rng); + k_2 = SecretKey::new(&secp, &mut test_rng); + } else { + k_1 = SecretKey::new(&secp, &mut thread_rng()); + k_2 = SecretKey::new(&secp, &mut thread_rng()); + } + + let commitment = secp.commit(amount, blind.clone())?; + let nonce_commitment = secp.commit_blind(k_1.clone(), k_2.clone())?; + + let e = ComSignature::calc_challenge( + &secp, + &commitment, + &nonce_commitment, + &msg, + use_test_rng, + )?; + + // s = k_1 + (e * amount) + let mut s = k_amt.clone(); + s.mul_assign(&secp, &e)?; + s.add_assign(&secp, &k_1)?; + + // t = k_2 + (e * blind) + let mut t = blind.clone(); + t.mul_assign(&secp, &e)?; + t.add_assign(&secp, &k_2)?; + + Ok(ComSignature::new(&nonce_commitment, &s, &t)) + } + + /// Verify the commitment signature + pub fn verify(&self, commit: &Commitment, msg: &Vec) -> Result<(), ComSigError> { + let secp = Secp256k1::with_caps(ContextFlag::Commit); + + let s1 = secp.commit_blind(self.s.clone(), self.t.clone())?; + + let mut ce = commit.to_pubkey(&secp)?; + let e = ComSignature::calc_challenge(&secp, &commit, &self.pub_nonce, &msg, false)?; + ce.mul_assign(&secp, &e)?; + + let commits = vec![Commitment::from_pubkey(&secp, &ce)?, self.pub_nonce.clone()]; + let s2 = secp.commit_sum(commits, Vec::new())?; + + if s1 != s2 { + return Err(ComSigError::InvalidSig); + } + + Ok(()) + } + + fn calc_challenge( + secp: &Secp256k1, + commit: &Commitment, + nonce_commit: &Commitment, + msg: &Vec, + use_test_rng: bool, + ) -> Result { + let mut challenge_hasher = Blake2b::new(32); + if use_test_rng { + return Ok(super::secp::random_secret(use_test_rng)); + } + challenge_hasher.update(&commit.0); + challenge_hasher.update(&nonce_commit.0); + challenge_hasher.update(msg); + + let mut challenge = [0; 32]; + challenge.copy_from_slice(challenge_hasher.finalize().as_bytes()); + + Ok(SecretKey::from_slice(&secp, &challenge)?) + } +} + +/// Serializes a ComSignature to and from hex +pub mod comsig_serde { + use super::ComSignature; + use grin_core::ser::{self, ProtocolVersion}; + use grin_util::ToHex; + use serde::{Deserialize, Serializer}; + + /// Serializes a ComSignature as a hex string + pub fn serialize(comsig: &ComSignature, serializer: S) -> Result + where + S: Serializer, + { + use serde::ser::Error; + let bytes = ser::ser_vec(&comsig, ProtocolVersion::local()).map_err(Error::custom)?; + serializer.serialize_str(&bytes.to_hex()) + } + + /// Creates a ComSignature from a hex string + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + use serde::de::Error; + let bytes = String::deserialize(deserializer) + .and_then(|string| grin_util::from_hex(&string).map_err(Error::custom))?; + let sig: ComSignature = ser::deserialize_default(&mut &bytes[..]).map_err(Error::custom)?; + Ok(sig) + } +} + +#[allow(non_snake_case)] +impl Readable for ComSignature { + fn read(reader: &mut R) -> Result { + let R = Commitment::read(reader)?; + let s = super::secp::read_secret_key(reader)?; + let t = super::secp::read_secret_key(reader)?; + Ok(ComSignature::new(&R, &s, &t)) + } +} + +impl Writeable for ComSignature { + fn write(&self, writer: &mut W) -> Result<(), ser::Error> { + writer.write_fixed_bytes(self.pub_nonce.0)?; + writer.write_fixed_bytes(self.s.0)?; + writer.write_fixed_bytes(self.t.0)?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::{ComSigError, ComSignature, ContextFlag, Secp256k1, SecretKey}; + + use grin_util::secp::rand::{thread_rng, RngCore}; + use rand::Rng; + + /// Test signing and verification of ComSignatures + #[test] + fn verify_comsig() -> Result<(), ComSigError> { + let secp = Secp256k1::with_caps(ContextFlag::Commit); + + let amount = thread_rng().next_u64(); + let blind = SecretKey::new(&secp, &mut thread_rng()); + let msg: [u8; 16] = rand::thread_rng().gen(); + let comsig = ComSignature::sign(amount, &blind, &msg.to_vec(), false)?; + + let commit = secp.commit(amount, blind.clone())?; + assert!(comsig.verify(&commit, &msg.to_vec()).is_ok()); + + let wrong_msg: [u8; 16] = rand::thread_rng().gen(); + assert!(comsig.verify(&commit, &wrong_msg.to_vec()).is_err()); + + let wrong_commit = secp.commit(amount, SecretKey::new(&secp, &mut thread_rng()))?; + assert!(comsig.verify(&wrong_commit, &msg.to_vec()).is_err()); + + Ok(()) + } +} diff --git a/libwallet/src/mwixnet/onion/crypto/dalek.rs b/libwallet/src/mwixnet/onion/crypto/dalek.rs new file mode 100644 index 0000000..a10c65b --- /dev/null +++ b/libwallet/src/mwixnet/onion/crypto/dalek.rs @@ -0,0 +1,297 @@ +// Copyright 2023 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Dalek key wrapper for mwixnet primitives + +use grin_util::secp::key::SecretKey; + +use ed25519_dalek::{PublicKey, Signature, Verifier}; +use grin_core::ser::{self, Readable, Reader, Writeable, Writer}; +use grin_util::ToHex; +use thiserror::Error; + +/// Error types for Dalek structures and logic +#[derive(Clone, Error, Debug, PartialEq)] +pub enum DalekError { + /// Hex deser error + #[error("Hex error {0:?}")] + HexError(String), + /// Key parsing error + #[error("Failed to parse secret key")] + KeyParseError, + /// Error validating signature + #[error("Failed to verify signature")] + SigVerifyFailed, +} + +/// Encapsulates an ed25519_dalek::PublicKey and provides (de-)serialization +#[derive(Clone, Debug, PartialEq)] +pub struct DalekPublicKey(PublicKey); + +impl DalekPublicKey { + /// Convert DalekPublicKey to hex string + pub fn to_hex(&self) -> String { + self.0.to_hex() + } + + /// Convert hex string to DalekPublicKey. + pub fn from_hex(hex: &str) -> Result { + let bytes = grin_util::from_hex(hex) + .map_err(|_| DalekError::HexError(format!("failed to decode {}", hex)))?; + let pk = PublicKey::from_bytes(bytes.as_ref()) + .map_err(|_| DalekError::HexError(format!("failed to decode {}", hex)))?; + Ok(DalekPublicKey(pk)) + } + + /// Compute DalekPublicKey from a SecretKey + pub fn from_secret(key: &SecretKey) -> Self { + let secret = ed25519_dalek::SecretKey::from_bytes(&key.0).unwrap(); + let pk: PublicKey = (&secret).into(); + DalekPublicKey(pk) + } +} + +impl AsRef for DalekPublicKey { + fn as_ref(&self) -> &PublicKey { + &self.0 + } +} + +#[cfg(test)] +/// Serializes an Option to and from hex +pub mod option_dalek_pubkey_serde { + use super::DalekPublicKey; + use grin_util::ToHex; + use serde::de::Error; + use serde::{Deserialize, Deserializer, Serializer}; + + /// + pub fn serialize(pk: &Option, serializer: S) -> Result + where + S: Serializer, + { + match pk { + Some(pk) => serializer.serialize_str(&pk.0.to_hex()), + None => serializer.serialize_none(), + } + } + + /// + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + Option::::deserialize(deserializer).and_then(|res| match res { + Some(string) => DalekPublicKey::from_hex(&string) + .map_err(|e| Error::custom(e.to_string())) + .and_then(|pk: DalekPublicKey| Ok(Some(pk))), + None => Ok(None), + }) + } +} + +impl Readable for DalekPublicKey { + fn read(reader: &mut R) -> Result { + let pk = PublicKey::from_bytes(&reader.read_fixed_bytes(32)?) + .map_err(|_| ser::Error::CorruptedData)?; + Ok(DalekPublicKey(pk)) + } +} + +impl Writeable for DalekPublicKey { + fn write(&self, writer: &mut W) -> Result<(), ser::Error> { + writer.write_fixed_bytes(self.0.to_bytes())?; + Ok(()) + } +} + +/// Encapsulates an ed25519_dalek::Signature and provides (de-)serialization +#[derive(Clone, Debug, PartialEq)] +pub struct DalekSignature(Signature); + +impl DalekSignature { + /// Convert hex string to DalekSignature. + pub fn from_hex(hex: &str) -> Result { + let bytes = grin_util::from_hex(hex) + .map_err(|_| DalekError::HexError(format!("failed to decode {}", hex)))?; + let sig = Signature::from_bytes(bytes.as_ref()) + .map_err(|_| DalekError::HexError(format!("failed to decode {}", hex)))?; + Ok(DalekSignature(sig)) + } + + /// Verifies DalekSignature + pub fn verify(&self, pk: &DalekPublicKey, msg: &[u8]) -> Result<(), DalekError> { + pk.as_ref() + .verify(&msg, &self.0) + .map_err(|_| DalekError::SigVerifyFailed) + } +} + +impl AsRef for DalekSignature { + fn as_ref(&self) -> &Signature { + &self.0 + } +} + +/// Serializes a DalekSignature to and from hex +#[cfg(test)] +pub mod dalek_sig_serde { + use super::DalekSignature; + use grin_util::ToHex; + use serde::de::Error; + use serde::{Deserialize, Deserializer, Serializer}; + + /// + pub fn serialize(sig: &DalekSignature, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&sig.0.to_hex()) + } + + /// + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let str = String::deserialize(deserializer)?; + let sig = DalekSignature::from_hex(&str).map_err(|e| Error::custom(e.to_string()))?; + Ok(sig) + } +} + +/// Dalek signature sign wrapper +// TODO: This is likely duplicated throughout crate, check +#[cfg(test)] +pub fn sign(sk: &SecretKey, message: &[u8]) -> Result { + use ed25519_dalek::{Keypair, Signer}; + let secret = + ed25519_dalek::SecretKey::from_bytes(&sk.0).map_err(|_| DalekError::KeyParseError)?; + let public: PublicKey = (&secret).into(); + let keypair = Keypair { secret, public }; + let sig = keypair.sign(&message); + Ok(DalekSignature(sig)) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mwixnet::onion::test_util::rand_keypair; + use grin_core::ser::{self, ProtocolVersion}; + use grin_util::ToHex; + use rand::Rng; + use serde::{Deserialize, Serialize}; + use serde_json::Value; + + #[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] + struct TestPubKeySerde { + #[serde(with = "option_dalek_pubkey_serde", default)] + pk: Option, + } + + #[test] + fn pubkey_test() -> Result<(), Box> { + // Test from_hex + let rand_pk = rand_keypair().1; + let pk_from_hex = DalekPublicKey::from_hex(rand_pk.0.to_hex().as_str()).unwrap(); + assert_eq!(rand_pk.0, pk_from_hex.0); + + // Test ser (de-)serialization + let bytes = ser::ser_vec(&rand_pk, ProtocolVersion::local()).unwrap(); + assert_eq!(bytes.len(), 32); + let pk_from_deser: DalekPublicKey = ser::deserialize_default(&mut &bytes[..]).unwrap(); + assert_eq!(rand_pk.0, pk_from_deser.0); + + // Test serde with Some(rand_pk) + let some = TestPubKeySerde { + pk: Some(rand_pk.clone()), + }; + let val = serde_json::to_value(some.clone()).unwrap(); + if let Value::Object(o) = &val { + if let Value::String(s) = o.get("pk").unwrap() { + assert_eq!(s, &rand_pk.0.to_hex()); + } else { + panic!("Invalid type"); + } + } else { + panic!("Invalid type") + } + assert_eq!(some, serde_json::from_value(val).unwrap()); + + // Test serde with empty pk field + let none = TestPubKeySerde { pk: None }; + let val = serde_json::to_value(none.clone()).unwrap(); + if let Value::Object(o) = &val { + if let Value::Null = o.get("pk").unwrap() { + // ok + } else { + panic!("Invalid type"); + } + } else { + panic!("Invalid type") + } + assert_eq!(none, serde_json::from_value(val).unwrap()); + + // Test serde with no pk field + let none2 = serde_json::from_str::("{}").unwrap(); + assert_eq!(none, none2); + + Ok(()) + } + + #[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] + struct TestSigSerde { + #[serde(with = "dalek_sig_serde")] + sig: DalekSignature, + } + + #[test] + fn sig_test() -> Result<(), Box> { + // Sign a message + let (sk, pk) = rand_keypair(); + let msg: [u8; 16] = rand::thread_rng().gen(); + let sig = sign(&sk, &msg).unwrap(); + + // Verify signature + assert!(sig.verify(&pk, &msg).is_ok()); + + // Wrong message + let wrong_msg: [u8; 16] = rand::thread_rng().gen(); + assert!(sig.verify(&pk, &wrong_msg).is_err()); + + // Wrong pubkey + let wrong_pk = rand_keypair().1; + assert!(sig.verify(&wrong_pk, &msg).is_err()); + + // Test from_hex + let sig_from_hex = DalekSignature::from_hex(sig.0.to_hex().as_str()).unwrap(); + assert_eq!(sig.0, sig_from_hex.0); + + // Test serde (de-)serialization + let serde_test = TestSigSerde { sig: sig.clone() }; + let val = serde_json::to_value(serde_test.clone()).unwrap(); + if let Value::Object(o) = &val { + if let Value::String(s) = o.get("sig").unwrap() { + assert_eq!(s, &sig.0.to_hex()); + } else { + panic!("Invalid type"); + } + } else { + panic!("Invalid type") + } + assert_eq!(serde_test, serde_json::from_value(val).unwrap()); + + Ok(()) + } +} diff --git a/libwallet/src/mwixnet/onion/crypto/mod.rs b/libwallet/src/mwixnet/onion/crypto/mod.rs new file mode 100644 index 0000000..58544e9 --- /dev/null +++ b/libwallet/src/mwixnet/onion/crypto/mod.rs @@ -0,0 +1,21 @@ +// Copyright 2024 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Onion and comsig modules for mxmixnet + +pub mod comsig; +pub mod dalek; +pub mod secp; + +pub use comsig::{comsig_serde, ComSigError, ComSignature}; diff --git a/libwallet/src/mwixnet/onion/crypto/secp.rs b/libwallet/src/mwixnet/onion/crypto/secp.rs new file mode 100644 index 0000000..eda6abe --- /dev/null +++ b/libwallet/src/mwixnet/onion/crypto/secp.rs @@ -0,0 +1,98 @@ +// Copyright 2024 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! SECP operations for comsig + +pub use grin_util::secp::{ + self as secp256k1zkp, + constants::SECRET_KEY_SIZE, + key::{SecretKey, ZERO_KEY}, + pedersen::Commitment, + rand::thread_rng, + ContextFlag, Secp256k1, +}; + +use grin_core::ser::{self, Reader}; +use rand::rngs::mock::StepRng; + +/// Generate a random SecretKey. +pub fn random_secret(use_test_rng: bool) -> SecretKey { + let secp = Secp256k1::new(); + if use_test_rng { + // allow for consistent test results + let mut test_rng = StepRng::new(1_234_567_890_u64, 1); + SecretKey::new(&secp, &mut test_rng) + } else { + SecretKey::new(&secp, &mut thread_rng()) + } +} + +/// Deserialize a SecretKey from a Reader +pub fn read_secret_key(reader: &mut R) -> Result { + let buf = reader.read_fixed_bytes(SECRET_KEY_SIZE)?; + let secp = Secp256k1::with_caps(ContextFlag::None); + let pk = SecretKey::from_slice(&secp, &buf).map_err(|_| ser::Error::CorruptedData)?; + Ok(pk) +} + +/// Build a Pedersen Commitment using the provided value and blinding factor +#[cfg(test)] +pub fn commit(value: u64, blind: &SecretKey) -> Result { + let secp = Secp256k1::with_caps(ContextFlag::Commit); + let commit = secp.commit(value, blind.clone())?; + Ok(commit) +} + +/// Add a blinding factor to an existing Commitment +pub fn add_excess( + commitment: &Commitment, + excess: &SecretKey, +) -> Result { + let secp = Secp256k1::with_caps(ContextFlag::Commit); + let excess_commit: Commitment = secp.commit(0, excess.clone())?; + + let commits = vec![commitment.clone(), excess_commit.clone()]; + let sum = secp.commit_sum(commits, Vec::new())?; + Ok(sum) +} + +/// Subtracts a value (v*H) from an existing commitment +pub fn sub_value(commitment: &Commitment, value: u64) -> Result { + let secp = Secp256k1::with_caps(ContextFlag::Commit); + let neg_commit: Commitment = secp.commit(value, ZERO_KEY)?; + let sum = secp.commit_sum(vec![commitment.clone()], vec![neg_commit.clone()])?; + Ok(sum) +} + +/// Signs the message with the provided SecretKey +#[cfg(test)] +#[allow(dead_code)] +pub fn sign( + sk: &SecretKey, + msg: &grin_util::secp::Message, +) -> Result { + let secp = Secp256k1::with_caps(ContextFlag::Full); + let pubkey = grin_util::secp::PublicKey::from_secret_key(&secp, &sk)?; + let sig = grin_util::secp::aggsig::sign_single( + &secp, + &msg, + &sk, + None, + None, + None, + Some(&pubkey), + None, + )?; + Ok(sig) +} diff --git a/libwallet/src/mwixnet/onion/mod.rs b/libwallet/src/mwixnet/onion/mod.rs new file mode 100644 index 0000000..d052d1c --- /dev/null +++ b/libwallet/src/mwixnet/onion/mod.rs @@ -0,0 +1,207 @@ +// Copyright 2024 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Onion module definition + +mod crypto; +pub mod onion; +pub mod util; + +pub use crypto::{ + comsig_serde, dalek::DalekPublicKey as MwixnetPublicKey, ComSigError, ComSignature, +}; + +use chacha20::cipher::StreamCipher; +use grin_core::core::FeeFields; +use grin_util::secp::{ + pedersen::{Commitment, RangeProof}, + SecretKey, +}; +use x25519_dalek::PublicKey as xPublicKey; +use x25519_dalek::{SharedSecret, StaticSecret}; + +use crypto::secp::random_secret; +use onion::{new_stream_cipher, Onion, OnionError, Payload, RawBytes}; + +/// Onion hop struct +#[derive(Clone)] +pub struct Hop { + /// Comsig server public key + pub server_pubkey: xPublicKey, + /// Kernel excess + pub excess: SecretKey, + /// Fee + pub fee: FeeFields, + /// Rangeproof + pub rangeproof: Option, +} + +/// Crate a new hop +#[cfg(test)] +pub fn new_hop( + server_key: &SecretKey, + hop_excess: &SecretKey, + fee: u32, + proof: Option, +) -> Hop { + Hop { + server_pubkey: xPublicKey::from(&StaticSecret::from(server_key.0.clone())), + excess: hop_excess.clone(), + fee: FeeFields::from(fee as u32), + rangeproof: proof, + } +} + +/// Create an Onion for the Commitment, encrypting the payload for each hop +pub fn create_onion( + commitment: &Commitment, + hops: &Vec, + use_test_rng: bool, +) -> Result { + if hops.is_empty() { + return Ok(Onion { + ephemeral_pubkey: xPublicKey::from([0u8; 32]), + commit: commitment.clone(), + enc_payloads: vec![], + }); + } + + let mut shared_secrets: Vec = Vec::new(); + let mut enc_payloads: Vec = Vec::new(); + let mut ephemeral_sk = StaticSecret::from(random_secret(use_test_rng).0); + let onion_ephemeral_pk = xPublicKey::from(&ephemeral_sk); + for i in 0..hops.len() { + let hop = &hops[i]; + let shared_secret = ephemeral_sk.diffie_hellman(&hop.server_pubkey); + shared_secrets.push(shared_secret); + + ephemeral_sk = StaticSecret::from(random_secret(use_test_rng).0); + let next_ephemeral_pk = if i < (hops.len() - 1) { + xPublicKey::from(&ephemeral_sk) + } else { + xPublicKey::from([0u8; 32]) + }; + + let payload = Payload { + next_ephemeral_pk, + excess: hop.excess.clone(), + fee: hop.fee.clone(), + rangeproof: hop.rangeproof.clone(), + }; + enc_payloads.push(payload.serialize()?); + } + + for i in (0..shared_secrets.len()).rev() { + let mut cipher = new_stream_cipher(&shared_secrets[i])?; + for j in i..shared_secrets.len() { + cipher.apply_keystream(&mut enc_payloads[j]); + } + } + + let onion = Onion { + ephemeral_pubkey: onion_ephemeral_pk, + commit: commitment.clone(), + enc_payloads, + }; + Ok(onion) +} + +/// Internal tests +#[allow(missing_docs, dead_code)] +#[cfg(test)] +pub mod test_util { + use super::*; + use crypto::dalek::DalekPublicKey; + use crypto::secp; + + use grin_core::core::hash::Hash; + use grin_util::secp::Secp256k1; + use grin_util::ToHex; + use rand::{thread_rng, RngCore}; + + pub fn rand_onion() -> Onion { + let commit = rand_commit(); + let mut hops = Vec::new(); + let k = (thread_rng().next_u64() % 5) + 1; + for i in 0..k { + let rangeproof = if i == (k - 1) { + Some(rand_proof()) + } else { + None + }; + let hop = new_hop( + &random_secret(false), + &random_secret(false), + thread_rng().next_u32(), + rangeproof, + ); + hops.push(hop); + } + + create_onion(&commit, &hops, false).unwrap() + } + + pub fn rand_commit() -> Commitment { + secp::commit(rand::thread_rng().next_u64(), &secp::random_secret(false)).unwrap() + } + + pub fn rand_hash() -> Hash { + Hash::from_hex(secp::random_secret(false).to_hex().as_str()).unwrap() + } + + pub fn rand_proof() -> RangeProof { + let secp = Secp256k1::new(); + secp.bullet_proof( + rand::thread_rng().next_u64(), + secp::random_secret(false), + secp::random_secret(false), + secp::random_secret(false), + None, + None, + ) + } + + pub fn proof( + value: u64, + fee: u32, + input_blind: &SecretKey, + hop_excesses: &Vec<&SecretKey>, + ) -> (Commitment, RangeProof) { + let secp = Secp256k1::new(); + + let mut blind = input_blind.clone(); + for hop_excess in hop_excesses { + blind.add_assign(&secp, &hop_excess).unwrap(); + } + + let out_value = value - (fee as u64); + + let rp = secp.bullet_proof( + out_value, + blind.clone(), + secp::random_secret(false), + secp::random_secret(false), + None, + None, + ); + + (secp::commit(out_value, &blind).unwrap(), rp) + } + + pub fn rand_keypair() -> (SecretKey, DalekPublicKey) { + let sk = random_secret(false); + let pk = DalekPublicKey::from_secret(&sk); + (sk, pk) + } +} diff --git a/libwallet/src/mwixnet/onion/onion.rs b/libwallet/src/mwixnet/onion/onion.rs new file mode 100644 index 0000000..2540dae --- /dev/null +++ b/libwallet/src/mwixnet/onion/onion.rs @@ -0,0 +1,438 @@ +// Copyright 2023 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Onion defn for mwixnet + +use super::util::{read_optional, vec_to_array, write_optional}; + +use std::convert::TryFrom; +use std::fmt; +use std::hash::{Hash, Hasher}; +use std::result::Result; + +use chacha20::cipher::{NewCipher, StreamCipher}; +use chacha20::{ChaCha20, Key, Nonce}; +use grin_core::core::FeeFields; +use grin_core::ser::{self, Readable, Reader, Writeable, Writer}; +use grin_util::secp::{ + self as secp256k1zkp, + key::SecretKey, + pedersen::{Commitment, RangeProof}, +}; +use grin_util::{self, ToHex}; +use hmac::digest::InvalidLength; +use hmac::{Hmac, Mac}; +use serde::ser::SerializeStruct; +use serde::Deserialize; +use sha2::Sha256; +use thiserror::Error; +use x25519_dalek::{PublicKey as xPublicKey, SharedSecret, StaticSecret}; + +use super::crypto::secp; + +type HmacSha256 = Hmac; +/// Raw bytes alias +pub type RawBytes = Vec; + +const CURRENT_ONION_VERSION: u8 = 0; + +/// A data packet with layers of encryption +#[derive(Clone, Debug)] +pub struct Onion { + /// The onion originator's portion of the shared secret + pub ephemeral_pubkey: xPublicKey, + /// The pedersen commitment before adjusting the excess and subtracting the fee + pub commit: Commitment, + /// The encrypted payloads which represent the layers of the onion + pub enc_payloads: Vec, +} + +impl PartialEq for Onion { + fn eq(&self, other: &Onion) -> bool { + *self.ephemeral_pubkey.as_bytes() == *other.ephemeral_pubkey.as_bytes() + && self.commit == other.commit + && self.enc_payloads == other.enc_payloads + } +} + +impl Eq for Onion {} + +impl Hash for Onion { + fn hash(&self, state: &mut H) { + state.write(self.ephemeral_pubkey.as_bytes()); + state.write(self.commit.as_ref()); + state.write_usize(self.enc_payloads.len()); + for p in &self.enc_payloads { + state.write(p.as_slice()); + } + } +} + +/// A single, decrypted/peeled layer of an Onion. +#[derive(Debug, Clone)] +pub struct Payload { + /// PK of next server + pub next_ephemeral_pk: xPublicKey, + /// Excess calculation + pub excess: SecretKey, + /// Fee + pub fee: FeeFields, + /// Rangeproof + pub rangeproof: Option, +} + +impl Payload { + /// Deserialize + pub fn deserialize(bytes: &Vec) -> Result { + let payload: Payload = ser::deserialize_default(&mut &bytes[..])?; + Ok(payload) + } + + /// Serialize + pub fn serialize(&self) -> Result, ser::Error> { + let mut vec = vec![]; + ser::serialize_default(&mut vec, &self)?; + Ok(vec) + } +} + +impl Readable for Payload { + fn read(reader: &mut R) -> Result { + let version = reader.read_u8()?; + if version != CURRENT_ONION_VERSION { + return Err(ser::Error::UnsupportedProtocolVersion); + } + + let next_ephemeral_pk = + xPublicKey::from(vec_to_array::<32>(&reader.read_fixed_bytes(32)?)?); + let excess = secp::read_secret_key(reader)?; + let fee = FeeFields::try_from(reader.read_u64()?).map_err(|_| ser::Error::CorruptedData)?; + let rangeproof = read_optional(reader)?; + Ok(Payload { + next_ephemeral_pk, + excess, + fee, + rangeproof, + }) + } +} + +impl Writeable for Payload { + fn write(&self, writer: &mut W) -> Result<(), ser::Error> { + writer.write_u8(CURRENT_ONION_VERSION)?; + writer.write_fixed_bytes(&self.next_ephemeral_pk.as_bytes())?; + writer.write_fixed_bytes(&self.excess)?; + writer.write_u64(self.fee.into())?; + write_optional(writer, &self.rangeproof)?; + Ok(()) + } +} + +/// An onion with a layer decrypted +#[derive(Clone, Debug)] +pub struct PeeledOnion { + /// The payload from the peeled layer + pub payload: Payload, + /// The onion remaining after a layer was peeled + pub onion: Onion, +} + +impl Onion { + /// Serialize to binary + pub fn serialize(&self) -> Result, ser::Error> { + let mut vec = vec![]; + ser::serialize_default(&mut vec, &self)?; + Ok(vec) + } + + /// Peel a single layer off of the Onion, returning the peeled Onion and decrypted Payload + pub fn peel_layer(&self, server_key: &SecretKey) -> Result { + let shared_secret = StaticSecret::from(server_key.0).diffie_hellman(&self.ephemeral_pubkey); + let mut cipher = new_stream_cipher(&shared_secret)?; + + let mut decrypted_bytes = self.enc_payloads[0].clone(); + cipher.apply_keystream(&mut decrypted_bytes); + let decrypted_payload = Payload::deserialize(&decrypted_bytes) + .map_err(|e| OnionError::DeserializationError(e))?; + + let enc_payloads: Vec = self + .enc_payloads + .iter() + .enumerate() + .filter(|&(i, _)| i != 0) + .map(|(_, enc_payload)| { + let mut p = enc_payload.clone(); + cipher.apply_keystream(&mut p); + p + }) + .collect(); + + let mut commitment = self.commit.clone(); + commitment = secp::add_excess(&commitment, &decrypted_payload.excess) + .map_err(|e| OnionError::CalcCommitError(e))?; + commitment = secp::sub_value(&commitment, decrypted_payload.fee.into()) + .map_err(|e| OnionError::CalcCommitError(e))?; + + let peeled_onion = Onion { + ephemeral_pubkey: decrypted_payload.next_ephemeral_pk, + commit: commitment.clone(), + enc_payloads, + }; + Ok(PeeledOnion { + payload: decrypted_payload, + onion: peeled_onion, + }) + } +} + +/// Create a new stream cipher +pub fn new_stream_cipher(shared_secret: &SharedSecret) -> Result { + let mut mu_hmac = HmacSha256::new_from_slice(b"MWIXNET")?; + mu_hmac.update(shared_secret.as_bytes()); + let mukey = mu_hmac.finalize().into_bytes(); + + let key = Key::from_slice(&mukey[0..32]); + let nonce = Nonce::from_slice(b"NONCE1234567"); + + Ok(ChaCha20::new(&key, &nonce)) +} + +impl Writeable for Onion { + fn write(&self, writer: &mut W) -> Result<(), ser::Error> { + writer.write_fixed_bytes(self.ephemeral_pubkey.as_bytes())?; + writer.write_fixed_bytes(&self.commit)?; + writer.write_u64(self.enc_payloads.len() as u64)?; + for p in &self.enc_payloads { + writer.write_u64(p.len() as u64)?; + p.write(writer)?; + } + Ok(()) + } +} + +impl Readable for Onion { + fn read(reader: &mut R) -> Result { + let pubkey_bytes: [u8; 32] = vec_to_array(&reader.read_fixed_bytes(32)?)?; + let ephemeral_pubkey = xPublicKey::from(pubkey_bytes); + let commit = Commitment::read(reader)?; + let mut enc_payloads: Vec = Vec::new(); + let len = reader.read_u64()?; + for _ in 0..len { + let size = reader.read_u64()?; + let bytes = reader.read_fixed_bytes(size as usize)?; + enc_payloads.push(bytes); + } + Ok(Onion { + ephemeral_pubkey, + commit, + enc_payloads, + }) + } +} + +impl serde::ser::Serialize for Onion { + fn serialize(&self, serializer: S) -> Result + where + S: serde::ser::Serializer, + { + let mut state = serializer.serialize_struct("Onion", 3)?; + + state.serialize_field("pubkey", &self.ephemeral_pubkey.as_bytes().to_hex())?; + state.serialize_field("commit", &self.commit.to_hex())?; + + let hex_payloads: Vec = self.enc_payloads.iter().map(|v| v.to_hex()).collect(); + state.serialize_field("data", &hex_payloads)?; + state.end() + } +} + +impl<'de> serde::de::Deserialize<'de> for Onion { + fn deserialize(deserializer: D) -> Result + where + D: serde::de::Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(field_identifier, rename_all = "snake_case")] + enum Field { + Pubkey, + Commit, + Data, + } + + struct OnionVisitor; + + impl<'de> serde::de::Visitor<'de> for OnionVisitor { + type Value = Onion; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("an Onion") + } + + fn visit_map(self, mut map: A) -> Result + where + A: serde::de::MapAccess<'de>, + { + let mut pubkey = None; + let mut commit = None; + let mut data = None; + + while let Some(key) = map.next_key()? { + match key { + Field::Pubkey => { + let val: String = map.next_value()?; + let vec = + grin_util::from_hex(&val).map_err(serde::de::Error::custom)?; + pubkey = + Some(xPublicKey::from(vec_to_array::<32>(&vec).map_err( + |_| serde::de::Error::custom("Invalid length pubkey"), + )?)); + } + Field::Commit => { + let val: String = map.next_value()?; + let vec = + grin_util::from_hex(&val).map_err(serde::de::Error::custom)?; + commit = Some(Commitment::from_vec(vec)); + } + Field::Data => { + let val: Vec = map.next_value()?; + let mut vec: Vec> = Vec::new(); + for hex in val { + vec.push( + grin_util::from_hex(&hex).map_err(serde::de::Error::custom)?, + ); + } + data = Some(vec); + } + } + } + + Ok(Onion { + ephemeral_pubkey: pubkey.unwrap(), + commit: commit.unwrap(), + enc_payloads: data.unwrap(), + }) + } + } + + const FIELDS: &[&str] = &["pubkey", "commit", "data"]; + deserializer.deserialize_struct("Onion", &FIELDS, OnionVisitor) + } +} + +/// Error types for creating and peeling Onions +#[derive(Clone, Error, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub enum OnionError { + /// Invalid Key Length + #[error("Invalid key length for MAC initialization")] + InvalidKeyLength, + /// Serialization Error + #[error("Serialization error occurred: {0:?}")] + SerializationError(ser::Error), + /// Deserialization Error + #[error("Deserialization error occurred: {0:?}")] + DeserializationError(ser::Error), + /// Error calculating blinding factor + #[error("Error calculating blinding factor: {0:?}")] + CalcBlindError(secp256k1zkp::Error), + /// Error calculating ephemeral pubkey + #[error("Error calculating ephemeral pubkey: {0:?}")] + CalcPubKeyError(secp256k1zkp::Error), + /// Error calculating commit + #[error("Error calculating commitment: {0:?}")] + CalcCommitError(secp256k1zkp::Error), +} + +impl From for OnionError { + fn from(_err: InvalidLength) -> OnionError { + OnionError::InvalidKeyLength + } +} + +impl From for OnionError { + fn from(err: ser::Error) -> OnionError { + OnionError::SerializationError(err) + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + use crate::mwixnet::onion::crypto::secp::random_secret; + use crate::mwixnet::onion::{new_hop, Hop}; + + use grin_core::core::FeeFields; + + /// Test end-to-end Onion creation and unwrapping logic. + #[test] + fn onion() { + let total_fee: u64 = 10; + let fee_per_hop: u32 = 2; + let in_value: u64 = 1000; + let out_value: u64 = in_value - total_fee; + let blind = random_secret(false); + let commitment = secp::commit(in_value, &blind).unwrap(); + + let mut hops: Vec = Vec::new(); + let mut keys: Vec = Vec::new(); + let mut final_commit = secp::commit(out_value, &blind).unwrap(); + let mut final_blind = blind.clone(); + for i in 0..5 { + keys.push(random_secret(false)); + + let excess = random_secret(false); + + let secp = secp256k1zkp::Secp256k1::with_caps(secp256k1zkp::ContextFlag::Commit); + final_blind.add_assign(&secp, &excess).unwrap(); + final_commit = secp::add_excess(&final_commit, &excess).unwrap(); + let proof = if i == 4 { + let n1 = random_secret(false); + let rp = secp.bullet_proof( + out_value, + final_blind.clone(), + n1.clone(), + n1.clone(), + None, + None, + ); + assert!(secp.verify_bullet_proof(final_commit, rp, None).is_ok()); + Some(rp) + } else { + None + }; + + let hop = new_hop(&keys[i], &excess, fee_per_hop, proof); + hops.push(hop); + } + + let mut onion_packet = + crate::mwixnet::onion::create_onion(&commitment, &hops, false).unwrap(); + + let mut payload = Payload { + next_ephemeral_pk: onion_packet.ephemeral_pubkey.clone(), + excess: random_secret(false), + fee: FeeFields::from(fee_per_hop), + rangeproof: None, + }; + for i in 0..5 { + let peeled = onion_packet.peel_layer(&keys[i]).unwrap(); + payload = peeled.payload; + onion_packet = peeled.onion; + } + + assert!(payload.rangeproof.is_some()); + assert_eq!(payload.rangeproof.unwrap(), hops[4].rangeproof.unwrap()); + assert_eq!(secp::commit(out_value, &final_blind).unwrap(), final_commit); + assert_eq!(payload.fee, FeeFields::from(fee_per_hop)); + } +} diff --git a/libwallet/src/mwixnet/onion/util.rs b/libwallet/src/mwixnet/onion/util.rs new file mode 100644 index 0000000..6661177 --- /dev/null +++ b/libwallet/src/mwixnet/onion/util.rs @@ -0,0 +1,185 @@ +// Copyright 2023 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Util fns for mwixnet +//! TODO: possibly redundant, check or move elsewhere + +use grin_core::ser::{self, Readable, Reader, Writeable, Writer}; +use std::convert::TryInto; + +/// Writes an optional value as '1' + value if Some, or '0' if None +/// +/// This function is used to serialize an optional value into a Writer. If the option +/// contains Some value, it writes '1' followed by the serialized value. If the option +/// is None, it just writes '0'. +/// +/// # Arguments +/// +/// * `writer` - A Writer instance where the data will be written. +/// * `o` - The Optional value that will be written. +/// +/// # Returns +/// +/// * If successful, returns Ok with nothing. +/// * If an error occurs during writing, returns Err wrapping the error. +/// +/// # Example +/// +/// ``` +/// use grin_wallet_libwallet::mwixnet::onion_util::write_optional; +/// let mut writer:Vec = vec![]; +/// let optional_value: Option = Some(10); +/// //write_optional(&mut writer, &optional_value); +/// ``` +pub fn write_optional( + writer: &mut W, + o: &Option, +) -> Result<(), ser::Error> { + match &o { + Some(o) => { + writer.write_u8(1)?; + o.write(writer)?; + } + None => writer.write_u8(0)?, + }; + Ok(()) +} + +/// Reads an optional value as '1' + value if Some, or '0' if None +/// +/// This function is used to deserialize an optional value from a Reader. If the first byte +/// read is '0', it returns None. If the first byte is '1', it reads the next value and +/// returns Some(value). +/// +/// # Arguments +/// +/// * `reader` - A Reader instance from where the data will be read. +/// +/// # Returns +/// +/// * If successful, returns Ok wrapping an optional value. If the first byte read was '0', +/// returns None. If it was '1', returns Some(value). +/// * If an error occurs during reading, returns Err wrapping the error. +/// +/// # Example +/// +/// ``` +/// use grin_wallet_libwallet::mwixnet::onion_util::read_optional; +/// use grin_core::ser::{BinReader, ProtocolVersion, DeserializationMode}; +/// let mut buf: &[u8] = &[1, 0, 0, 0, 10]; +/// let mut reader = BinReader::new(&mut buf, ProtocolVersion::local(), DeserializationMode::default()); +/// let optional_value: Option = read_optional(&mut reader).unwrap(); +/// assert_eq!(optional_value, Some(10)); +/// ``` +pub fn read_optional(reader: &mut R) -> Result, ser::Error> { + let o = if reader.read_u8()? == 0 { + None + } else { + Some(O::read(reader)?) + }; + Ok(o) +} + +/// Convert a vector to an array of size `S`. +/// +/// # Arguments +/// +/// * `vec` - The input vector. +/// +/// # Returns +/// +/// * If successful, returns an `Ok` wrapping an array of size `S` containing +/// the first `S` bytes of `vec`. +/// * If `vec` is smaller than `S`, returns an `Err` indicating a count error. +/// +/// # Example +/// +/// ``` +/// use grin_wallet_libwallet::mwixnet::onion_util::vec_to_array; +/// let v = vec![0, 1, 2, 3, 4, 5]; +/// let a = vec_to_array::<4>(&v).unwrap(); +/// assert_eq!(a, [0, 1, 2, 3]); +/// ``` +pub fn vec_to_array(vec: &Vec) -> Result<[u8; S], ser::Error> { + if vec.len() < S { + return Err(ser::Error::CountError); + } + let arr: [u8; S] = vec[0..S].try_into().unwrap(); + Ok(arr) +} + +#[cfg(test)] +mod tests { + use super::*; + use grin_core::ser::{BinReader, BinWriter, DeserializationMode, ProtocolVersion}; + + #[test] + fn test_write_optional() { + // Test with Some value + let mut buf: Vec = vec![]; + let val: Option = Some(10); + write_optional(&mut BinWriter::default(&mut buf), &val).unwrap(); + assert_eq!(buf, &[1, 0, 0, 0, 10]); // 1 for Some, then 10 as a little-endian u32 + + // Test with None value + buf.clear(); + let val: Option = None; + write_optional(&mut BinWriter::default(&mut buf), &val).unwrap(); + assert_eq!(buf, &[0]); // 0 for None + } + + #[test] + fn test_read_optional() { + // Test with Some value + let mut buf: &[u8] = &[1, 0, 0, 0, 10]; // 1 for Some, then 10 as a little-endian u32 + let val: Option = read_optional(&mut BinReader::new( + &mut buf, + ProtocolVersion::local(), + DeserializationMode::default(), + )) + .unwrap(); + assert_eq!(val, Some(10)); + + // Test with None value + buf = &[0]; // 0 for None + let val: Option = read_optional(&mut BinReader::new( + &mut buf, + ProtocolVersion::local(), + DeserializationMode::default(), + )) + .unwrap(); + assert_eq!(val, None); + } + + #[test] + fn test_vec_to_array_success() { + let v = vec![1, 2, 3, 4, 5, 6, 7, 8]; + let a = vec_to_array::<4>(&v).unwrap(); + assert_eq!(a, [1, 2, 3, 4]); + } + + #[test] + fn test_vec_to_array_too_small() { + let v = vec![1, 2, 3]; + let res = vec_to_array::<4>(&v); + assert!(res.is_err()); + } + + #[test] + fn test_vec_to_array_empty() { + let v = vec![]; + let res = vec_to_array::<4>(&v); + assert!(res.is_err()); + } +} diff --git a/libwallet/src/mwixnet/types.rs b/libwallet/src/mwixnet/types.rs new file mode 100644 index 0000000..1213f3a --- /dev/null +++ b/libwallet/src/mwixnet/types.rs @@ -0,0 +1,43 @@ +// Copyright 2024 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Types related to mwixnet requests required by rest of lib crate apis +//! Should rexport all needed types here + +use super::onion::comsig_serde; +use grin_core::libtx::secp_ser::string_or_u64; +use grin_util::secp::key::SecretKey; +use serde::{Deserialize, Serialize}; + +pub use super::onion::{onion::Onion, ComSignature, Hop}; + +/// A Swap request +#[derive(Serialize, Deserialize, Debug)] +pub struct SwapReq { + /// Com signature + #[serde(with = "comsig_serde")] + pub comsig: ComSignature, + /// Onion + pub onion: Onion, +} + +/// mwixnetRequest Creation Params +#[derive(Serialize, Deserialize, Debug)] +pub struct MixnetReqCreationParams { + /// List of all the server keys + pub server_keys: Vec, + /// Fees per hop + #[serde(with = "string_or_u64")] + pub fee_per_hop: u64, +} diff --git a/libwallet/src/slate.rs b/libwallet/src/slate.rs new file mode 100644 index 0000000..43a7ba9 --- /dev/null +++ b/libwallet/src/slate.rs @@ -0,0 +1,1101 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Functions for building partial transactions to be passed +//! around during an interactive wallet exchange + +use crate::error::Error; +use crate::grin_core::core::amount_to_hr_string; +use crate::grin_core::core::transaction::{ + FeeFields, Input, Inputs, KernelFeatures, NRDRelativeHeight, Output, OutputFeatures, + Transaction, TxKernel, Weighting, +}; +use crate::grin_core::libtx::{aggsig, build, proof::ProofBuild, tx_fee}; +use crate::grin_core::map_vec; +use crate::grin_keychain::{BlindSum, BlindingFactor, Keychain, SwitchCommitmentType}; +use crate::grin_util::secp::key::{PublicKey, SecretKey}; +use crate::grin_util::secp::pedersen::Commitment; +use crate::grin_util::secp::Signature; +use crate::grin_util::{secp, static_secp_instance}; +use ed25519_dalek::PublicKey as DalekPublicKey; +use ed25519_dalek::Signature as DalekSignature; +use serde::ser::{Serialize, Serializer}; +use serde_json; +use std::fmt; +use uuid::Uuid; + +use crate::slate_versions::v4::{ + CommitsV4, KernelFeaturesArgsV4, OutputFeaturesV4, ParticipantDataV4, PaymentInfoV4, + SlateStateV4, SlateV4, VersionCompatInfoV4, +}; +use crate::slate_versions::VersionedSlate; +use crate::slate_versions::{CURRENT_SLATE_VERSION, GRIN_BLOCK_HEADER_VERSION}; +use crate::Context; + +#[derive(Debug, Clone)] +pub struct PaymentInfo { + /// Sender address + pub sender_address: DalekPublicKey, + /// Receiver address + pub receiver_address: DalekPublicKey, + /// Receiver signature + pub receiver_signature: Option, +} + +/// Public data for each participant in the slate +#[derive(Debug, Clone)] +pub struct ParticipantData { + /// Public key corresponding to private blinding factor + pub public_blind_excess: PublicKey, + /// Public key corresponding to private nonce + pub public_nonce: PublicKey, + /// Public partial signature + pub part_sig: Option, +} + +impl ParticipantData { + /// A helper to return whether this participant + /// has completed round 1 and round 2; + /// Round 1 has to be completed before instantiation of this struct + /// anyhow, and for each participant consists of: + /// -Inputs added to transaction + /// -Outputs added to transaction + /// -Public signature nonce chosen and added + /// -Public contribution to blinding factor chosen and added + /// Round 2 can only be completed after all participants have + /// performed round 1, and adds: + /// -Part sig is filled out + pub fn is_complete(&self) -> bool { + self.part_sig.is_some() + } +} + +/// A 'Slate' is passed around to all parties to build up all of the public +/// transaction data needed to create a finalized transaction. Callers can pass +/// the slate around by whatever means they choose, (but we can provide some +/// binary or JSON serialization helpers here). + +#[derive(Debug, Clone)] +pub struct Slate { + /// Versioning info + pub version_info: VersionCompatInfo, + /// The number of participants intended to take part in this transaction + pub num_participants: u8, + /// Unique transaction ID, selected by sender + pub id: Uuid, + /// Slate state + pub state: SlateState, + /// The core transaction data: + /// inputs, outputs, kernels, kernel offset + /// Optional as of V4 to allow for a compact + /// transaction initiation + pub tx: Option, + /// base amount (excluding fee) + pub amount: u64, + /// fee amount and shift + pub fee_fields: FeeFields, + /// TTL, the block height at which wallets + /// should refuse to process the transaction and unlock all + /// associated outputs + pub ttl_cutoff_height: u64, + /// Kernel Features flag - + /// 0: plain + /// 1: coinbase (invalid) + /// 2: height_locked + /// 3: NRD + pub kernel_features: u8, + /// Offset, needed when posting of transasction is deferred + pub offset: BlindingFactor, + /// Participant data, each participant in the transaction will + /// insert their public data here. For now, 0 is sender and 1 + /// is receiver, though this will change for multi-party + pub participant_data: Vec, + /// Payment Proof + pub payment_proof: Option, + /// Kernel features arguments + pub kernel_features_args: Option, +} + +impl fmt::Display for Slate { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", serde_json::to_string_pretty(&self).unwrap()) + } +} + +/// Slate state definition +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SlateState { + /// Unknown, coming from earlier slate versions + Unknown, + /// Standard flow, freshly init + Standard1, + /// Standard flow, return journey + Standard2, + /// Standard flow, ready for transaction posting + Standard3, + /// Invoice flow, freshly init + Invoice1, + ///Invoice flow, return journey + Invoice2, + /// Invoice flow, ready for tranasction posting + Invoice3, +} + +impl fmt::Display for SlateState { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let res = match self { + SlateState::Unknown => "UN", + SlateState::Standard1 => "S1", + SlateState::Standard2 => "S2", + SlateState::Standard3 => "S3", + SlateState::Invoice1 => "I1", + SlateState::Invoice2 => "I2", + SlateState::Invoice3 => "I3", + }; + write!(f, "{}", res) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +/// Kernel features arguments definition +pub struct KernelFeaturesArgs { + /// Lock height, for HeightLocked (also NRD relative lock height) + pub lock_height: u64, +} + +impl Default for KernelFeaturesArgs { + fn default() -> KernelFeaturesArgs { + KernelFeaturesArgs { lock_height: 0 } + } +} + +/// Versioning and compatibility info about this slate +#[derive(Debug, Clone)] +pub struct VersionCompatInfo { + /// The current version of the slate format + pub version: u16, + /// The grin block header version this slate is intended for + pub block_header_version: u16, +} + +impl Slate { + /// Return the transaction, throwing an error if it doesn't exist + /// to be used at points in the code where the existence of a transaction + /// is assumed + pub fn tx_or_err(&self) -> Result<&Transaction, Error> { + match &self.tx { + Some(t) => Ok(t), + None => Err(Error::SlateTransactionRequired), + } + } + + /// As above, but return mutable reference + pub fn tx_or_err_mut(&mut self) -> Result<&mut Transaction, Error> { + match &mut self.tx { + Some(t) => Ok(t), + None => Err(Error::SlateTransactionRequired), + } + } + + /// number of participants + pub fn num_participants(&self) -> u8 { + match self.num_participants { + 0 => 2, + n => n, + } + } + + /// Recieve a slate, upgrade it to the latest version internally + /// Throw error if this can't be done + pub fn deserialize_upgrade(slate_json: &str) -> Result { + let v_slate: VersionedSlate = + serde_json::from_str(slate_json).map_err(|_| Error::SlateVersionParse)?; + Slate::upgrade(v_slate) + } + + /// Upgrade a versioned slate + pub fn upgrade(v_slate: VersionedSlate) -> Result { + let v4: SlateV4 = match v_slate { + VersionedSlate::V4(s) => s, + }; + Ok(v4.into()) + } + /// Compact the slate for initial sending, storing the excess + offset explicit + /// and removing my input/output data + /// This info must be stored in the context for repopulation later + pub fn compact(&mut self) -> Result<(), Error> { + self.tx = None; + Ok(()) + } + + /// Build a new empty transaction. + /// Wallet currently only supports tx with "features and commit" inputs. + pub fn empty_transaction() -> Transaction { + let mut tx = Transaction::empty(); + tx.body = tx.body.replace_inputs(Inputs::FeaturesAndCommit(vec![])); + tx + } + + /// Create a new slate + pub fn blank(num_participants: u8, is_invoice: bool) -> Slate { + let np = match num_participants { + 0 => 2, + n => n, + }; + let state = match is_invoice { + true => SlateState::Invoice1, + false => SlateState::Standard1, + }; + Slate { + num_participants: np, // assume 2 if not present + id: Uuid::new_v4(), + state, + tx: Some(Slate::empty_transaction()), + amount: 0, + fee_fields: FeeFields::zero(), + ttl_cutoff_height: 0, + kernel_features: 0, + offset: BlindingFactor::zero(), + participant_data: vec![], + version_info: VersionCompatInfo { + version: CURRENT_SLATE_VERSION, + block_header_version: GRIN_BLOCK_HEADER_VERSION, + }, + payment_proof: None, + kernel_features_args: None, + } + } + /// Removes any signature data that isn't mine, for compacting + /// slates for a return journey + pub fn remove_other_sigdata( + &mut self, + keychain: &K, + sec_nonce: &SecretKey, + sec_key: &SecretKey, + ) -> Result<(), Error> + where + K: Keychain, + { + let pub_nonce = PublicKey::from_secret_key(keychain.secp(), &sec_nonce)?; + let pub_key = PublicKey::from_secret_key(keychain.secp(), &sec_key)?; + self.participant_data = self + .participant_data + .clone() + .into_iter() + .filter(|v| v.public_nonce == pub_nonce && v.public_blind_excess == pub_key) + .collect(); + Ok(()) + } + + /// Adds selected inputs and outputs to the slate's transaction + /// Returns blinding factor + pub fn add_transaction_elements( + &mut self, + keychain: &K, + builder: &B, + elems: Vec>>, + ) -> Result + where + K: Keychain, + B: ProofBuild, + { + self.update_kernel()?; + if elems.is_empty() { + return Ok(BlindingFactor::zero()); + } + let (tx, blind) = + build::partial_transaction(self.tx_or_err()?.clone(), &elems, keychain, builder)?; + self.tx = Some(tx); + Ok(blind) + } + + /// Update the tx kernel based on kernel features derived from the current slate. + /// The fee may change as we build a transaction and we need to + /// update the tx kernel to reflect this during the tx building process. + pub fn update_kernel(&mut self) -> Result<(), Error> { + self.tx = Some( + self.tx_or_err()? + .clone() + .replace_kernel(TxKernel::with_features(self.kernel_features()?)), + ); + Ok(()) + } + + /// Completes callers part of round 1, adding public key info + /// to the slate + pub fn fill_round_1(&mut self, keychain: &K, context: &mut Context) -> Result<(), Error> + where + K: Keychain, + { + self.add_participant_info(keychain, context, None) + } + + // Build kernel features based on variant and associated data. + // 0: plain + // 1: coinbase (invalid) + // 2: height_locked (with associated lock_height) + // 3: NRD (with associated relative_height) + // Any other value is invalid. + fn kernel_features(&self) -> Result { + match self.kernel_features { + 0 => Ok(KernelFeatures::Plain { + fee: self.fee_fields, + }), + 1 => Err(Error::InvalidKernelFeatures(1)), + 2 => Ok(KernelFeatures::HeightLocked { + fee: self.fee_fields, + lock_height: match &self.kernel_features_args { + Some(a) => a.lock_height, + None => return Err(Error::KernelFeaturesMissing(format!("lock_height"))), + }, + }), + 3 => Ok(KernelFeatures::NoRecentDuplicate { + fee: self.fee_fields, + relative_height: match &self.kernel_features_args { + Some(a) => NRDRelativeHeight::new(a.lock_height)?, + None => return Err(Error::KernelFeaturesMissing(format!("lock_height"))), + }, + }), + n => Err(Error::UnknownKernelFeatures(n)), + } + } + + /// This is the msg that we will sign as part of the tx kernel. + pub fn msg_to_sign(&self) -> Result { + let msg = self.kernel_features()?.kernel_sig_msg()?; + Ok(msg) + } + + /// Completes caller's part of round 2, completing signatures + pub fn fill_round_2( + &mut self, + keychain: &K, + sec_key: &SecretKey, + sec_nonce: &SecretKey, + ) -> Result<(), Error> + where + K: Keychain, + { + // TODO: Note we're unable to verify fees in this instance + // Left here is a reminder that we no longer check fees + // self.check_fees()?; + + self.verify_part_sigs(keychain.secp())?; + let sig_part = aggsig::calculate_partial_sig( + keychain.secp(), + sec_key, + sec_nonce, + &self.pub_nonce_sum(keychain.secp())?, + Some(&self.pub_blind_sum(keychain.secp())?), + &self.msg_to_sign()?, + )?; + let pub_excess = PublicKey::from_secret_key(keychain.secp(), &sec_key)?; + let pub_nonce = PublicKey::from_secret_key(keychain.secp(), &sec_nonce)?; + for i in 0..self.num_participants() as usize { + // find my entry + if self.participant_data[i].public_blind_excess == pub_excess + && self.participant_data[i].public_nonce == pub_nonce + { + self.participant_data[i].part_sig = Some(sig_part); + break; + } + } + Ok(()) + } + + /// Creates the final signature, callable by either the sender or recipient + /// (after phase 3: sender confirmation) + pub fn finalize(&mut self, keychain: &K) -> Result<(), Error> + where + K: Keychain, + { + let final_sig = self.finalize_signature(keychain.secp())?; + self.finalize_transaction(keychain, &final_sig) + } + + /// Return the sum of public nonces + pub fn pub_nonce_sum(&self, secp: &secp::Secp256k1) -> Result { + let pub_nonces: Vec<&PublicKey> = self + .participant_data + .iter() + .map(|p| &p.public_nonce) + .collect(); + if pub_nonces.len() == 0 { + return Err(Error::Commit(format!("Participant nonces cannot be empty"))); + } + match PublicKey::from_combination(secp, pub_nonces) { + Ok(k) => Ok(k), + Err(e) => Err(Error::Secp(e)), + } + } + + /// Return the sum of public blinding factors + pub fn pub_blind_sum(&self, secp: &secp::Secp256k1) -> Result { + let pub_blinds: Vec<&PublicKey> = self + .participant_data + .iter() + .map(|p| &p.public_blind_excess) + .collect(); + if pub_blinds.len() == 0 { + return Err(Error::Commit(format!( + "Participant Blind sums cannot be empty" + ))); + } + match PublicKey::from_combination(secp, pub_blinds) { + Ok(k) => Ok(k), + Err(e) => Err(Error::Secp(e)), + } + } + + /// Return vector of all partial sigs + fn part_sigs(&self) -> Vec<&Signature> { + self.participant_data + .iter() + .filter(|p| p.part_sig.is_some()) + .map(|p| p.part_sig.as_ref().unwrap()) + .collect() + } + + /// Adds participants public keys to the slate data + /// and saves participant's transaction context + /// sec_key can be overridden to replace the blinding + /// factor (by whoever split the offset) + pub fn add_participant_info( + &mut self, + keychain: &K, + context: &Context, + part_sig: Option, + ) -> Result<(), Error> + where + K: Keychain, + { + // Add our public key and nonce to the slate + let pub_key = PublicKey::from_secret_key(keychain.secp(), &context.sec_key)?; + let pub_nonce = PublicKey::from_secret_key(keychain.secp(), &context.sec_nonce)?; + let mut part_sig = part_sig; + + // Remove if already here and replace + self.participant_data = self + .participant_data + .clone() + .into_iter() + .filter(|v| { + if v.public_nonce == pub_nonce + && v.public_blind_excess == pub_key + && part_sig == None + { + part_sig = v.part_sig + } + v.public_nonce != pub_nonce || v.public_blind_excess != pub_key + }) + .collect(); + + self.participant_data.push(ParticipantData { + public_blind_excess: pub_key, + public_nonce: pub_nonce, + part_sig: part_sig, + }); + Ok(()) + } + + /// Add our contribution to the offset based on the excess, inputs and outputs + pub fn adjust_offset( + &mut self, + keychain: &K, + context: &Context, + ) -> Result<(), Error> { + let mut sum = BlindSum::new() + .add_blinding_factor(self.offset.clone()) + .sub_blinding_factor(BlindingFactor::from_secret_key( + context.initial_sec_key.clone(), + )); + for (id, _, amount) in &context.input_ids { + sum = sum.sub_blinding_factor(BlindingFactor::from_secret_key(keychain.derive_key( + *amount, + id, + SwitchCommitmentType::Regular, + )?)); + } + for (id, _, amount) in &context.output_ids { + sum = sum.add_blinding_factor(BlindingFactor::from_secret_key(keychain.derive_key( + *amount, + id, + SwitchCommitmentType::Regular, + )?)); + } + self.offset = keychain.blind_sum(&sum)?; + + Ok(()) + } + + /// Checks the fees in the transaction in the given slate are valid + fn check_fees(&self) -> Result<(), Error> { + let tx = self.tx_or_err()?; + // double check the fee amount included in the partial tx + // we don't necessarily want to just trust the sender + // we could just overwrite the fee here (but we won't) due to the sig + let fee = tx_fee(tx.inputs().len(), tx.outputs().len(), tx.kernels().len()); + + if fee > tx.fee() { + // apply fee mask past HF4 + return Err(Error::Fee(format!( + "Fee Dispute Error: {}, {}", + tx.fee(), + fee, + ))); + } + + if fee > self.amount + self.fee_fields.fee() { + let reason = format!( + "Rejected the transfer because transaction fee ({}) exceeds received amount ({}).", + amount_to_hr_string(fee, false), + amount_to_hr_string(self.amount + self.fee_fields.fee(), false) + ); + info!("{}", reason); + return Err(Error::Fee(reason)); + } + + Ok(()) + } + + /// Verifies all of the partial signatures in the Slate are valid + fn verify_part_sigs(&self, secp: &secp::Secp256k1) -> Result<(), Error> { + // collect public nonces + for p in self.participant_data.iter() { + if p.is_complete() { + aggsig::verify_partial_sig( + secp, + p.part_sig.as_ref().unwrap(), + &self.pub_nonce_sum(secp)?, + &p.public_blind_excess, + Some(&self.pub_blind_sum(secp)?), + &self.msg_to_sign()?, + )?; + } + } + Ok(()) + } + + /// This should be callable by either the sender or receiver + /// once phase 3 is done + /// + /// Receive Part 3 of interactive transactions from sender, Sender + /// Confirmation Return Ok/Error + /// -Receiver receives sS + /// -Receiver verifies sender's sig, by verifying that + /// kS * G + e *xS * G = sS* G + /// -Receiver calculates final sig as s=(sS+sR, kS * G+kR * G) + /// -Receiver puts into TX kernel: + /// + /// Signature S + /// pubkey xR * G+xS * G + /// fee (= M) + /// + /// Returns completed transaction ready for posting to the chain + + fn finalize_signature(&mut self, secp: &secp::Secp256k1) -> Result { + self.verify_part_sigs(secp)?; + + let part_sigs = self.part_sigs(); + let pub_nonce_sum = self.pub_nonce_sum(secp)?; + let final_pubkey = self.pub_blind_sum(secp)?; + // get the final signature + let final_sig = aggsig::add_signatures(secp, part_sigs, &pub_nonce_sum)?; + + // Calculate the final public key (for our own sanity check) + + // Check our final sig verifies + aggsig::verify_completed_sig( + secp, + &final_sig, + &final_pubkey, + Some(&final_pubkey), + &self.msg_to_sign()?, + )?; + + Ok(final_sig) + } + + /// Calculate the excess + pub fn calc_excess(&self, secp: &secp::Secp256k1) -> Result { + let sum = self.pub_blind_sum(secp)?; + Ok(Commitment::from_pubkey(secp, &sum)?) + } + + /// builds a final transaction after the aggregated sig exchange + fn finalize_transaction( + &mut self, + keychain: &K, + final_sig: &secp::Signature, + ) -> Result<(), Error> + where + K: Keychain, + { + self.check_fees()?; + // build the final excess based on final tx and offset + let final_excess = self.calc_excess(keychain.secp())?; + + debug!("Final Tx excess: {:?}", final_excess); + + let final_tx = self.tx_or_err()?; + + // update the tx kernel to reflect the offset excess and sig + assert_eq!(final_tx.kernels().len(), 1); + + let mut kernel = final_tx.kernels()[0]; + kernel.excess = final_excess; + kernel.excess_sig = final_sig.clone(); + + let final_tx = final_tx.clone().replace_kernel(kernel); + + // confirm the kernel verifies successfully before proceeding + debug!("Validating final transaction"); + trace!( + "Final tx: {}", + serde_json::to_string_pretty(&final_tx).unwrap() + ); + final_tx.kernels()[0].verify()?; + + // confirm the overall transaction is valid (including the updated kernel) + // accounting for tx weight limits + if let Err(e) = final_tx.validate(Weighting::AsTransaction) { + error!("Error with final tx validation: {}", e); + Err(e.into()) + } else { + // replace our slate tx with the new one with updated kernel + self.tx = Some(final_tx); + + Ok(()) + } + } +} + +impl Serialize for Slate { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let v4 = SlateV4::from(self); + v4.serialize(serializer) + } +} +// Current slate version to versioned conversions + +// Slate to versioned +impl From for SlateV4 { + fn from(slate: Slate) -> SlateV4 { + let Slate { + num_participants: num_parts, + id, + state, + tx: _, + amount, + fee_fields, + kernel_features, + ttl_cutoff_height: ttl, + offset: off, + participant_data, + version_info, + payment_proof, + kernel_features_args, + } = slate.clone(); + let participant_data = map_vec!(participant_data, |data| ParticipantDataV4::from(data)); + let ver = VersionCompatInfoV4::from(&version_info); + let payment_proof = match payment_proof { + Some(p) => Some(PaymentInfoV4::from(&p)), + None => None, + }; + let feat_args = match kernel_features_args { + Some(a) => Some(KernelFeaturesArgsV4::from(&a)), + None => None, + }; + let sta = SlateStateV4::from(&state); + SlateV4 { + num_parts, + id, + sta, + coms: (&slate).into(), + amt: amount, + fee: fee_fields, + feat: kernel_features, + ttl, + off, + sigs: participant_data, + ver, + proof: payment_proof, + feat_args, + } + } +} + +impl From<&Slate> for SlateV4 { + fn from(slate: &Slate) -> SlateV4 { + let Slate { + num_participants: num_parts, + id, + state, + tx: _, + amount, + fee_fields, + kernel_features, + ttl_cutoff_height: ttl, + offset, + participant_data, + version_info, + payment_proof, + kernel_features_args, + } = slate; + let num_parts = *num_parts; + let id = *id; + let amount = *amount; + let fee_fields = *fee_fields; + let feat = *kernel_features; + let ttl = *ttl; + let off = offset.clone(); + let participant_data = map_vec!(participant_data, |data| ParticipantDataV4::from(data)); + let ver = VersionCompatInfoV4::from(version_info); + let payment_proof = match payment_proof { + Some(p) => Some(PaymentInfoV4::from(p)), + None => None, + }; + let sta = SlateStateV4::from(state); + let feat_args = match kernel_features_args { + Some(a) => Some(KernelFeaturesArgsV4::from(a)), + None => None, + }; + SlateV4 { + num_parts, + id, + sta, + coms: slate.into(), + amt: amount, + fee: fee_fields, + feat, + ttl, + off, + sigs: participant_data, + ver, + proof: payment_proof, + feat_args, + } + } +} + +impl From<&Slate> for Option> { + fn from(slate: &Slate) -> Self { + match slate.tx { + None => None, + Some(ref tx) => { + let mut ret_vec = vec![]; + match tx.inputs() { + Inputs::CommitOnly(_) => panic!("commit only inputs unsupported"), + Inputs::FeaturesAndCommit(ref inputs) => { + for input in inputs { + ret_vec.push(input.into()); + } + } + } + for output in tx.outputs() { + ret_vec.push(output.into()); + } + Some(ret_vec) + } + } + } +} + +impl From<&ParticipantData> for ParticipantDataV4 { + fn from(data: &ParticipantData) -> ParticipantDataV4 { + let ParticipantData { + public_blind_excess, + public_nonce, + part_sig, + } = data; + let public_blind_excess = *public_blind_excess; + let public_nonce = *public_nonce; + let part_sig = *part_sig; + ParticipantDataV4 { + xs: public_blind_excess, + nonce: public_nonce, + part: part_sig, + } + } +} + +impl From<&SlateState> for SlateStateV4 { + fn from(data: &SlateState) -> SlateStateV4 { + match data { + SlateState::Unknown => SlateStateV4::Unknown, + SlateState::Standard1 => SlateStateV4::Standard1, + SlateState::Standard2 => SlateStateV4::Standard2, + SlateState::Standard3 => SlateStateV4::Standard3, + SlateState::Invoice1 => SlateStateV4::Invoice1, + SlateState::Invoice2 => SlateStateV4::Invoice2, + SlateState::Invoice3 => SlateStateV4::Invoice3, + } + } +} + +impl From<&KernelFeaturesArgs> for KernelFeaturesArgsV4 { + fn from(data: &KernelFeaturesArgs) -> KernelFeaturesArgsV4 { + let KernelFeaturesArgs { lock_height } = data; + let lock_hgt = *lock_height; + KernelFeaturesArgsV4 { lock_hgt } + } +} + +impl From<&VersionCompatInfo> for VersionCompatInfoV4 { + fn from(data: &VersionCompatInfo) -> VersionCompatInfoV4 { + let VersionCompatInfo { + version, + block_header_version, + } = data; + let version = *version; + let block_header_version = *block_header_version; + VersionCompatInfoV4 { + version, + block_header_version, + } + } +} + +impl From<&PaymentInfo> for PaymentInfoV4 { + fn from(data: &PaymentInfo) -> PaymentInfoV4 { + let PaymentInfo { + sender_address, + receiver_address, + receiver_signature, + } = data; + let sender_address = *sender_address; + let receiver_address = *receiver_address; + let receiver_signature = *receiver_signature; + PaymentInfoV4 { + saddr: sender_address, + raddr: receiver_address, + rsig: receiver_signature, + } + } +} + +impl From for OutputFeaturesV4 { + fn from(of: OutputFeatures) -> OutputFeaturesV4 { + let index = match of { + OutputFeatures::Plain => 0, + OutputFeatures::Coinbase => 1, + }; + OutputFeaturesV4(index) + } +} + +// Versioned to current slate +impl From for Slate { + fn from(slate: SlateV4) -> Slate { + let SlateV4 { + num_parts: num_participants, + id, + sta, + coms: _, + amt: amount, + fee: fee_fields, + feat: kernel_features, + ttl: ttl_cutoff_height, + off: offset, + sigs: participant_data, + ver, + proof: payment_proof, + feat_args, + } = slate.clone(); + let participant_data = map_vec!(participant_data, |data| ParticipantData::from(data)); + let version_info = VersionCompatInfo::from(&ver); + let payment_proof = match &payment_proof { + Some(p) => Some(PaymentInfo::from(p)), + None => None, + }; + let kernel_features_args = match &feat_args { + Some(a) => Some(KernelFeaturesArgs::from(a)), + None => None, + }; + let state = SlateState::from(&sta); + Slate { + num_participants, + id, + state, + tx: (&slate).into(), + amount, + fee_fields, + kernel_features, + ttl_cutoff_height, + offset, + participant_data, + version_info, + payment_proof, + kernel_features_args, + } + } +} + +pub fn tx_from_slate_v4(slate: &SlateV4) -> Option { + let coms = match slate.coms.as_ref() { + Some(c) => c, + None => return None, + }; + let secp = static_secp_instance(); + let secp = secp.lock(); + let mut calc_slate = Slate::blank(2, false); + calc_slate.fee_fields = slate.fee; + for d in slate.sigs.iter() { + calc_slate.participant_data.push(ParticipantData { + public_blind_excess: d.xs, + public_nonce: d.nonce, + part_sig: d.part, + }); + } + let excess = match calc_slate.calc_excess(&secp) { + Ok(e) => e, + Err(_) => Commitment::from_vec(vec![0]), + }; + let excess_sig = match calc_slate.finalize_signature(&secp) { + Ok(s) => s, + Err(_) => Signature::from_raw_data(&[0; 64]).unwrap(), + }; + let kernel = TxKernel { + features: match slate.feat { + 0 => KernelFeatures::Plain { fee: slate.fee }, + 1 => KernelFeatures::HeightLocked { + fee: slate.fee, + lock_height: match slate.feat_args.as_ref() { + Some(a) => a.lock_hgt, + None => 0, + }, + }, + _ => KernelFeatures::Plain { fee: slate.fee }, + }, + excess, + excess_sig, + }; + let mut tx = Slate::empty_transaction().with_kernel(kernel); + + let mut outputs = vec![]; + let mut inputs = vec![]; + + for c in coms.iter() { + match &c.p { + Some(p) => { + outputs.push(Output::new(c.f.into(), c.c, p.clone())); + } + None => { + inputs.push(Input { + features: c.f.into(), + commit: c.c, + }); + } + } + } + + tx.body = tx + .body + .replace_inputs(inputs.as_slice().into()) + .replace_outputs(outputs.as_slice()); + tx.offset = slate.off.clone(); + Some(tx) +} + +// Node's Transaction object and lock height to SlateV4 `coms` +impl From<&SlateV4> for Option { + fn from(slate: &SlateV4) -> Option { + tx_from_slate_v4(slate) + } +} + +impl From<&ParticipantDataV4> for ParticipantData { + fn from(data: &ParticipantDataV4) -> ParticipantData { + let ParticipantDataV4 { + xs: public_blind_excess, + nonce: public_nonce, + part: part_sig, + } = data; + let public_blind_excess = *public_blind_excess; + let public_nonce = *public_nonce; + let part_sig = *part_sig; + ParticipantData { + public_blind_excess, + public_nonce, + part_sig, + } + } +} + +impl From<&KernelFeaturesArgsV4> for KernelFeaturesArgs { + fn from(data: &KernelFeaturesArgsV4) -> KernelFeaturesArgs { + let KernelFeaturesArgsV4 { lock_hgt } = data; + let lock_height = *lock_hgt; + KernelFeaturesArgs { lock_height } + } +} + +impl From<&SlateStateV4> for SlateState { + fn from(data: &SlateStateV4) -> SlateState { + match data { + SlateStateV4::Unknown => SlateState::Unknown, + SlateStateV4::Standard1 => SlateState::Standard1, + SlateStateV4::Standard2 => SlateState::Standard2, + SlateStateV4::Standard3 => SlateState::Standard3, + SlateStateV4::Invoice1 => SlateState::Invoice1, + SlateStateV4::Invoice2 => SlateState::Invoice2, + SlateStateV4::Invoice3 => SlateState::Invoice3, + } + } +} + +impl From<&VersionCompatInfoV4> for VersionCompatInfo { + fn from(data: &VersionCompatInfoV4) -> VersionCompatInfo { + let VersionCompatInfoV4 { + version, + block_header_version, + } = data; + let version = *version; + let block_header_version = *block_header_version; + VersionCompatInfo { + version, + block_header_version, + } + } +} + +impl From<&PaymentInfoV4> for PaymentInfo { + fn from(data: &PaymentInfoV4) -> PaymentInfo { + let PaymentInfoV4 { + saddr: sender_address, + raddr: receiver_address, + rsig: receiver_signature, + } = data; + let sender_address = *sender_address; + let receiver_address = *receiver_address; + let receiver_signature = *receiver_signature; + PaymentInfo { + sender_address, + receiver_address, + receiver_signature, + } + } +} + +impl From for OutputFeatures { + fn from(of: OutputFeaturesV4) -> OutputFeatures { + match of.0 { + 1 => OutputFeatures::Coinbase, + 0 | _ => OutputFeatures::Plain, + } + } +} diff --git a/libwallet/src/slate_versions/mod.rs b/libwallet/src/slate_versions/mod.rs new file mode 100644 index 0000000..ca2e749 --- /dev/null +++ b/libwallet/src/slate_versions/mod.rs @@ -0,0 +1,122 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! This module contains old slate versions and conversions to the newest slate version +//! Used for serialization and deserialization of slates in a backwards compatible way. +//! Versions earlier than V3 are removed for the 4.0.0 release, but versioning code +//! remains for future needs + +use crate::slate::Slate; +use crate::slate_versions::v4::{CoinbaseV4, SlateV4}; +use crate::slate_versions::v4_bin::SlateV4Bin; +use crate::types::CbData; +use crate::Error; +use std::convert::TryFrom; + +pub mod ser; + +#[allow(missing_docs)] +pub mod v4; +#[allow(missing_docs)] +pub mod v4_bin; + +/// The most recent version of the slate +pub const CURRENT_SLATE_VERSION: u16 = 4; + +/// The grin block header this slate is intended to be compatible with +pub const GRIN_BLOCK_HEADER_VERSION: u16 = 3; + +/// Existing versions of the slate +#[derive(EnumIter, Serialize, Deserialize, Clone, Debug, PartialEq, PartialOrd, Eq, Ord)] +pub enum SlateVersion { + /// V4 (most current) + V4, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(untagged)] +/// Versions are ordered newest to oldest so serde attempts to +/// deserialize newer versions first, then falls back to older versions. +pub enum VersionedSlate { + /// Current (4.0.0 Onwards ) + V4(SlateV4), +} + +impl VersionedSlate { + /// Return slate version + pub fn version(&self) -> SlateVersion { + match *self { + VersionedSlate::V4(_) => SlateVersion::V4, + } + } + + /// convert this slate type to a specified older version + pub fn into_version(slate: Slate, version: SlateVersion) -> Result { + match version { + SlateVersion::V4 => Ok(VersionedSlate::V4(slate.into())), + } + } +} + +impl From for Slate { + fn from(slate: VersionedSlate) -> Slate { + match slate { + VersionedSlate::V4(s) => Slate::from(s), + } + } +} + +#[derive(Deserialize, Serialize)] +#[serde(untagged)] +/// Binary versions, can only be parsed 1:1 into the appropriate +/// version, and VersionedSlate can up/downgrade from there +pub enum VersionedBinSlate { + /// Version 4, binary + V4(SlateV4Bin), +} + +impl TryFrom for VersionedBinSlate { + type Error = Error; + fn try_from(slate: VersionedSlate) -> Result { + match slate { + VersionedSlate::V4(s) => Ok(VersionedBinSlate::V4(SlateV4Bin(s))), + } + } +} + +impl From for VersionedSlate { + fn from(slate: VersionedBinSlate) -> VersionedSlate { + match slate { + VersionedBinSlate::V4(s) => VersionedSlate::V4(s.0), + } + } +} + +#[derive(Deserialize, Serialize)] +#[serde(untagged)] +/// Versions are ordered newest to oldest so serde attempts to +/// deserialize newer versions first, then falls back to older versions. +pub enum VersionedCoinbase { + /// Current supported coinbase version. + V4(CoinbaseV4), +} + +impl VersionedCoinbase { + /// convert this coinbase data to a specific versioned representation for the json api. + pub fn into_version(cb: CbData, version: SlateVersion) -> VersionedCoinbase { + match version { + SlateVersion::V4 => VersionedCoinbase::V4(cb.into()), + } + } +} diff --git a/libwallet/src/slate_versions/ser.rs b/libwallet/src/slate_versions/ser.rs new file mode 100644 index 0000000..58f866a --- /dev/null +++ b/libwallet/src/slate_versions/ser.rs @@ -0,0 +1,660 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//! Sane serialization & deserialization of cryptographic structs into hex + +use serde::{Deserialize, Deserializer, Serializer}; + +/// Seralizes a byte string into base64 +pub fn as_base64(bytes: T, serializer: S) -> Result +where + T: AsRef<[u8]>, + S: Serializer, +{ + serializer.serialize_str(&base64::encode(&bytes)) +} + +/// Creates a Vec from a base string +pub fn bytes_from_base64<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + use serde::de::Error; + String::deserialize(deserializer) + .and_then(|string| base64::decode(&string).map_err(|err| Error::custom(err.to_string()))) +} + +/// Serializes an Option to and from hex +pub mod option_rangeproof_hex { + use crate::grin_util::secp::pedersen::RangeProof; + use crate::grin_util::{from_hex, ToHex}; + use serde::de::{Error, IntoDeserializer}; + use serde::{Deserialize, Deserializer, Serializer}; + + /// + pub fn serialize(proof: &Option, serializer: S) -> Result + where + S: Serializer, + { + match proof { + Some(p) => serializer.serialize_str(&p.to_hex()), + None => serializer.serialize_none(), + } + } + + /// + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + Option::::deserialize(deserializer).and_then(|res| match res { + Some(string) => from_hex(&string) + .map_err(|err| Error::custom(err.to_string())) + .and_then(|val| Ok(Some(RangeProof::deserialize(val.into_deserializer())?))), + None => Ok(None), + }) + } +} + +/// Serializes an OnionV3Address to and from hex +pub mod option_ov3_serde { + use serde::de::Error; + use serde::{Deserialize, Deserializer, Serializer}; + use std::convert::TryFrom; + + use crate::util::{OnionV3Address, OnionV3AddressError}; + + /// + pub fn serialize(addr: &Option, serializer: S) -> Result + where + S: Serializer, + { + match addr { + Some(a) => serializer.serialize_str(&a.to_string()), + None => serializer.serialize_none(), + } + } + + /// + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + Option::::deserialize(deserializer).and_then(|res| match res { + Some(s) => OnionV3Address::try_from(s.as_str()) + .map_err(|err: OnionV3AddressError| Error::custom(format!("{:?}", err))) + .and_then(|a| Ok(Some(a))), + None => Ok(None), + }) + } +} + +/// Serializes an OnionV3Address to and from hex +pub mod ov3_serde { + use serde::de::Error; + use serde::{Deserialize, Deserializer, Serializer}; + use std::convert::TryFrom; + + use crate::util::{OnionV3Address, OnionV3AddressError}; + + /// + pub fn serialize(addr: &OnionV3Address, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&addr.to_string()) + } + + /// + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + String::deserialize(deserializer).and_then(|s| { + OnionV3Address::try_from(s.as_str()) + .map_err(|err: OnionV3AddressError| Error::custom(format!("{:?}", err))) + .and_then(Ok) + }) + } +} + +/// Serializes an ed25519 PublicKey to and from hex +pub mod dalek_seckey_serde { + use crate::grin_util::{from_hex, ToHex}; + use ed25519_dalek::SecretKey as DalekSecretKey; + use serde::{Deserialize, Deserializer, Serializer}; + + /// + pub fn serialize(key: &DalekSecretKey, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&key.to_bytes().to_hex()) + } + + /// + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + use serde::de::Error; + String::deserialize(deserializer) + .and_then(|string| from_hex(&string).map_err(|err| Error::custom(err.to_string()))) + .and_then(|bytes: Vec| { + DalekSecretKey::from_bytes(&bytes).map_err(|err| Error::custom(err.to_string())) + }) + } +} + +/// Serializes an ed25519 PublicKey to and from hex +pub mod dalek_pubkey_serde { + use crate::grin_util::{from_hex, ToHex}; + use ed25519_dalek::PublicKey as DalekPublicKey; + use serde::{Deserialize, Deserializer, Serializer}; + + /// + pub fn serialize(key: &DalekPublicKey, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&key.to_bytes().to_hex()) + } + + /// + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + use serde::de::Error; + String::deserialize(deserializer) + .and_then(|string| from_hex(&string).map_err(|err| Error::custom(err.to_string()))) + .and_then(|bytes: Vec| { + DalekPublicKey::from_bytes(&bytes).map_err(|err| Error::custom(err.to_string())) + }) + } +} + +/// Serializes an x25519 PublicKey to and from hex +pub mod dalek_xpubkey_serde { + use crate::grin_util::{from_hex, ToHex}; + use serde::{Deserialize, Deserializer, Serializer}; + use x25519_dalek::PublicKey as xDalekPublicKey; + + /// + pub fn serialize(key: &xDalekPublicKey, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&key.as_bytes().to_hex()) + } + + /// + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + use serde::de::Error; + String::deserialize(deserializer) + .and_then(|string| from_hex(&string).map_err(|err| Error::custom(err.to_string()))) + .and_then(|bytes: Vec| { + let mut b = [0u8; 32]; + b.copy_from_slice(&bytes[0..32]); + Ok(xDalekPublicKey::from(b)) + }) + } +} + +/// Serializes an ed25519 PublicKey to and from base64 +pub mod dalek_pubkey_base64 { + use base64; + use ed25519_dalek::PublicKey as DalekPublicKey; + use serde::{Deserialize, Deserializer, Serializer}; + + /// + pub fn serialize(key: &DalekPublicKey, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&base64::encode(&key.to_bytes())) + } + + /// + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + use serde::de::Error; + String::deserialize(deserializer) + .and_then(|string| { + base64::decode(&string).map_err(|err| Error::custom(err.to_string())) + }) + .and_then(|bytes: Vec| { + DalekPublicKey::from_bytes(&bytes).map_err(|err| Error::custom(err.to_string())) + }) + } +} + +/// Serializes an Option to and from hex +pub mod option_dalek_pubkey_base64 { + use base64; + use ed25519_dalek::PublicKey as DalekPublicKey; + use serde::de::Error; + use serde::{Deserialize, Deserializer, Serializer}; + + /// + pub fn serialize(key: &Option, serializer: S) -> Result + where + S: Serializer, + { + match key { + Some(key) => serializer.serialize_str(&base64::encode(&key.to_bytes())), + None => serializer.serialize_none(), + } + } + + /// + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + Option::::deserialize(deserializer).and_then(|res| match res { + Some(string) => base64::decode(&string) + .map_err(|err| Error::custom(err.to_string())) + .and_then(|bytes: Vec| { + let mut b = [0u8; 32]; + b.copy_from_slice(&bytes[0..32]); + DalekPublicKey::from_bytes(&b) + .map(Some) + .map_err(|err| Error::custom(err.to_string())) + }), + None => { + println!("None fine"); + Ok(None) + } + }) + } +} + +/// Serializes an Option to and from hex +pub mod option_dalek_pubkey_serde { + use ed25519_dalek::PublicKey as DalekPublicKey; + use serde::de::Error; + use serde::{Deserialize, Deserializer, Serializer}; + + use crate::grin_util::{from_hex, ToHex}; + + /// + pub fn serialize(key: &Option, serializer: S) -> Result + where + S: Serializer, + { + match key { + Some(key) => serializer.serialize_str(&key.to_bytes().to_hex()), + None => serializer.serialize_none(), + } + } + + /// + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + Option::::deserialize(deserializer).and_then(|res| match res { + Some(string) => from_hex(&string) + .map_err(|err| Error::custom(err.to_string())) + .and_then(|bytes: Vec| { + let mut b = [0u8; 32]; + b.copy_from_slice(&bytes[0..32]); + DalekPublicKey::from_bytes(&b) + .map(Some) + .map_err(|err| Error::custom(err.to_string())) + }), + None => Ok(None), + }) + } +} + +/// Serializes an Option to and from hex +pub mod option_xdalek_pubkey_serde { + use serde::de::Error; + use serde::{Deserialize, Deserializer, Serializer}; + use x25519_dalek::PublicKey as xDalekPublicKey; + + use crate::grin_util::{from_hex, ToHex}; + + /// + pub fn serialize(key: &Option, serializer: S) -> Result + where + S: Serializer, + { + match key { + Some(key) => serializer.serialize_str(&key.as_bytes().to_hex()), + None => serializer.serialize_none(), + } + } + + /// + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + Option::::deserialize(deserializer).and_then(|res| match res { + Some(string) => from_hex(&string) + .map_err(|err| Error::custom(err.to_string())) + .and_then(|bytes: Vec| { + let mut b = [0u8; 32]; + b.copy_from_slice(&bytes[0..32]); + Ok(Some(xDalekPublicKey::from(b))) + }), + None => Ok(None), + }) + } +} + +/// Serializes an ed25519_dalek::Signature to and from hex +pub mod dalek_sig_serde { + use ed25519_dalek::Signature as DalekSignature; + use serde::de::Error; + use serde::{Deserialize, Deserializer, Serializer}; + use std::convert::TryFrom; + + use crate::grin_util::{from_hex, ToHex}; + + /// + pub fn serialize(sig: &DalekSignature, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&sig.to_bytes().as_ref().to_hex()) + } + + /// + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + String::deserialize(deserializer) + .and_then(|string| from_hex(&string).map_err(|err| Error::custom(err.to_string()))) + .and_then(|bytes: Vec| { + let mut b = [0u8; 64]; + b.copy_from_slice(&bytes[0..64]); + DalekSignature::try_from(b).map_err(|err| Error::custom(err.to_string())) + }) + } +} + +/// Serializes an Option to and from hex +pub mod option_dalek_sig_serde { + use ed25519_dalek::Signature as DalekSignature; + use serde::de::Error; + use serde::{Deserialize, Deserializer, Serializer}; + use std::convert::TryFrom; + + use crate::grin_util::{from_hex, ToHex}; + + /// + pub fn serialize(sig: &Option, serializer: S) -> Result + where + S: Serializer, + { + match sig { + Some(s) => serializer.serialize_str(&s.to_bytes().as_ref().to_hex()), + None => serializer.serialize_none(), + } + } + + /// + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + Option::::deserialize(deserializer).and_then(|res| match res { + Some(string) => from_hex(&string) + .map_err(|err| Error::custom(err.to_string())) + .and_then(|bytes: Vec| { + let mut b = [0u8; 64]; + b.copy_from_slice(&bytes[0..64]); + DalekSignature::try_from(b) + .map(Some) + .map_err(|err| Error::custom(err.to_string())) + }), + None => Ok(None), + }) + } +} + +/// Serializes an Option to and from base64 +pub mod option_dalek_sig_base64 { + use base64; + use ed25519_dalek::Signature as DalekSignature; + use serde::de::Error; + use serde::{Deserialize, Deserializer, Serializer}; + use std::convert::TryFrom; + + /// + pub fn serialize(sig: &Option, serializer: S) -> Result + where + S: Serializer, + { + match sig { + Some(s) => serializer.serialize_str(&base64::encode(&s.to_bytes().to_vec())), + None => serializer.serialize_none(), + } + } + + /// + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + Option::::deserialize(deserializer).and_then(|res| match res { + Some(string) => base64::decode(&string) + .map_err(|err| Error::custom(err.to_string())) + .and_then(|bytes: Vec| { + let mut b = [0u8; 64]; + b.copy_from_slice(&bytes[0..64]); + DalekSignature::try_from(b) + .map(Some) + .map_err(|err| Error::custom(err.to_string())) + }), + None => Ok(None), + }) + } +} + +/// Serializes slates 'version_info' field +pub mod version_info_v4 { + use serde::de::Error; + use serde::{Deserialize, Deserializer, Serializer}; + + use crate::slate_versions::v4::VersionCompatInfoV4; + + /// + pub fn serialize(v: &VersionCompatInfoV4, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&format!("{}:{}", v.version, v.block_header_version)) + } + + /// + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + String::deserialize(deserializer).and_then(|s| { + let mut retval = VersionCompatInfoV4 { + version: 0, + block_header_version: 0, + }; + let v: Vec<&str> = s.split(':').collect(); + if v.len() != 2 { + return Err(Error::custom("Cannot parse version")); + } + match u16::from_str_radix(v[0], 10) { + Ok(u) => retval.version = u, + Err(e) => return Err(Error::custom(format!("Cannot parse version: {}", e))), + } + match u16::from_str_radix(v[1], 10) { + Ok(u) => retval.block_header_version = u, + Err(e) => return Err(Error::custom(format!("Cannot parse version: {}", e))), + } + Ok(retval) + }) + } +} + +/// Serializes slates 'state' field +pub mod slate_state_v4 { + use serde::de::Error; + use serde::{Deserialize, Deserializer, Serializer}; + + use crate::slate_versions::v4::SlateStateV4; + + /// + pub fn serialize(st: &SlateStateV4, serializer: S) -> Result + where + S: Serializer, + { + let label = match st { + SlateStateV4::Unknown => "NA", + SlateStateV4::Standard1 => "S1", + SlateStateV4::Standard2 => "S2", + SlateStateV4::Standard3 => "S3", + SlateStateV4::Invoice1 => "I1", + SlateStateV4::Invoice2 => "I2", + SlateStateV4::Invoice3 => "I3", + }; + serializer.serialize_str(label) + } + + /// + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + String::deserialize(deserializer).and_then(|s| { + let retval = match s.as_str() { + "NA" => SlateStateV4::Unknown, + "S1" => SlateStateV4::Standard1, + "S2" => SlateStateV4::Standard2, + "S3" => SlateStateV4::Standard3, + "I1" => SlateStateV4::Invoice1, + "I2" => SlateStateV4::Invoice2, + "I3" => SlateStateV4::Invoice3, + _ => return Err(Error::custom("Invalid Slate state")), + }; + Ok(retval) + }) + } +} + +/// Serializes an secp256k1 pubkey to base64 +pub mod uuid_base64 { + use base64; + use serde::{Deserialize, Deserializer, Serializer}; + use uuid::Uuid; + + /// + pub fn serialize(id: &Uuid, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&base64::encode(&id.as_bytes())) + } + + /// + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + use serde::de::Error; + String::deserialize(deserializer) + .and_then(|string| { + base64::decode(&string).map_err(|err| Error::custom(err.to_string())) + }) + .and_then(|bytes: Vec| { + let mut b = [0u8; 16]; + b.copy_from_slice(&bytes[0..16]); + Ok(Uuid::from_bytes(b)) + }) + } +} +// Test serialization methods of components that are being used +#[cfg(test)] +mod test { + use super::*; + use rand::rngs::mock::StepRng; + + use crate::grin_util::{secp, static_secp_instance}; + use ed25519_dalek::Keypair; + use ed25519_dalek::PublicKey as DalekPublicKey; + use ed25519_dalek::SecretKey as DalekSecretKey; + use ed25519_dalek::Signature as DalekSignature; + use ed25519_dalek::Signer; + use serde::Deserialize; + + use serde_json; + + #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] + struct SerTest { + #[serde(with = "dalek_pubkey_serde")] + pub pub_key: DalekPublicKey, + #[serde(with = "option_dalek_pubkey_serde")] + pub pub_key_opt: Option, + #[serde(with = "dalek_sig_serde")] + pub sig: DalekSignature, + #[serde(with = "option_dalek_sig_serde")] + pub sig_opt: Option, + } + + impl SerTest { + pub fn random() -> SerTest { + let secp_inst = static_secp_instance(); + let secp = secp_inst.lock(); + let mut test_rng = StepRng::new(1234567890u64, 1); + let sec_key = secp::key::SecretKey::new(&secp, &mut test_rng); + let d_skey = DalekSecretKey::from_bytes(&sec_key.0).unwrap(); + let d_pub_key: DalekPublicKey = (&d_skey).into(); + + let keypair = Keypair { + public: d_pub_key, + secret: d_skey, + }; + + let d_sig = keypair.sign("test sig".as_bytes()); + println!("D sig: {:?}", d_sig); + + SerTest { + pub_key: d_pub_key.clone(), + pub_key_opt: Some(d_pub_key), + sig: d_sig.clone(), + sig_opt: Some(d_sig), + } + } + } + + #[test] + fn ser_dalek_primitives() { + for _ in 0..10 { + let s = SerTest::random(); + println!("Before Serialization: {:?}", s); + let serialized = serde_json::to_string_pretty(&s).unwrap(); + println!("JSON: {}", serialized); + let deserialized: SerTest = serde_json::from_str(&serialized).unwrap(); + println!("After Serialization: {:?}", deserialized); + println!(); + assert_eq!(s, deserialized); + } + } +} diff --git a/libwallet/src/slate_versions/v4.rs b/libwallet/src/slate_versions/v4.rs new file mode 100644 index 0000000..26c59e0 --- /dev/null +++ b/libwallet/src/slate_versions/v4.rs @@ -0,0 +1,387 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Contains V4 of the slate (grin-wallet 4.0.0) +//! Changes from V3: +//! /#### Top-Level Slate Struct + +//! * The `version_info` struct is removed, and is replaced with `ver`, which has the format "[version]:[block header version]" +//! * `sta` is added, with possible values S1|S2|S3|I1|I2|I3|NA +//! * `num_participants` is renamed to `num_parts` +//! * `num_parts` may be omitted from the slate. If omitted its value is assumed to be 2. +//! * `amount` is renamed to `amt` +//! * `amt` may be removed from the slate on the S2 phase of a transaction. +//! * `fee` may be removed from the slate on the S2 phase of a transaction. It may also be ommited when intiating an I1 transaction, and added during the I2 phase. +//! * `lock_height` is removed +//! * `feat` is added to the slate denoting the Kernel feature set. May be omitted from the slate if kernel is plain (0) +//! * `ttl_cutoff_height` is renamed to `ttl` +//! * `ttl` may be omitted from the slate. If omitted its value is assumed to be 0 (no TTL). +//! * The `participant_data` struct is renamed to `sigs` +//! * `tx` is removed +//! * The `coms` (commitments) array is added, from which the final transaction object can be reconstructed +//! * The `payment_proof` struct is renamed to `proof` +//! * The feat_args struct is added, which may be populated for non-Plain kernels +//! * `proof` may be omitted from the slate if it is None (null), +//! * `off` (offset) is added, and will be modified by every participant in the transaction with a random +//! value - the value of their inputs' blinding factors +//! +//! #### Participant Data (`sigs`) +//! +//! * `public_blind_excess` is renamed to `xs` +//! * `public_nonce` is renamed to `nonce` +//! * `part_sig` is renamed to `part` +//! * `part` may be omitted if it has not yet been filled out +//! * `message` is removed +//! * `message_sig` is removed +//! * `id` is removed. Parties can identify themselves via the keys stored in their transaction context +//! +//! #### Payment Proof Data (`proof`) +//! +//! * The `sender_address` field is renamed to `saddr` +//! * The `receiver_address` field is renamed to `raddr` +//! * The `receiver_signature` field is renamed to `rsig` +//! * `rsig` may be omitted if it has not yet been filled out + +use crate::grin_core::core::FeeFields; +use crate::grin_core::core::{Input, Output, TxKernel}; +use crate::grin_core::libtx::secp_ser; +use crate::grin_keychain::{BlindingFactor, Identifier}; +use crate::grin_util::secp; +use crate::grin_util::secp::key::PublicKey; +use crate::grin_util::secp::pedersen::{Commitment, RangeProof}; +use crate::grin_util::secp::Signature; +use crate::{slate_versions::ser, CbData}; +use ed25519_dalek::PublicKey as DalekPublicKey; +use ed25519_dalek::Signature as DalekSignature; +use uuid::Uuid; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct SlateV4 { + // Required Fields + /// Versioning info + #[serde(with = "ser::version_info_v4")] + pub ver: VersionCompatInfoV4, + /// Unique transaction ID, selected by sender + pub id: Uuid, + /// Slate state + #[serde(with = "ser::slate_state_v4")] + pub sta: SlateStateV4, + /// Offset, modified by each participant inserting inputs + /// as the transaction progresses + #[serde( + serialize_with = "secp_ser::as_hex", + deserialize_with = "secp_ser::blind_from_hex" + )] + #[serde(default = "default_offset_zero")] + #[serde(skip_serializing_if = "offset_is_zero")] + pub off: BlindingFactor, + // Optional fields depending on state + /// The number of participants intended to take part in this transaction + #[serde(default = "default_num_participants_2")] + #[serde(skip_serializing_if = "num_parts_is_2")] + pub num_parts: u8, + /// base amount (excluding fee) + #[serde(with = "secp_ser::string_or_u64")] + #[serde(skip_serializing_if = "u64_is_blank")] + #[serde(default = "default_u64_0")] + pub amt: u64, + /// fee + #[serde(skip_serializing_if = "fee_is_zero")] + #[serde(default = "default_fee")] + pub fee: FeeFields, + /// kernel features, if any + #[serde(skip_serializing_if = "u8_is_blank")] + #[serde(default = "default_u8_0")] + pub feat: u8, + /// TTL, the block height at which wallets + /// should refuse to process the transaction and unlock all + #[serde(with = "secp_ser::string_or_u64")] + #[serde(skip_serializing_if = "u64_is_blank")] + #[serde(default = "default_u64_0")] + pub ttl: u64, + // Structs always required + /// Participant data, each participant in the transaction will + /// insert their public data here. For now, 0 is sender and 1 + /// is receiver, though this will change for multi-party + pub sigs: Vec, + // Situational, but required at some point in the tx + /// Inputs/Output commits added to slate + #[serde(default = "default_coms_none")] + #[serde(skip_serializing_if = "Option::is_none")] + pub coms: Option>, + // Optional Structs + /// Payment Proof + #[serde(default = "default_payment_none")] + #[serde(skip_serializing_if = "Option::is_none")] + pub proof: Option, + /// Kernel features arguments + #[serde(default = "default_kernel_features_none")] + #[serde(skip_serializing_if = "Option::is_none")] + pub feat_args: Option, +} + +fn default_payment_none() -> Option { + None +} + +fn default_offset_zero() -> BlindingFactor { + BlindingFactor::zero() +} + +fn offset_is_zero(o: &BlindingFactor) -> bool { + *o == BlindingFactor::zero() +} + +fn default_coms_none() -> Option> { + None +} + +fn default_u64_0() -> u64 { + 0 +} + +fn num_parts_is_2(n: &u8) -> bool { + *n == 2 +} + +fn default_num_participants_2() -> u8 { + 2 +} + +fn default_kernel_features_none() -> Option { + None +} + +/// Slate state definition +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum SlateStateV4 { + /// Unknown, coming from earlier versions of the slate + Unknown, + /// Standard flow, freshly init + Standard1, + /// Standard flow, return journey + Standard2, + /// Standard flow, ready for transaction posting + Standard3, + /// Invoice flow, freshly init + Invoice1, + ///Invoice flow, return journey + Invoice2, + /// Invoice flow, ready for tranasction posting + Invoice3, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +/// Kernel features arguments definition +pub struct KernelFeaturesArgsV4 { + /// Lock height, for HeightLocked + pub lock_hgt: u64, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct VersionCompatInfoV4 { + /// The current version of the slate format + pub version: u16, + /// Version of grin block header this slate is compatible with + pub block_header_version: u16, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct ParticipantDataV4 { + /// Public key corresponding to private blinding factor + #[serde(with = "secp_ser::pubkey_serde")] + pub xs: PublicKey, + /// Public key corresponding to private nonce + #[serde(with = "secp_ser::pubkey_serde")] + pub nonce: PublicKey, + /// Public partial signature + #[serde(default = "default_part_sig_none")] + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(with = "secp_ser::option_sig_serde")] + pub part: Option, +} + +fn default_part_sig_none() -> Option { + None +} + +#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] +pub struct PaymentInfoV4 { + #[serde(with = "ser::dalek_pubkey_serde")] + pub saddr: DalekPublicKey, + #[serde(with = "ser::dalek_pubkey_serde")] + pub raddr: DalekPublicKey, + #[serde(default = "default_receiver_signature_none")] + #[serde(with = "ser::option_dalek_sig_serde")] + #[serde(skip_serializing_if = "Option::is_none")] + pub rsig: Option, +} + +fn default_receiver_signature_none() -> Option { + None +} + +#[derive(Debug, Copy, Clone, Serialize, Deserialize)] +pub struct CommitsV4 { + /// Options for an output's structure or use + #[serde(default = "default_output_feature")] + #[serde(skip_serializing_if = "output_feature_is_plain")] + pub f: OutputFeaturesV4, + /// The homomorphic commitment representing the output amount + #[serde( + serialize_with = "secp_ser::as_hex", + deserialize_with = "secp_ser::commitment_from_hex" + )] + pub c: Commitment, + /// A proof that the commitment is in the right range + /// Only applies for transaction outputs + #[serde(with = "ser::option_rangeproof_hex")] + #[serde(default = "default_range_proof")] + #[serde(skip_serializing_if = "Option::is_none")] + pub p: Option, +} + +impl From<&Output> for CommitsV4 { + fn from(out: &Output) -> CommitsV4 { + CommitsV4 { + f: out.features().into(), + c: out.commitment(), + p: Some(out.proof()), + } + } +} + +// This will need to be reworked once we no longer support input features with "commit only" inputs. +impl From<&Input> for CommitsV4 { + fn from(input: &Input) -> CommitsV4 { + CommitsV4 { + f: input.features.into(), + c: input.commitment(), + p: None, + } + } +} + +fn default_output_feature() -> OutputFeaturesV4 { + OutputFeaturesV4(0) +} + +fn output_feature_is_plain(o: &OutputFeaturesV4) -> bool { + o.0 == 0 +} + +#[derive(Serialize, Deserialize, Copy, Debug, Clone, PartialEq, Eq)] +pub struct OutputFeaturesV4(pub u8); + +pub fn sig_is_blank(s: &secp::Signature) -> bool { + for b in s.to_raw_data().iter() { + if *b != 0 { + return false; + } + } + true +} + +fn default_range_proof() -> Option { + None +} + +fn u64_is_blank(u: &u64) -> bool { + *u == 0 +} + +fn default_u8_0() -> u8 { + 0 +} + +fn u8_is_blank(u: &u8) -> bool { + *u == 0 +} + +fn fee_is_zero(f: &FeeFields) -> bool { + f.is_zero() +} + +fn default_fee() -> FeeFields { + FeeFields::zero() +} + +/// A mining node requests new coinbase via the foreign api every time a new candidate block is built. +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct CoinbaseV4 { + /// Output + output: CbOutputV4, + /// Kernel + kernel: CbKernelV4, + /// Key Id + key_id: Option, +} + +impl From for CoinbaseV4 { + fn from(cb: CbData) -> CoinbaseV4 { + CoinbaseV4 { + output: CbOutputV4::from(&cb.output), + kernel: CbKernelV4::from(&cb.kernel), + key_id: cb.key_id, + } + } +} + +impl From<&Output> for CbOutputV4 { + fn from(output: &Output) -> CbOutputV4 { + CbOutputV4 { + features: CbOutputFeatures::Coinbase, + commit: output.commitment(), + proof: output.proof(), + } + } +} + +impl From<&TxKernel> for CbKernelV4 { + fn from(kernel: &TxKernel) -> CbKernelV4 { + CbKernelV4 { + features: CbKernelFeatures::Coinbase, + excess: kernel.excess, + excess_sig: kernel.excess_sig, + } + } +} + +#[derive(Debug, Copy, Clone, Serialize, Deserialize)] +enum CbOutputFeatures { + Coinbase, +} + +#[derive(Debug, Copy, Clone, Serialize, Deserialize)] +enum CbKernelFeatures { + Coinbase, +} + +#[derive(Debug, Copy, Clone, Serialize, Deserialize)] +struct CbOutputV4 { + features: CbOutputFeatures, + #[serde(serialize_with = "secp_ser::as_hex")] + commit: Commitment, + #[serde(serialize_with = "secp_ser::as_hex")] + proof: RangeProof, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +struct CbKernelV4 { + features: CbKernelFeatures, + #[serde(serialize_with = "secp_ser::as_hex")] + excess: Commitment, + #[serde(with = "secp_ser::sig_serde")] + excess_sig: secp::Signature, +} diff --git a/libwallet/src/slate_versions/v4_bin.rs b/libwallet/src/slate_versions/v4_bin.rs new file mode 100644 index 0000000..68f5190 --- /dev/null +++ b/libwallet/src/slate_versions/v4_bin.rs @@ -0,0 +1,592 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Wraps a V4 Slate into a V4 Binary slate + +use crate::grin_core::core::transaction::{FeeFields, OutputFeatures}; +use crate::grin_core::ser as grin_ser; +use crate::grin_core::ser::{Readable, Reader, Writeable, Writer}; +use crate::grin_keychain::BlindingFactor; +use crate::grin_util::secp::key::PublicKey; +use crate::grin_util::secp::pedersen::{Commitment, RangeProof}; +use crate::grin_util::secp::Signature; +use ed25519_dalek::PublicKey as DalekPublicKey; +use ed25519_dalek::Signature as DalekSignature; +use std::convert::TryFrom; +use uuid::Uuid; + +use crate::slate_versions::v4::{ + CommitsV4, KernelFeaturesArgsV4, ParticipantDataV4, PaymentInfoV4, SlateStateV4, SlateV4, + VersionCompatInfoV4, +}; + +impl Writeable for SlateStateV4 { + fn write(&self, writer: &mut W) -> Result<(), grin_ser::Error> { + let b = match self { + SlateStateV4::Unknown => 0, + SlateStateV4::Standard1 => 1, + SlateStateV4::Standard2 => 2, + SlateStateV4::Standard3 => 3, + SlateStateV4::Invoice1 => 4, + SlateStateV4::Invoice2 => 5, + SlateStateV4::Invoice3 => 6, + }; + writer.write_u8(b) + } +} + +impl Readable for SlateStateV4 { + fn read(reader: &mut R) -> Result { + let b = reader.read_u8()?; + let sta = match b { + 0 => SlateStateV4::Unknown, + 1 => SlateStateV4::Standard1, + 2 => SlateStateV4::Standard2, + 3 => SlateStateV4::Standard3, + 4 => SlateStateV4::Invoice1, + 5 => SlateStateV4::Invoice2, + 6 => SlateStateV4::Invoice3, + _ => SlateStateV4::Unknown, + }; + Ok(sta) + } +} + +/// Allow serializing of Uuids not defined in crate +struct UuidWrap(Uuid); + +impl Writeable for UuidWrap { + fn write(&self, writer: &mut W) -> Result<(), grin_ser::Error> { + writer.write_fixed_bytes(&self.0.as_bytes()) + } +} + +impl Readable for UuidWrap { + fn read(reader: &mut R) -> Result { + let bytes = reader.read_fixed_bytes(16)?; + let mut b = [0u8; 16]; + b.copy_from_slice(&bytes[0..16]); + Ok(UuidWrap(Uuid::from_bytes(b))) + } +} + +/// Helper struct to serialize optional fields efficiently +struct SlateOptFields { + /// num parts, default 2 + pub num_parts: u8, + /// amt, default 0 + pub amt: u64, + /// fee_fields, default FeeFields::zero() + pub fee: FeeFields, + /// kernel features, default 0 + pub feat: u8, + /// ttl, default 0 + pub ttl: u64, +} + +impl Writeable for SlateOptFields { + fn write(&self, writer: &mut W) -> Result<(), grin_ser::Error> { + // Status byte, bits determing which optional fields are serialized + // 0 0 0 1 1 1 1 1 + // t f f a n + let mut status = 0u8; + if self.num_parts != 2 { + status |= 0x01; + } + if self.amt > 0 { + status |= 0x02; + } + if self.fee.fee() > 0 { + // apply fee mask past HF4 + status |= 0x04; + } + if self.feat > 0 { + status |= 0x08; + } + if self.ttl > 0 { + status |= 0x10; + } + writer.write_u8(status)?; + if status & 0x01 > 0 { + writer.write_u8(self.num_parts)?; + } + if status & 0x02 > 0 { + writer.write_u64(self.amt)?; + } + if status & 0x04 > 0 { + self.fee.write(writer)?; + } + if status & 0x08 > 0 { + writer.write_u8(self.feat)?; + } + if status & 0x10 > 0 { + writer.write_u64(self.ttl)?; + } + Ok(()) + } +} + +impl Readable for SlateOptFields { + fn read(reader: &mut R) -> Result { + let status = reader.read_u8()?; + let num_parts = if status & 0x01 > 0 { + reader.read_u8()? + } else { + 2 + }; + let amt = if status & 0x02 > 0 { + reader.read_u64()? + } else { + 0 + }; + let fee = if status & 0x04 > 0 { + FeeFields::read(reader)? + } else { + FeeFields::zero() + }; + let feat = if status & 0x08 > 0 { + reader.read_u8()? + } else { + 0 + }; + let ttl = if status & 0x10 > 0 { + reader.read_u64()? + } else { + 0 + }; + Ok(SlateOptFields { + num_parts, + amt, + fee, + feat, + ttl, + }) + } +} + +struct SigsWrap(Vec); +struct SigsWrapRef<'a>(&'a Vec); + +impl<'a> Writeable for SigsWrapRef<'a> { + fn write(&self, writer: &mut W) -> Result<(), grin_ser::Error> { + writer.write_u8(self.0.len() as u8)?; + for s in self.0.iter() { + //0 means part sig is not yet included + //1 means part sig included + if s.part.is_some() { + writer.write_u8(1)?; + } else { + writer.write_u8(0)?; + } + s.xs.write(writer)?; + s.nonce.write(writer)?; + if let Some(s) = s.part { + s.write(writer)?; + } + } + Ok(()) + } +} + +impl Readable for SigsWrap { + fn read(reader: &mut R) -> Result { + let sigs_len = reader.read_u8()?; + let sigs = { + let mut ret = vec![]; + for _ in 0..sigs_len as usize { + let has_partial = reader.read_u8()?; + let c = ParticipantDataV4 { + xs: PublicKey::read(reader)?, + nonce: PublicKey::read(reader)?, + part: match has_partial { + 1 => Some(Signature::read(reader)?), + 0 | _ => None, + }, + }; + ret.push(c); + } + ret + }; + Ok(SigsWrap(sigs)) + } +} + +/// Serialization of optional structs +struct SlateOptStructsRef<'a> { + /// coms, default none + pub coms: &'a Option>, + ///// proof, default none + pub proof: &'a Option, +} + +/// Serialization of optional structs +struct SlateOptStructs { + /// coms, default none + pub coms: Option>, + /// proof, default none + pub proof: Option, +} + +impl<'a> Writeable for SlateOptStructsRef<'a> { + fn write(&self, writer: &mut W) -> Result<(), grin_ser::Error> { + // Status byte, bits determing which optional structs are serialized + // 0 0 0 0 0 0 1 1 + // p c + let mut status = 0u8; + if self.coms.is_some() { + status |= 0x01 + }; + if self.proof.is_some() { + status |= 0x02 + }; + writer.write_u8(status)?; + if let Some(c) = self.coms { + ComsWrapRef(&c).write(writer)?; + } + if let Some(p) = self.proof { + ProofWrapRef(&p).write(writer)?; + } + Ok(()) + } +} + +impl Readable for SlateOptStructs { + fn read(reader: &mut R) -> Result { + let status = reader.read_u8()?; + let coms = if status & 0x01 > 0 { + Some(ComsWrap::read(reader)?.0) + } else { + None + }; + let proof = if status & 0x02 > 0 { + Some(ProofWrap::read(reader)?.0) + } else { + None + }; + Ok(SlateOptStructs { coms, proof }) + } +} + +struct ComsWrap(Vec); +struct ComsWrapRef<'a>(&'a Vec); + +impl<'a> Writeable for ComsWrapRef<'a> { + fn write(&self, writer: &mut W) -> Result<(), grin_ser::Error> { + writer.write_u16(self.0.len() as u16)?; + for o in self.0.iter() { + //0 means input + //1 means output with proof + if o.p.is_some() { + writer.write_u8(1)?; + } else { + writer.write_u8(0)?; + } + OutputFeatures::from(o.f).write(writer)?; + o.c.write(writer)?; + if let Some(p) = o.p { + p.write(writer)?; + } + } + Ok(()) + } +} + +impl Readable for ComsWrap { + fn read(reader: &mut R) -> Result { + let coms_len = reader.read_u16()?; + let coms = { + let mut ret = vec![]; + for _ in 0..coms_len as usize { + let is_output = reader.read_u8()?; + let c = CommitsV4 { + f: OutputFeatures::read(reader)?.into(), + c: Commitment::read(reader)?, + p: match is_output { + 1 => Some(RangeProof::read(reader)?), + 0 | _ => None, + }, + }; + ret.push(c); + } + ret + }; + Ok(ComsWrap(coms)) + } +} + +struct ProofWrap(PaymentInfoV4); +struct ProofWrapRef<'a>(&'a PaymentInfoV4); + +impl<'a> Writeable for ProofWrapRef<'a> { + fn write(&self, writer: &mut W) -> Result<(), grin_ser::Error> { + writer.write_fixed_bytes(self.0.saddr.to_bytes())?; + writer.write_fixed_bytes(self.0.raddr.to_bytes())?; + match self.0.rsig { + Some(s) => { + writer.write_u8(1)?; + writer.write_fixed_bytes(&s.to_bytes().to_vec())?; + } + None => writer.write_u8(0)?, + } + Ok(()) + } +} + +impl Readable for ProofWrap { + fn read(reader: &mut R) -> Result { + let saddr = DalekPublicKey::from_bytes(&reader.read_fixed_bytes(32)?).unwrap(); + let raddr = DalekPublicKey::from_bytes(&reader.read_fixed_bytes(32)?).unwrap(); + let rsig = match reader.read_u8()? { + 0 => None, + 1 | _ => Some(DalekSignature::try_from(&reader.read_fixed_bytes(64)?[..]).unwrap()), + }; + Ok(ProofWrap(PaymentInfoV4 { saddr, raddr, rsig })) + } +} + +#[derive(Debug, Clone)] +pub struct SlateV4Bin(pub SlateV4); + +impl From for SlateV4Bin { + fn from(slate: SlateV4) -> SlateV4Bin { + SlateV4Bin(slate) + } +} + +impl From for SlateV4 { + fn from(slate: SlateV4Bin) -> SlateV4 { + slate.0 + } +} + +impl serde::Serialize for SlateV4Bin { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut vec = vec![]; + grin_ser::serialize(&mut vec, grin_ser::ProtocolVersion(4), self) + .map_err(|err| serde::ser::Error::custom(err.to_string()))?; + serializer.serialize_bytes(&vec) + } +} + +impl<'de> serde::Deserialize<'de> for SlateV4Bin { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct SlateV4BinVisitor; + + impl<'de> serde::de::Visitor<'de> for SlateV4BinVisitor { + type Value = SlateV4Bin; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(formatter, "a serialised binary V4 slate") + } + + fn visit_bytes(self, value: &[u8]) -> Result + where + E: serde::de::Error, + { + let mut reader = std::io::Cursor::new(value.to_vec()); + let s = grin_ser::deserialize( + &mut reader, + grin_ser::ProtocolVersion(4), + grin_ser::DeserializationMode::default(), + ) + .map_err(|err| serde::de::Error::custom(err.to_string()))?; + Ok(s) + } + } + deserializer.deserialize_bytes(SlateV4BinVisitor) + } +} + +impl Writeable for SlateV4Bin { + fn write(&self, writer: &mut W) -> Result<(), grin_ser::Error> { + let v4 = &self.0; + writer.write_u16(v4.ver.version)?; + writer.write_u16(v4.ver.block_header_version)?; + (UuidWrap(v4.id)).write(writer)?; + v4.sta.write(writer)?; + v4.off.write(writer)?; + SlateOptFields { + num_parts: v4.num_parts, + amt: v4.amt, + fee: v4.fee, + feat: v4.feat, + ttl: v4.ttl, + } + .write(writer)?; + (SigsWrapRef(&v4.sigs)).write(writer)?; + SlateOptStructsRef { + coms: &v4.coms, + proof: &v4.proof, + } + .write(writer)?; + // Write lock height for height locked kernels + if v4.feat == 2 { + let lock_hgt = match &v4.feat_args { + Some(l) => l.lock_hgt, + None => 0, + }; + writer.write_u64(lock_hgt)?; + } + Ok(()) + } +} + +impl Readable for SlateV4Bin { + fn read(reader: &mut R) -> Result { + let ver = VersionCompatInfoV4 { + version: reader.read_u16()?, + block_header_version: reader.read_u16()?, + }; + let id = UuidWrap::read(reader)?.0; + let sta = SlateStateV4::read(reader)?; + let off = BlindingFactor::read(reader)?; + + let opts = SlateOptFields::read(reader)?; + let sigs = SigsWrap::read(reader)?.0; + let opt_structs = SlateOptStructs::read(reader)?; + + let feat_args = if opts.feat == 2 { + Some(KernelFeaturesArgsV4 { + lock_hgt: reader.read_u64()?, + }) + } else { + None + }; + + Ok(SlateV4Bin(SlateV4 { + ver, + id, + sta, + off, + num_parts: opts.num_parts, + amt: opts.amt, + fee: opts.fee, + feat: opts.feat, + ttl: opts.ttl, + sigs, + coms: opt_structs.coms, + proof: opt_structs.proof, + feat_args, + })) + } +} + +#[test] +fn slate_v4_serialize_deserialize() { + use crate::grin_util::from_hex; + use crate::grin_util::secp::key::PublicKey; + use crate::Slate; + use grin_core::global::{set_local_chain_type, ChainTypes}; + use grin_keychain::{ExtKeychain, Keychain, SwitchCommitmentType}; + set_local_chain_type(ChainTypes::Mainnet); + let slate = Slate::blank(1, false); + let mut v4 = SlateV4::from(slate); + + let keychain = ExtKeychain::from_random_seed(true).unwrap(); + let switch = SwitchCommitmentType::Regular; + // add some sig data + let id1 = ExtKeychain::derive_key_id(1, 1, 0, 0, 0); + let id2 = ExtKeychain::derive_key_id(1, 1, 1, 0, 0); + let skey1 = keychain.derive_key(0, &id1, switch).unwrap(); + let skey2 = keychain.derive_key(0, &id2, switch).unwrap(); + let xs = PublicKey::from_secret_key(keychain.secp(), &skey1).unwrap(); + let nonce = PublicKey::from_secret_key(keychain.secp(), &skey2).unwrap(); + let part = ParticipantDataV4 { + xs, + nonce, + part: None, + }; + let part2 = ParticipantDataV4 { + xs, + nonce, + part: Some(Signature::from_raw_data(&[11; 64]).unwrap()), + }; + v4.sigs.push(part.clone()); + v4.sigs.push(part2); + v4.sigs.push(part); + + // add some random commit data + let com1 = CommitsV4 { + f: OutputFeatures::Plain.into(), + c: Commitment::from_vec([3u8; 1].to_vec()), + p: None, + }; + let com2 = CommitsV4 { + f: OutputFeatures::Plain.into(), + c: Commitment::from_vec([4u8; 1].to_vec()), + p: Some(RangeProof::zero()), + }; + let mut coms = vec![]; + coms.push(com1.clone()); + coms.push(com1.clone()); + coms.push(com1.clone()); + coms.push(com2); + + v4.coms = Some(coms); + v4.amt = 234324899824; + v4.feat = 1; + v4.num_parts = 2; + v4.feat_args = Some(KernelFeaturesArgsV4 { lock_hgt: 23092039 }); + let v4_1 = v4.clone(); + let v4_1_copy = v4.clone(); + + let v4_bin = SlateV4Bin(v4); + let mut vec = Vec::new(); + let _ = grin_ser::serialize_default(&mut vec, &v4_bin).expect("serialization failed"); + let b4_bin_2: SlateV4Bin = grin_ser::deserialize_default(&mut &vec[..]).unwrap(); + let v4_2 = b4_bin_2.0.clone(); + assert_eq!(v4_1.ver, v4_2.ver); + assert_eq!(v4_1.id, v4_2.id); + assert_eq!(v4_1.amt, v4_2.amt); + assert_eq!(v4_1.fee, v4_2.fee); + let v4_2_coms = v4_2.coms.as_ref().unwrap().clone(); + for (i, c) in v4_1.coms.unwrap().iter().enumerate() { + assert_eq!(c.f, v4_2_coms[i].f); + assert_eq!(c.c, v4_2_coms[i].c); + assert_eq!(c.p, v4_2_coms[i].p); + } + assert_eq!(v4_1.sigs, v4_2.sigs); + assert_eq!(v4_1.proof, v4_2.proof); + + // Include Payment proof, remove coms to mix it up a bit + let mut v4 = v4_1_copy; + let raw_pubkey_str = "d03c09e9c19bb74aa9ea44e0fe5ae237a9bf40bddf0941064a80913a4459c8bb"; + let b = from_hex(raw_pubkey_str).unwrap(); + let d_pkey = DalekPublicKey::from_bytes(&b).unwrap(); + v4.proof = Some(PaymentInfoV4 { + raddr: d_pkey.clone(), + saddr: d_pkey.clone(), + rsig: None, + }); + v4.coms = None; + let v4_1 = v4.clone(); + let v4_bin = SlateV4Bin(v4); + let mut vec = Vec::new(); + let _ = grin_ser::serialize_default(&mut vec, &v4_bin).expect("serialization failed"); + let b4_bin_2: SlateV4Bin = grin_ser::deserialize_default(&mut &vec[..]).unwrap(); + let v4_2 = b4_bin_2.0.clone(); + assert_eq!(v4_1.ver, v4_2.ver); + assert_eq!(v4_1.id, v4_2.id); + assert_eq!(v4_1.amt, v4_2.amt); + assert_eq!(v4_1.fee, v4_2.fee); + assert!(v4_1.coms.is_none()); + assert_eq!(v4_1.sigs, v4_2.sigs); + assert_eq!(v4_1.proof, v4_2.proof); +} diff --git a/libwallet/src/slatepack/address.rs b/libwallet/src/slatepack/address.rs new file mode 100644 index 0000000..15c08b8 --- /dev/null +++ b/libwallet/src/slatepack/address.rs @@ -0,0 +1,287 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use bech32::{self, FromBase32, ToBase32}; +/// Slatepack Address definition +use ed25519_dalek::PublicKey as edDalekPublicKey; +use ed25519_dalek::SecretKey as edDalekSecretKey; +use rand::{thread_rng, Rng}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use x25519_dalek::PublicKey as xDalekPublicKey; + +use crate::grin_core::global; +use crate::grin_core::ser::{self, Readable, Reader, Writeable, Writer}; +use crate::grin_util::secp::key::SecretKey; +use crate::util::OnionV3Address; +use crate::Error; + +use std::convert::TryFrom; +use std::fmt::{self, Display}; + +/// Definition of a Slatepack address +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct SlatepackAddress { + /// Human-readable prefix + pub hrp: String, + /// ed25519 Public key, to be bech32 encoded, + /// interpreted as tor address or converted + /// to an X25519 public key for encrypting + /// slatepacks + pub pub_key: edDalekPublicKey, +} + +impl SlatepackAddress { + /// new with default hrp + pub fn new(pub_key: &edDalekPublicKey) -> Self { + let hrp = match global::get_chain_type() { + global::ChainTypes::Mainnet => "grin", + _ => "tgrin", + }; + Self { + hrp: String::from(hrp), + pub_key: pub_key.clone(), + } + } + + /// new with a random key + pub fn random() -> Self { + let bytes: [u8; 32] = thread_rng().gen(); + let pub_key = edDalekPublicKey::from(&edDalekSecretKey::from_bytes(&bytes).unwrap()); + SlatepackAddress::new(&pub_key) + } + + /// calculate encoded length + pub fn encoded_len(&self) -> Result { + let encoded = String::try_from(self)?; + // add length byte + Ok(encoded.as_bytes().len() + 1) + } + + /// utility to construct a public key that can be read by age 0.5+, + /// for some reason the author decided the library can no longer accept + /// x25519 keys to construct its types even though it uses them under the hood + pub fn to_age_pubkey_str(&self) -> Result { + let x_key = xDalekPublicKey::try_from(self)?; + Ok(bech32::encode("age", x_key.as_bytes().to_base32())?.to_string()) + } +} + +impl Display for SlatepackAddress { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str(&String::try_from(self).unwrap()) + } +} + +impl TryFrom<&str> for SlatepackAddress { + type Error = Error; + fn try_from(encoded: &str) -> Result { + let (hrp, data) = bech32::decode(&encoded)?; + let bytes = Vec::::from_base32(&data)?; + let pub_key = match edDalekPublicKey::from_bytes(&bytes) { + Ok(k) => k, + Err(e) => { + return Err(Error::ED25519Key(format!("{}", e))); + } + }; + Ok(SlatepackAddress { hrp, pub_key }) + } +} + +impl TryFrom<&SlatepackAddress> for String { + type Error = Error; + fn try_from(addr: &SlatepackAddress) -> Result { + let encoded = bech32::encode(&addr.hrp, addr.pub_key.to_bytes().to_base32())?; + Ok(encoded.to_string()) + } +} + +impl From<&SlatepackAddress> for OnionV3Address { + fn from(addr: &SlatepackAddress) -> Self { + OnionV3Address::from_bytes(addr.pub_key.to_bytes()) + } +} + +impl TryFrom for SlatepackAddress { + type Error = Error; + fn try_from(addr: OnionV3Address) -> Result { + Ok(SlatepackAddress::new(&addr.to_ed25519()?)) + } +} + +impl TryFrom<&SlatepackAddress> for xDalekPublicKey { + type Error = Error; + fn try_from(addr: &SlatepackAddress) -> Result { + let cep = + curve25519_dalek::edwards::CompressedEdwardsY::from_slice(addr.pub_key.as_bytes()); + let ep = match cep.decompress() { + Some(p) => p, + None => { + return Err(Error::ED25519Key( + "Can't decompress ed25519 Edwards Point".into(), + )); + } + }; + let res = xDalekPublicKey::from(ep.to_montgomery().to_bytes()); + Ok(res) + } +} + +impl TryFrom<&SecretKey> for SlatepackAddress { + type Error = Error; + fn try_from(key: &SecretKey) -> Result { + let d_skey = match edDalekSecretKey::from_bytes(&key.0) { + Ok(k) => k, + Err(e) => { + return Err(Error::ED25519Key(format!( + "Can't create slatepack address from SecretKey: {}", + e + ))); + } + }; + let d_pub_key: edDalekPublicKey = (&d_skey).into(); + Ok(Self::new(&d_pub_key)) + } +} + +/// Serializes a SlatepackAddress to a bech32 string +impl Serialize for SlatepackAddress { + /// + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str( + &String::try_from(self).map_err(|err| serde::ser::Error::custom(err.to_string()))?, + ) + } +} + +/// Deserialize from a bech32 string +impl<'de> Deserialize<'de> for SlatepackAddress { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct SlatepackAddressVisitor; + + impl<'de> serde::de::Visitor<'de> for SlatepackAddressVisitor { + type Value = SlatepackAddress; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(formatter, "a SlatepackAddress") + } + + fn visit_str(self, value: &str) -> Result + where + E: serde::de::Error, + { + let s = SlatepackAddress::try_from(value) + .map_err(|err| serde::de::Error::custom(err.to_string()))?; + Ok(s) + } + } + + deserializer.deserialize_str(SlatepackAddressVisitor) + } +} + +/// write binary trait +impl Writeable for SlatepackAddress { + fn write(&self, writer: &mut W) -> Result<(), ser::Error> { + // We're actually going to encode the bech32 address as opposed to + // the binary serialization + let encoded = match String::try_from(self) { + Ok(e) => e, + Err(e) => { + error!("Cannot parse Slatepack address: {:?}, {}", self, e); + return Err(ser::Error::CorruptedData); + } + }; + // write length, max 255 + let bytes = encoded.as_bytes(); + if bytes.len() > 255 { + error!( + "Cannot encode Slatepackaddress: {:?}, Too Long (Max 255)", + self + ); + return Err(ser::Error::CorruptedData); + } + writer.write_u8(bytes.len() as u8)?; + writer.write_fixed_bytes(&bytes) + } +} + +impl Readable for SlatepackAddress { + fn read(reader: &mut R) -> Result { + // read length as u8 + let len = reader.read_u8()?; + // and bech32 string + let encoded = match String::from_utf8(reader.read_fixed_bytes(len as usize)?) { + Ok(a) => a, + Err(e) => { + error!("Cannot parse Slatepack address from utf8: {}", e); + return Err(ser::Error::CorruptedData); + } + }; + let parsed_addr = match SlatepackAddress::try_from(encoded.as_str()) { + Ok(a) => a, + Err(e) => { + error!("Cannot parse Slatepack address: {}, {}", encoded, e); + return Err(ser::Error::CorruptedData); + } + }; + Ok(parsed_addr) + } +} + +#[test] +fn slatepack_address() -> Result<(), Error> { + use rand::{thread_rng, Rng}; + global::set_local_chain_type(global::ChainTypes::AutomatedTesting); + let sec_key_bytes: [u8; 32] = thread_rng().gen(); + + let ed_sec_key = edDalekSecretKey::from_bytes(&sec_key_bytes).unwrap(); + let ed_pub_key = edDalekPublicKey::from(&ed_sec_key); + let addr = SlatepackAddress::new(&ed_pub_key); + let x_pub_key = xDalekPublicKey::try_from(&addr)?; + + let x_dec_secret = x25519_dalek::StaticSecret::from(sec_key_bytes); + let x_pub_key_direct = xDalekPublicKey::from(&x_dec_secret); + + println!("ed sec key: {:?}", ed_sec_key); + println!("ed pub key: {:?}", ed_pub_key); + println!("x pub key from addr: {:?}", x_pub_key); + println!("x pub key direct: {:?}", x_pub_key_direct); + + let encoded = String::try_from(&addr).unwrap(); + println!("Encoded bech32: {}", encoded); + let parsed_addr = SlatepackAddress::try_from(encoded.as_str()).unwrap(); + assert_eq!(addr, parsed_addr); + + // ensure ed25519 pub keys and x25519 pubkeys are equivalent on decryption + let mut slatepack = super::Slatepack::default(); + let mut payload: Vec = Vec::with_capacity(243); + for _ in 0..payload.capacity() { + payload.push(rand::random()); + } + slatepack.payload = payload; + let orig_sp = slatepack.clone(); + + slatepack.try_encrypt_payload(vec![addr.clone()])?; + slatepack.try_decrypt_payload(Some(&ed_sec_key))?; + + assert_eq!(orig_sp.payload, slatepack.payload); + + Ok(()) +} diff --git a/libwallet/src/slatepack/armor.rs b/libwallet/src/slatepack/armor.rs new file mode 100644 index 0000000..eacdf32 --- /dev/null +++ b/libwallet/src/slatepack/armor.rs @@ -0,0 +1,198 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +// See the License for the specific language governing permissions and +// limitations under the License. + +// A note on encoding efficiency: 0.75 for Base64, 0.744 for Base62, 0.732 for Base58 +// slatepack uses a modified Base58Check encoding to create armored slate payloads: +// 1. Take first four bytes of SHA256(SHA256(slate.as_bytes())) +// 2. Concatenate result of step 1 and slate.as_bytes() +// 3. Base58 encode bytes from step 2 +// Finally add armor framing and space/newline formatting as desired + +use crate::Error; +use grin_core::global::max_tx_weight; +use grin_wallet_util::byte_ser; +use regex::Regex; +use sha2::{Digest, Sha256}; +use std::str; + +use super::types::{Slatepack, SlatepackBin}; + +// Framing and formatting for slate armor +pub static HEADER: &str = "BEGINSLATEPACK."; +static FOOTER: &str = ". ENDSLATEPACK."; +const WORD_LENGTH: usize = 15; +const WORDS_PER_LINE: usize = 200; +const WEIGHT_RATIO: u64 = 32; + +/// Maximum size for an armored Slatepack file +pub fn max_size() -> u64 { + max_tx_weight() + .saturating_mul(WEIGHT_RATIO) + .saturating_add(HEADER.len() as u64) + .saturating_add(FOOTER.len() as u64) +} + +/// Minimum size for an armored Slatepack file or stream +pub fn min_size() -> u64 { + HEADER.len() as u64 +} + +lazy_static! { + static ref HEADER_REGEX: Regex = + Regex::new(concat!(r"^[>\n\r\t ]*BEGINSLATEPACK[>\n\r\t ]*$")).unwrap(); + static ref FOOTER_REGEX: Regex = + Regex::new(concat!(r"^[>\n\r\t ]*ENDSLATEPACK[>\n\r\t ]*$")).unwrap(); + static ref WHITESPACE_LIST: [u8; 5] = [b'>', b'\n', b'\r', b'\t', b' ']; +} + +/// Wrapper for associated functions +pub struct SlatepackArmor; + +impl SlatepackArmor { + /// Decode an armored Slatepack + pub fn decode(armor_bytes: &[u8]) -> Result, Error> { + // Collect the bytes up to the first period, this is the header + let header_bytes = armor_bytes + .iter() + .take_while(|byte| **byte != b'.') + .cloned() + .collect::>(); + // Verify the header... + check_header(&header_bytes)?; + // Get the length of the header + let header_len = header_bytes.len() + 1; + // Skip the length of the header to read for the payload until the next period + let payload_bytes = armor_bytes[header_len as usize..] + .iter() + .take_while(|byte| **byte != b'.') + .cloned() + .collect::>(); + // Get length of the payload to check the footer framing + let payload_len = payload_bytes.len(); + // Get footer bytes and verify them + let consumed_bytes = header_len + payload_len + 1; + let footer_bytes = armor_bytes[consumed_bytes as usize..] + .iter() + .take_while(|byte| **byte != b'.') + .cloned() + .collect::>(); + check_footer(&footer_bytes)?; + // Clean up the payload bytes to be deserialized + let clean_payload = payload_bytes + .iter() + .filter(|byte| !WHITESPACE_LIST.contains(byte)) + .cloned() + .collect::>(); + // Decode payload from base58 + let base_decode = bs58::decode(&clean_payload) + .into_vec() + .map_err(|_| Error::SlatepackDeser("Bad bytes".into()))?; + let error_code = &base_decode[0..4]; + let slatepack_bytes = &base_decode[4..]; + // Make sure the error check code is valid for the slate data + error_check(error_code, slatepack_bytes)?; + // Return slate as binary or string + Ok(slatepack_bytes.to_vec()) + } + + /// Encode an armored slatepack + pub fn encode(slatepack: &Slatepack) -> Result { + let slatepack_bytes = byte_ser::to_bytes(&SlatepackBin(slatepack.clone())) + .map_err(|_| Error::SlatepackSer)?; + let encoded_slatepack = base58check(&slatepack_bytes)?; + let formatted_slatepack = format_slatepack(&format!("{}{}", HEADER, encoded_slatepack))?; + Ok(format!("{}{}\n", formatted_slatepack, FOOTER)) + } +} + +// Takes an error check code and a slate binary and verifies that the code was generated from slate +fn error_check(error_code: &[u8], slate_bytes: &[u8]) -> Result<(), Error> { + let new_check = generate_check(slate_bytes)?; + if error_code.iter().eq(new_check.iter()) { + Ok(()) + } else { + Err(Error::InvalidSlatepackData( + "Bad slate error code- some data was corrupted".to_string(), + )) + } +} + +// Checks header framing bytes and returns an error if they are invalid +fn check_header(header: &[u8]) -> Result<(), Error> { + let framing = str::from_utf8(header).map_err(|_| Error::SlatepackDeser("Bad bytes".into()))?; + if HEADER_REGEX.is_match(framing) { + Ok(()) + } else { + Err(Error::InvalidSlatepackData("Bad armor header".to_string())) + } +} + +// Checks footer framing bytes and returns an error if they are invalid +fn check_footer(footer: &[u8]) -> Result<(), Error> { + let framing = str::from_utf8(footer).map_err(|_| Error::SlatepackDeser("Bad bytes".into()))?; + if FOOTER_REGEX.is_match(framing) { + Ok(()) + } else { + Err(Error::InvalidSlatepackData("Bad armor footer".to_string())) + } +} + +// MODIFIED Base58Check encoding for slate bytes +fn base58check(slate: &[u8]) -> Result { + // Serialize the slate json string to a vector of bytes + let mut slate_bytes: Vec = slate.to_vec(); + // Get the four byte checksum for the slate binary + let mut check_bytes: Vec = generate_check(&slate_bytes)?; + // Make a new buffer and concatenate checksum bytes and slate bytes + let mut slate_buf = Vec::new(); + slate_buf.append(&mut check_bytes); + slate_buf.append(&mut slate_bytes); + // Encode the slate buffer containing the slate binary and checksum bytes as Base58 + let b58_slate = bs58::encode(slate_buf).into_string(); + Ok(b58_slate) +} + +// Adds human readable formatting to the slate payload for armoring +fn format_slatepack(slatepack: &str) -> Result { + let formatter = slatepack + .chars() + .enumerate() + .flat_map(|(i, c)| { + if i != 0 && i % WORD_LENGTH == 0 { + if WORDS_PER_LINE != 0 && i % (WORD_LENGTH * WORDS_PER_LINE) == 0 { + Some('\n') + } else { + Some(' ') + } + } else { + None + } + .into_iter() + .chain(std::iter::once(c)) + }) + .collect::(); + Ok(formatter) +} + +// Returns the first four bytes of a double sha256 hash of some bytes +fn generate_check(payload: &[u8]) -> Result, Error> { + let mut first_hasher = Sha256::new(); + first_hasher.update(payload); + let mut second_hasher = Sha256::new(); + second_hasher.update(first_hasher.finalize()); + let checksum = second_hasher.finalize(); + let check_bytes: Vec = checksum[0..4].to_vec(); + Ok(check_bytes) +} diff --git a/libwallet/src/slatepack/mod.rs b/libwallet/src/slatepack/mod.rs new file mode 100644 index 0000000..157b5cf --- /dev/null +++ b/libwallet/src/slatepack/mod.rs @@ -0,0 +1,25 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Functions and types for handling Slatepack transactions + +mod address; +mod armor; +mod packer; +mod types; + +pub use self::address::SlatepackAddress; +pub use self::armor::{max_size, min_size, SlatepackArmor}; +pub use self::packer::{Slatepacker, SlatepackerArgs}; +pub use self::types::{Slatepack, SlatepackBin}; diff --git a/libwallet/src/slatepack/packer.rs b/libwallet/src/slatepack/packer.rs new file mode 100644 index 0000000..f1c54e9 --- /dev/null +++ b/libwallet/src/slatepack/packer.rs @@ -0,0 +1,121 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::convert::TryFrom; +use std::str; + +use super::armor::HEADER; +use crate::Error; +use crate::{ + slatepack, Slate, SlateVersion, Slatepack, SlatepackAddress, SlatepackArmor, SlatepackBin, + VersionedBinSlate, VersionedSlate, +}; + +use grin_wallet_util::byte_ser; + +use ed25519_dalek::SecretKey as edSecretKey; + +#[derive(Clone)] +/// Arguments, mostly for encrypting decrypting a slatepack +pub struct SlatepackerArgs<'a> { + /// Optional sender to include in slatepack + pub sender: Option, + /// Optional list of recipients, for encryption + pub recipients: Vec, + /// Optional decryption key + pub dec_key: Option<&'a edSecretKey>, +} + +/// Helper struct to pack and unpack slatepacks +#[derive(Clone)] +pub struct Slatepacker<'a>(SlatepackerArgs<'a>); + +impl<'a> Slatepacker<'a> { + /// Create with pathbuf and recipients + pub fn new(args: SlatepackerArgs<'a>) -> Self { + Self(args) + } + + /// return slatepack + pub fn deser_slatepack(&self, data: &[u8], decrypt: bool) -> Result { + // check if data is armored, if so, remove and continue + let data_len = data.len() as u64; + if data_len < slatepack::min_size() || data_len > slatepack::max_size() { + let msg = format!("Data invalid length"); + return Err(Error::SlatepackDeser(msg)); + } + + let test_header = &data[..HEADER.len()]; + + let data = match str::from_utf8(test_header) { + Ok(s) => { + if s == HEADER { + SlatepackArmor::decode(data)? + } else { + data.to_vec() + } + } + Err(_) => data.to_vec(), + }; + + // try as bin first, then as json + let mut slatepack = match byte_ser::from_bytes::(&data) { + Ok(s) => s.0, + Err(e) => { + debug!("Not a valid binary slatepack: {} - Will try JSON", e); + let content = String::from_utf8(data).map_err(|e| { + let msg = format!("{}", e); + Error::SlatepackDeser(msg) + })?; + serde_json::from_str(&content).map_err(|e| { + let msg = format!("Error reading JSON slatepack: {}", e); + Error::SlatepackDeser(msg) + })? + } + }; + + slatepack.ver_check_warn(); + if decrypt { + slatepack.try_decrypt_payload(self.0.dec_key)?; + } + Ok(slatepack) + } + + /// Create slatepack from slate and args + pub fn create_slatepack(&self, slate: &Slate) -> Result { + let out_slate = VersionedSlate::into_version(slate.clone(), SlateVersion::V4)?; + let bin_slate = VersionedBinSlate::try_from(out_slate).map_err(|_| Error::SlatepackSer)?; + let mut slatepack = Slatepack::default(); + slatepack.payload = byte_ser::to_bytes(&bin_slate).map_err(|_| Error::SlatepackSer)?; + slatepack.sender = self.0.sender.clone(); + slatepack.try_encrypt_payload(self.0.recipients.clone())?; + Ok(slatepack) + } + + /// Armor a slatepack + pub fn armor_slatepack(&self, slatepack: &Slatepack) -> Result { + SlatepackArmor::encode(&slatepack) + } + + /// Return/upgrade slate from slatepack + pub fn get_slate(&self, slatepack: &Slatepack) -> Result { + let slate_bin = + byte_ser::from_bytes::(&slatepack.payload).map_err(|e| { + error!("Error reading slate from armored slatepack: {}", e); + let msg = format!("{}", e); + Error::SlatepackDeser(msg) + })?; + Ok(Slate::upgrade(slate_bin.into())?) + } +} diff --git a/libwallet/src/slatepack/types.rs b/libwallet/src/slatepack/types.rs new file mode 100644 index 0000000..fe08c93 --- /dev/null +++ b/libwallet/src/slatepack/types.rs @@ -0,0 +1,850 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use bech32::{self, ToBase32}; +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; +/// Slatepack Types + Serialization implementation +use ed25519_dalek::SecretKey as edSecretKey; +use sha2::{Digest, Sha512}; +use x25519_dalek::StaticSecret; + +use crate::dalek_ser; +use crate::grin_core::ser::{self, Readable, Reader, Writeable, Writer}; +use crate::Error; +use grin_wallet_util::byte_ser; + +use super::SlatepackAddress; + +use std::fmt; +use std::io::{Cursor, Read, Write}; + +pub const SLATEPACK_MAJOR_VERSION: u8 = 1; +pub const SLATEPACK_MINOR_VERSION: u8 = 0; + +/// Basic Slatepack definition +#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] +pub struct Slatepack { + // Required Fields + /// Versioning info + #[serde(with = "slatepack_version")] + pub slatepack: SlatepackVersion, + /// Delivery Mode, 0 = plain_text, 1 = age encrypted + pub mode: u8, + + // Optional Fields + /// Optional Sender address + #[serde(default = "default_sender_none")] + #[serde(skip_serializing_if = "Option::is_none")] + pub sender: Option, + + // Encrypted metadata, to be serialized into payload only + // shouldn't be accessed directly + /// Encrypted metadata + #[serde(default = "default_enc_metadata")] + #[serde(skip_serializing_if = "enc_metadata_is_empty")] + encrypted_meta: SlatepackEncMetadata, + + // Payload (e.g. slate), including encrypted metadata, if present + /// Binary payload, can be encrypted or plaintext + #[serde( + serialize_with = "dalek_ser::as_base64", + deserialize_with = "dalek_ser::bytes_from_base64" + )] + pub payload: Vec, + + /// Test mode + #[serde(default = "default_future_test_mode")] + #[serde(skip)] + pub future_test_mode: bool, +} + +fn default_sender_none() -> Option { + None +} + +fn default_enc_metadata() -> SlatepackEncMetadata { + SlatepackEncMetadata { + sender: None, + recipients: vec![], + } +} + +fn default_future_test_mode() -> bool { + false +} + +fn enc_metadata_is_empty(data: &SlatepackEncMetadata) -> bool { + data.sender.is_none() && data.recipients.is_empty() +} + +impl fmt::Display for Slatepack { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", serde_json::to_string_pretty(&self).unwrap()) + } +} + +impl Default for Slatepack { + fn default() -> Self { + Self { + slatepack: SlatepackVersion { + major: SLATEPACK_MAJOR_VERSION, + minor: SLATEPACK_MINOR_VERSION, + }, + mode: 0, + sender: None, + encrypted_meta: default_enc_metadata(), + payload: vec![], + future_test_mode: false, + } + } +} + +impl Slatepack { + /// return length of optional fields + pub fn opt_fields_len(&self) -> Result { + let mut retval = 0; + if let Some(s) = self.sender.as_ref() { + retval += s.encoded_len().unwrap(); + } + Ok(retval) + } + + // test function that pads the encrypted meta payload with unknown data + fn pad_test_data(data: &mut Vec) { + let extra_bytes = 139; + let mut len_bytes = [0u8; 4]; + len_bytes.copy_from_slice(&data[0..4]); + let mut meta_len = Cursor::new(len_bytes).read_u32::().unwrap(); + meta_len += extra_bytes; + let mut len_bytes = vec![]; + len_bytes.write_u32::(meta_len).unwrap(); + data[..4].clone_from_slice(&len_bytes[..4]); + for _ in 0..extra_bytes { + data.push(rand::random()) + } + } + + /// age encrypt the payload with the given public key + pub fn try_encrypt_payload(&mut self, recipients: Vec) -> Result<(), Error> { + if recipients.is_empty() { + return Ok(()); + } + + // Move our sender to the encrypted metadata field + self.encrypted_meta.sender = self.sender.clone(); + self.sender = None; + + // Create encrypted metadata, which will be length prefixed + let bin_meta = SlatepackEncMetadataBin(self.encrypted_meta.clone()); + let mut to_encrypt = byte_ser::to_bytes(&bin_meta).map_err(|_| Error::SlatepackSer)?; + + if self.future_test_mode { + Slatepack::pad_test_data(&mut to_encrypt); + } + + to_encrypt.append(&mut self.payload); + + let rec_keys: Result, _> = recipients + .into_iter() + .map(|addr| { + let recp_key: age::x25519::Recipient = addr.to_age_pubkey_str()?.parse()?; + Ok(Box::new(recp_key) as Box) + }) + .collect(); + + let keys = match rec_keys { + Ok(k) => k, + Err(e) => return Err(e), + }; + + let encryptor = age::Encryptor::with_recipients(keys); + let mut encrypted = vec![]; + let mut writer = encryptor.wrap_output(&mut encrypted)?; + writer.write_all(&to_encrypt)?; + writer.finish()?; + self.payload = encrypted.to_vec(); + self.mode = 1; + Ok(()) + } + + /// As above, decrypt if needed + pub fn try_decrypt_payload(&mut self, dec_key: Option<&edSecretKey>) -> Result<(), Error> { + if self.mode == 0 { + return Ok(()); + } + let dec_key = match dec_key { + Some(k) => k, + None => return Ok(()), + }; + let mut b = [0u8; 32]; + b.copy_from_slice(&dec_key.as_bytes()[0..32]); + let mut hasher = Sha512::new(); + hasher.update(b); + let result = hasher.finalize(); + b.copy_from_slice(&result[0..32]); + + let x_dec_secret = StaticSecret::from(b); + let x_dec_secret_bech32 = + bech32::encode("age-secret-key-", (&x_dec_secret).to_bytes().to_base32())?; + let key: age::x25519::Identity = x_dec_secret_bech32.parse()?; + + let decryptor = match age::Decryptor::new(&self.payload[..])? { + age::Decryptor::Recipients(d) => d, + _ => unreachable!(), + }; + let mut decrypted = vec![]; + let mut reader = decryptor.decrypt(std::iter::once(&key as &dyn age::Identity))?; + reader.read_to_end(&mut decrypted)?; + // Parse encrypted metadata from payload, first 4 bytes of decrypted payload + // will be encrypted metadata length + let mut len_bytes = [0u8; 4]; + len_bytes.copy_from_slice(&decrypted[0..4]); + let meta_len = Cursor::new(len_bytes).read_u32::()?; + self.payload = decrypted.split_off(meta_len as usize + 4); + let meta = byte_ser::from_bytes::(&decrypted) + .map_err(|_| Error::SlatepackSer)? + .0; + self.sender = meta.sender; + self.encrypted_meta.recipients = meta.recipients; + self.mode = 0; + + Ok(()) + } + + /// add a recipient to encrypted metadata + pub fn add_recipient(&mut self, address: SlatepackAddress) { + self.encrypted_meta.recipients.push(address) + } + + /// retrieve recipients + pub fn recipients(&self) -> &[SlatepackAddress] { + &self.encrypted_meta.recipients + } + + /// version check warning + // TODO: API? + pub fn ver_check_warn(&self) { + if self.slatepack.major > SLATEPACK_MAJOR_VERSION + || (self.slatepack.major == SLATEPACK_MAJOR_VERSION + && self.slatepack.minor > SLATEPACK_MINOR_VERSION) + { + warn!("Incoming Slatepack's version is greater than what this wallet recognizes"); + warn!("You may need to upgrade if it contains unsupported features"); + warn!( + "Incoming: {}.{}, This wallet: {}.{}", + self.slatepack.major, + self.slatepack.minor, + SLATEPACK_MAJOR_VERSION, + SLATEPACK_MINOR_VERSION + ); + } + } +} + +/// Wrapper for outputting slate as binary +#[derive(Debug, Clone)] +pub struct SlatepackBin(pub Slatepack); + +impl serde::Serialize for SlatepackBin { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut vec = vec![]; + ser::serialize(&mut vec, ser::ProtocolVersion(4), self) + .map_err(|err| serde::ser::Error::custom(err.to_string()))?; + serializer.serialize_bytes(&vec) + } +} + +impl<'de> serde::Deserialize<'de> for SlatepackBin { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct SlatepackBinVisitor; + + impl<'de> serde::de::Visitor<'de> for SlatepackBinVisitor { + type Value = SlatepackBin; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(formatter, "a serialised binary Slatepack") + } + + fn visit_bytes(self, value: &[u8]) -> Result + where + E: serde::de::Error, + { + let mut reader = std::io::Cursor::new(value.to_vec()); + let s = ser::deserialize( + &mut reader, + ser::ProtocolVersion(4), + ser::DeserializationMode::default(), + ) + .map_err(|err| serde::de::Error::custom(err.to_string()))?; + Ok(s) + } + } + + deserializer.deserialize_bytes(SlatepackBinVisitor) + } +} + +impl Writeable for SlatepackBin { + fn write(&self, writer: &mut W) -> Result<(), ser::Error> { + let sp = self.0.clone(); + // Version (2) + sp.slatepack.write(writer)?; + // Mode (1) + writer.write_u8(sp.mode)?; + + // 16 bits of optional content flags (2), most reserved for future use + let mut opt_flags: u16 = 0; + if sp.sender.is_some() { + opt_flags |= 0x01; + } + writer.write_u16(opt_flags)?; + + // Bytes to skip from here (Start of optional fields) to get to payload + writer.write_u32(sp.opt_fields_len()? as u32)?; + + // write optional fields + if let Some(s) = sp.sender { + s.write(writer)?; + }; + + // encrypted metadata is only included in the payload + // on encryption, and is not serialised here + + // Now write payload (length prefixed) + writer.write_bytes(sp.payload.clone()) + } +} + +impl Readable for SlatepackBin { + fn read(reader: &mut R) -> Result { + // Version (2) + let slatepack = SlatepackVersion::read(reader)?; + // Mode (1) + let mode = reader.read_u8()?; + if mode > 1 { + return Err(ser::Error::UnexpectedData { + expected: vec![0, 1], + received: vec![mode], + }); + } + // optional content flags (2) + let opt_flags = reader.read_u16()?; + + // start of header + let mut bytes_to_payload = reader.read_u32()?; + + let sender = if opt_flags & 0x01 > 0 { + let addr = SlatepackAddress::read(reader)?; + let len = match addr.encoded_len() { + Ok(e) => e as u32, + Err(e) => { + error!("Cannot parse Slatepack address: {}", e); + return Err(ser::Error::CorruptedData); + } + }; + bytes_to_payload -= len; + Some(addr) + } else { + None + }; + + // skip over any unknown future fields until header + while bytes_to_payload > 0 { + let _ = reader.read_u8()?; + bytes_to_payload -= 1; + } + + let payload = reader.read_bytes_len_prefix()?; + + Ok(SlatepackBin(Slatepack { + slatepack, + mode, + sender, + encrypted_meta: default_enc_metadata(), + payload, + future_test_mode: false, + })) + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct SlatepackVersion { + /// Major + pub major: u8, + /// Minor + pub minor: u8, +} + +impl Writeable for SlatepackVersion { + fn write(&self, writer: &mut W) -> Result<(), ser::Error> { + writer.write_u8(self.major)?; + writer.write_u8(self.minor) + } +} + +impl Readable for SlatepackVersion { + fn read(reader: &mut R) -> Result { + let major = reader.read_u8()?; + let minor = reader.read_u8()?; + Ok(SlatepackVersion { major, minor }) + } +} + +/// Serializes version field JSON +pub mod slatepack_version { + use serde::de::Error; + use serde::{Deserialize, Deserializer, Serializer}; + + use super::SlatepackVersion; + + /// + pub fn serialize(v: &SlatepackVersion, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&format!("{}.{}", v.major, v.minor)) + } + + /// + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + String::deserialize(deserializer).and_then(|s| { + let mut retval = SlatepackVersion { major: 0, minor: 0 }; + let v: Vec<&str> = s.split('.').collect(); + if v.len() != 2 { + return Err(Error::custom("Cannot parse version")); + } + match u8::from_str_radix(v[0], 10) { + Ok(u) => retval.major = u, + Err(e) => return Err(Error::custom(format!("Cannot parse version: {}", e))), + } + match u8::from_str_radix(v[1], 10) { + Ok(u) => retval.minor = u, + Err(e) => return Err(Error::custom(format!("Cannot parse version: {}", e))), + } + Ok(retval) + }) + } +} + +/// Encapsulates encrypted metadata fields +#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] +pub struct SlatepackEncMetadata { + /// Encrypted Sender address, if desired + #[serde(default = "default_sender_none")] + #[serde(skip_serializing_if = "Option::is_none")] + sender: Option, + /// Recipients list, if desired (mostly for future multiparty needs) + #[serde(default = "default_recipients_empty")] + #[serde(skip_serializing_if = "recipients_empty")] + recipients: Vec, +} + +fn recipients_empty(value: &[SlatepackAddress]) -> bool { + value.is_empty() +} + +fn default_recipients_empty() -> Vec { + vec![] +} + +impl SlatepackEncMetadata { + // return length in bytes for encoding (without the 4 byte length header) + pub fn encoded_len(&self) -> Result { + let mut length = 2; //opt flags + if let Some(s) = &self.sender { + length += s.encoded_len()?; + } + if !self.recipients.is_empty() { + length += 2; + for r in self.recipients.iter() { + length += r.encoded_len()?; + } + } + Ok(length) + } +} + +/// Wrapper for outputting encrypted metadata as binary +#[derive(Debug, Clone)] +pub struct SlatepackEncMetadataBin(pub SlatepackEncMetadata); + +impl serde::Serialize for SlatepackEncMetadataBin { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut vec = vec![]; + ser::serialize(&mut vec, ser::ProtocolVersion(4), self) + .map_err(|err| serde::ser::Error::custom(err.to_string()))?; + serializer.serialize_bytes(&vec) + } +} + +impl<'de> serde::Deserialize<'de> for SlatepackEncMetadataBin { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct SlatepackEncMetadataBinVisitor; + + impl<'de> serde::de::Visitor<'de> for SlatepackEncMetadataBinVisitor { + type Value = SlatepackEncMetadataBin; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(formatter, "a serialised binary Slatepack Metadata") + } + + fn visit_bytes(self, value: &[u8]) -> Result + where + E: serde::de::Error, + { + let mut reader = std::io::Cursor::new(value.to_vec()); + let s = ser::deserialize( + &mut reader, + ser::ProtocolVersion(4), + ser::DeserializationMode::default(), + ) + .map_err(|err| serde::de::Error::custom(err.to_string()))?; + Ok(s) + } + } + + deserializer.deserialize_bytes(SlatepackEncMetadataBinVisitor) + } +} + +impl Writeable for SlatepackEncMetadataBin { + fn write(&self, writer: &mut W) -> Result<(), ser::Error> { + let inner = &self.0; + // write entire metadata length + writer.write_u32(inner.encoded_len().map_err(|e| { + error!("Cannot write encrypted metadata length: {}", e); + ser::Error::CorruptedData + })? as u32)?; + + // 16 bits of optional content flags (2), most reserved for future use + let mut opt_flags: u16 = 0; + if inner.sender.is_some() { + opt_flags |= 0x01; + } + if !inner.recipients.is_empty() { + opt_flags |= 0x02; + } + writer.write_u16(opt_flags)?; + + if let Some(s) = &inner.sender { + s.write(writer)?; + }; + + // Recipients List + if !inner.recipients.is_empty() { + let len = inner.recipients.len(); + // write number of recipients + if len as u16 > std::u16::MAX { + error!("Too many recipients: {}", len); + return Err(ser::Error::CorruptedData); + } + writer.write_u16(len as u16)?; + for r in inner.recipients.iter() { + r.write(writer)?; + } + } + Ok(()) + } +} + +impl Readable for SlatepackEncMetadataBin { + fn read(reader: &mut R) -> Result { + // length header, always present + let mut bytes_remaining = reader.read_u32()?; + + // optional content flags (2) + let opt_flags = reader.read_u16()?; + bytes_remaining -= 2; + + let sender = if opt_flags & 0x01 > 0 { + let addr = SlatepackAddress::read(reader)?; + let len = match addr.encoded_len() { + Ok(e) => e as u32, + Err(e) => { + error!("Cannot parse Slatepack address: {}", e); + return Err(ser::Error::CorruptedData); + } + }; + bytes_remaining -= len; + Some(addr) + } else { + None + }; + + let mut recipients = vec![]; + if opt_flags & 0x02 > 0 { + // number of recipients + let count = reader.read_u16()?; + bytes_remaining -= 2; + for _ in 0..count { + let addr = SlatepackAddress::read(reader)?; + let len = match addr.encoded_len() { + Ok(e) => e as u32, + Err(e) => { + error!("Cannot parse Slatepack address: {}", e); + return Err(ser::Error::CorruptedData); + } + }; + bytes_remaining -= len; + recipients.push(addr); + } + } + + // bleed off any unknown data beyond this + while bytes_remaining > 0 { + let _ = reader.read_u8()?; + bytes_remaining -= 1; + } + + Ok(SlatepackEncMetadataBin(SlatepackEncMetadata { + sender, + recipients, + })) + } +} + +#[test] +fn slatepack_bin_basic_ser() -> Result<(), grin_wallet_util::byte_ser::Error> { + use grin_wallet_util::byte_ser; + let mut payload: Vec = Vec::with_capacity(243); + for _ in 0..payload.capacity() { + payload.push(rand::random()); + } + let sp = Slatepack { + payload, + ..Slatepack::default() + }; + let ser = byte_ser::to_bytes(&SlatepackBin(sp.clone()))?; + let deser = byte_ser::from_bytes::(&ser)?.0; + assert_eq!(sp.slatepack, deser.slatepack); + assert_eq!(sp.mode, deser.mode); + assert!(sp.sender.is_none()); + Ok(()) +} + +#[test] +fn slatepack_bin_opt_fields_ser() -> Result<(), grin_wallet_util::byte_ser::Error> { + use crate::grin_core::global; + use grin_wallet_util::byte_ser; + global::set_local_chain_type(global::ChainTypes::AutomatedTesting); + let mut payload: Vec = Vec::with_capacity(243); + for _ in 0..payload.capacity() { + payload.push(rand::random()); + } + + // includes optional fields + let sender = Some(SlatepackAddress::random()); + let sp = Slatepack { + sender, + payload, + ..Slatepack::default() + }; + let ser = byte_ser::to_bytes(&SlatepackBin(sp.clone()))?; + let deser = byte_ser::from_bytes::(&ser)?.0; + assert_eq!(sp, deser); + + Ok(()) +} + +// ensure that a slatepack with unknown data in the optional fields can be read +#[test] +fn slatepack_bin_future() -> Result<(), grin_wallet_util::byte_ser::Error> { + use crate::grin_core::global; + use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; + use grin_wallet_util::byte_ser; + use rand::{thread_rng, Rng}; + use std::io::Cursor; + global::set_local_chain_type(global::ChainTypes::AutomatedTesting); + let payload_size = 1234; + let mut payload: Vec = Vec::with_capacity(payload_size); + for _ in 0..payload.capacity() { + payload.push(rand::random()); + } + let sender = Some(SlatepackAddress::random()); + + println!( + "sender len: {}", + sender.as_ref().unwrap().encoded_len().unwrap() + ); + + let sp = Slatepack { + sender, + payload: payload.clone(), + ..Slatepack::default() + }; + let ser = byte_ser::to_bytes(&SlatepackBin(sp.clone()))?; + + // Add an amount of meaningless (to us) data + let num_extra_bytes = 248; + let mut new_bytes = vec![]; + // Version 2 + // mode 1 + // opt flags 2 + // opt fields len (bytes to payload) 4 + // bytes 5-8 are opt fields len + + // sender 64 + + let mut opt_fields_len_bytes = [0u8; 4]; + opt_fields_len_bytes.copy_from_slice(&ser[5..9]); + let mut rdr = Cursor::new(opt_fields_len_bytes.to_vec()); + let opt_fields_len = rdr.read_u32::().unwrap(); + // check this matches what we expect below + assert_eq!(opt_fields_len, 65); + + let end_head_pos = opt_fields_len as usize + 8 + 1; + + for i in 0..end_head_pos { + new_bytes.push(ser[i]); + } + for _ in 0..num_extra_bytes { + new_bytes.push(thread_rng().gen()); + } + for i in 0..8 { + //push payload length prefix + new_bytes.push(ser[end_head_pos + i]); + } + for i in 0..payload_size { + new_bytes.push(ser[end_head_pos + 8 + i]); + } + + assert_eq!(new_bytes.len(), ser.len() + num_extra_bytes as usize); + + // and set new opt fields length + let mut wtr = vec![]; + wtr.write_u32::(opt_fields_len + num_extra_bytes as u32) + .unwrap(); + for i in 0..wtr.len() { + new_bytes[5 + i] = wtr[i]; + } + + let deser = byte_ser::from_bytes::(&new_bytes)?.0; + assert_eq!(sp, deser); + Ok(()) +} + +// test encryption and encrypted metadata, which only gets written +// if mode == 1 +#[test] +fn slatepack_encrypted_meta() -> Result<(), Error> { + use crate::grin_core::global; + use crate::{Slate, SlateVersion, VersionedBinSlate, VersionedSlate}; + use ed25519_dalek::PublicKey as edDalekPublicKey; + use ed25519_dalek::SecretKey as edDalekSecretKey; + use rand::{thread_rng, Rng}; + use std::convert::TryFrom; + global::set_local_chain_type(global::ChainTypes::AutomatedTesting); + + let sec_key_bytes: [u8; 32] = thread_rng().gen(); + + let ed_sec_key = edDalekSecretKey::from_bytes(&sec_key_bytes).unwrap(); + let ed_pub_key = edDalekPublicKey::from(&ed_sec_key); + let addr = SlatepackAddress::new(&ed_pub_key); + + let encoded = String::try_from(&addr).unwrap(); + let parsed_addr = SlatepackAddress::try_from(encoded.as_str()).unwrap(); + assert_eq!(addr, parsed_addr); + + let mut slatepack = super::Slatepack::default(); + slatepack.sender = Some(SlatepackAddress::random()); + slatepack.add_recipient(SlatepackAddress::random()); + slatepack.add_recipient(SlatepackAddress::random()); + + let v_slate = VersionedSlate::into_version(Slate::blank(2, false), SlateVersion::V4)?; + let bin_slate = VersionedBinSlate::try_from(v_slate).map_err(|_| Error::SlatepackSer)?; + slatepack.payload = byte_ser::to_bytes(&bin_slate).map_err(|_| Error::SlatepackSer)?; + + let orig_sp = slatepack.clone(); + + slatepack.try_encrypt_payload(vec![addr.clone()])?; + + // sender should have been moved to encrypted meta + assert!(slatepack.sender.is_none()); + + let ser = byte_ser::to_bytes(&SlatepackBin(slatepack)).unwrap(); + let mut slatepack = byte_ser::from_bytes::(&ser).unwrap().0; + + slatepack.try_decrypt_payload(Some(&ed_sec_key))?; + assert!(slatepack.sender.is_some()); + + assert_eq!(orig_sp, slatepack); + + Ok(()) +} + +// Ensure adding unknown (future) bytes to the encrypted +// metadata won't break parsing +#[test] +fn slatepack_encrypted_meta_future() -> Result<(), Error> { + use crate::grin_core::global; + use crate::{Slate, SlateVersion, VersionedBinSlate, VersionedSlate}; + use ed25519_dalek::PublicKey as edDalekPublicKey; + use ed25519_dalek::SecretKey as edDalekSecretKey; + use rand::{thread_rng, Rng}; + use std::convert::TryFrom; + global::set_local_chain_type(global::ChainTypes::AutomatedTesting); + + let sec_key_bytes: [u8; 32] = thread_rng().gen(); + + let ed_sec_key = edDalekSecretKey::from_bytes(&sec_key_bytes).unwrap(); + let ed_pub_key = edDalekPublicKey::from(&ed_sec_key); + let addr = SlatepackAddress::new(&ed_pub_key); + + let encoded = String::try_from(&addr).unwrap(); + let parsed_addr = SlatepackAddress::try_from(encoded.as_str()).unwrap(); + assert_eq!(addr, parsed_addr); + + let mut slatepack = Slatepack::default(); + slatepack.sender = Some(SlatepackAddress::random()); + slatepack.add_recipient(SlatepackAddress::random()); + slatepack.add_recipient(SlatepackAddress::random()); + + let v_slate = VersionedSlate::into_version(Slate::blank(2, false), SlateVersion::V4)?; + let bin_slate = VersionedBinSlate::try_from(v_slate).map_err(|_| Error::SlatepackSer)?; + slatepack.payload = byte_ser::to_bytes(&bin_slate).map_err(|_| Error::SlatepackSer)?; + + let orig_sp = slatepack.clone(); + + slatepack.future_test_mode = true; + + slatepack.try_encrypt_payload(vec![addr.clone()])?; + + // sender should have been moved to encrypted meta + assert!(slatepack.sender.is_none()); + + let ser = byte_ser::to_bytes(&SlatepackBin(slatepack)).unwrap(); + let mut slatepack = byte_ser::from_bytes::(&ser).unwrap().0; + + slatepack.try_decrypt_payload(Some(&ed_sec_key))?; + assert!(slatepack.sender.is_some()); + + assert_eq!(orig_sp, slatepack); + + Ok(()) +} diff --git a/libwallet/src/types.rs b/libwallet/src/types.rs new file mode 100644 index 0000000..3a63b7a --- /dev/null +++ b/libwallet/src/types.rs @@ -0,0 +1,1138 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Types and traits that should be provided by a wallet +//! implementation + +use crate::config::{TorConfig, WalletConfig}; +use crate::error::Error; +use crate::grin_core::core::hash::Hash; +use crate::grin_core::core::FeeFields; +use crate::grin_core::core::{Output, Transaction, TxKernel}; +use crate::grin_core::libtx::{aggsig, secp_ser}; +use crate::grin_core::{global, ser}; +use crate::grin_keychain::{Identifier, Keychain}; +use crate::grin_util::logger::LoggingConfig; +use crate::grin_util::secp::key::{PublicKey, SecretKey}; +use crate::grin_util::secp::{self, pedersen, Secp256k1}; +use crate::grin_util::{ToHex, ZeroingString}; +use crate::slate_versions::ser as dalek_ser; +use crate::InitTxArgs; +use chrono::prelude::*; +use ed25519_dalek::PublicKey as DalekPublicKey; +use ed25519_dalek::Signature as DalekSignature; +use rand::rngs::mock::StepRng; +use rand::thread_rng; +use serde; +use serde_json; +use std::collections::HashMap; +use std::fmt; +use std::time::Duration; +use uuid::Uuid; + +/// Combined trait to allow dynamic wallet dispatch +pub trait WalletInst<'a, L, C, K>: Send + Sync +where + L: WalletLCProvider<'a, C, K> + Send + Sync, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + /// Return the stored instance + fn lc_provider(&mut self) -> Result<&mut (dyn WalletLCProvider<'a, C, K> + 'a), Error>; +} + +/// Trait for a provider of wallet lifecycle methods +pub trait WalletLCProvider<'a, C, K>: Send + Sync +where + C: NodeClient + 'a, + K: Keychain + 'a, +{ + /// Sets the top level system wallet directory + /// default is assumed to be ~/.grin/main/wallet_data (or testnet equivalent) + fn set_top_level_directory(&mut self, dir: &str) -> Result<(), Error>; + + /// Sets the top level system wallet directory + /// default is assumed to be ~/.grin/main/wallet_data (or testnet equivalent) + fn get_top_level_directory(&self) -> Result; + + /// Output a grin-wallet.toml file into the current top-level system wallet directory + fn create_config( + &self, + chain_type: &global::ChainTypes, + file_name: &str, + wallet_config: Option, + logging_config: Option, + tor_config: Option, + ) -> Result<(), Error>; + + /// + fn create_wallet( + &mut self, + name: Option<&str>, + mnemonic: Option, + mnemonic_length: usize, + password: ZeroingString, + test_mode: bool, + ) -> Result<(), Error>; + + /// + fn open_wallet( + &mut self, + name: Option<&str>, + password: ZeroingString, + create_mask: bool, + use_test_rng: bool, + ) -> Result, Error>; + + /// + fn close_wallet(&mut self, name: Option<&str>) -> Result<(), Error>; + + /// whether a wallet exists at the given directory + fn wallet_exists(&self, name: Option<&str>) -> Result; + + /// return mnemonic of given wallet + fn get_mnemonic( + &self, + name: Option<&str>, + password: ZeroingString, + ) -> Result; + + /// Check whether a provided mnemonic string is valid + fn validate_mnemonic(&self, mnemonic: ZeroingString) -> Result<(), Error>; + + /// Recover a seed from phrase, without destroying existing data + /// should back up seed + fn recover_from_mnemonic( + &self, + mnemonic: ZeroingString, + password: ZeroingString, + ) -> Result<(), Error>; + + /// changes password + fn change_password( + &self, + name: Option<&str>, + old: ZeroingString, + new: ZeroingString, + ) -> Result<(), Error>; + + /// deletes wallet + fn delete_wallet(&self, name: Option<&str>) -> Result<(), Error>; + + /// return wallet instance + fn wallet_inst(&mut self) -> Result<&mut Box + 'a>, Error>; +} + +/// TODO: +/// Wallets should implement this backend for their storage. All functions +/// here expect that the wallet instance has instantiated itself or stored +/// whatever credentials it needs +pub trait WalletBackend<'ck, C, K>: Send + Sync +where + C: NodeClient + 'ck, + K: Keychain + 'ck, +{ + /// Set the keychain, which should already be initialized + /// Optionally return a token value used to XOR the stored + /// key value + fn set_keychain( + &mut self, + k: Box, + mask: bool, + use_test_rng: bool, + ) -> Result, Error>; + + /// Close wallet and remove any stored credentials (TBD) + fn close(&mut self) -> Result<(), Error>; + + /// Return the keychain being used. Ensure a cloned copy so it will be dropped + /// and zeroized by the caller + /// Can optionally take a mask value + fn keychain(&self, mask: Option<&SecretKey>) -> Result; + + /// Return the client being used to communicate with the node + fn w2n_client(&mut self) -> &mut C; + + /// return the commit for caching if allowed, none otherwise + fn calc_commit_for_cache( + &mut self, + keychain_mask: Option<&SecretKey>, + amount: u64, + id: &Identifier, + ) -> Result, Error>; + + /// Set parent key id by stored account name + fn set_parent_key_id_by_name(&mut self, label: &str) -> Result<(), Error>; + + /// The BIP32 path of the parent path to use for all output-related + /// functions, (essentially 'accounts' within a wallet. + fn set_parent_key_id(&mut self, _: Identifier); + + /// return the parent path + fn parent_key_id(&mut self) -> Identifier; + + /// Iterate over all output data stored by the backend + fn iter<'a>(&'a self) -> Box + 'a>; + + /// Get output data by id + fn get(&self, id: &Identifier, mmr_index: &Option) -> Result; + + /// Get an (Optional) tx log entry by uuid + fn get_tx_log_entry(&self, uuid: &Uuid) -> Result, Error>; + + /// Retrieves the private context associated with a given slate id + fn get_private_context( + &mut self, + keychain_mask: Option<&SecretKey>, + slate_id: &[u8], + ) -> Result; + + /// Iterate over all output data stored by the backend + fn tx_log_iter<'a>(&'a self) -> Box + 'a>; + + /// Iterate over all stored account paths + fn acct_path_iter<'a>(&'a self) -> Box + 'a>; + + /// Gets an account path for a given label + fn get_acct_path(&self, label: String) -> Result, Error>; + + /// Stores a transaction + fn store_tx(&self, uuid: &str, tx: &Transaction) -> Result<(), Error>; + + /// Retrieves a stored transaction from a TxLogEntry + fn get_stored_tx(&self, uuid: &str) -> Result, Error>; + + /// Create a new write batch to update or remove output data + fn batch<'a>( + &'a mut self, + keychain_mask: Option<&SecretKey>, + ) -> Result + 'a>, Error>; + + /// Batch for use when keychain isn't available or required + fn batch_no_mask<'a>(&'a mut self) -> Result + 'a>, Error>; + + /// Return the current child Index + fn current_child_index(&mut self, parent_key_id: &Identifier) -> Result; + + /// Next child ID when we want to create a new output, based on current parent + fn next_child(&mut self, keychain_mask: Option<&SecretKey>) -> Result; + + /// last verified height of outputs directly descending from the given parent key + fn last_confirmed_height(&mut self) -> Result; + + /// last block scanned during scan or restore + fn last_scanned_block(&mut self) -> Result; + + /// Flag whether the wallet needs a full UTXO scan on next update attempt + fn init_status(&mut self) -> Result; +} + +/// Batch trait to update the output data backend atomically. Trying to use a +/// batch after commit MAY result in a panic. Due to this being a trait, the +/// commit method can't take ownership. +/// TODO: Should these be split into separate batch objects, for outputs, +/// tx_log entries and meta/details? +pub trait WalletOutputBatch +where + K: Keychain, +{ + /// Return the keychain being used + fn keychain(&mut self) -> &mut K; + + /// Add or update data about an output to the backend + fn save(&mut self, out: OutputData) -> Result<(), Error>; + + /// Gets output data by id + fn get(&self, id: &Identifier, mmr_index: &Option) -> Result; + + /// Iterate over all output data stored by the backend + fn iter(&self) -> Box>; + + /// Delete data about an output from the backend + fn delete(&mut self, id: &Identifier, mmr_index: &Option) -> Result<(), Error>; + + /// Save last stored child index of a given parent + fn save_child_index(&mut self, parent_key_id: &Identifier, child_n: u32) -> Result<(), Error>; + + /// Save last confirmed height of outputs for a given parent + fn save_last_confirmed_height( + &mut self, + parent_key_id: &Identifier, + height: u64, + ) -> Result<(), Error>; + + /// Save the last PMMR index that was scanned via a scan operation + fn save_last_scanned_block(&mut self, block: ScannedBlockInfo) -> Result<(), Error>; + + /// Save flag indicating whether wallet needs a full UTXO scan + fn save_init_status(&mut self, value: WalletInitStatus) -> Result<(), Error>; + + /// get next tx log entry for the parent + fn next_tx_log_id(&mut self, parent_key_id: &Identifier) -> Result; + + /// Iterate over tx log data stored by the backend + fn tx_log_iter(&self) -> Box>; + + /// save a tx log entry + fn save_tx_log_entry(&mut self, t: TxLogEntry, parent_id: &Identifier) -> Result<(), Error>; + + /// save an account label -> path mapping + fn save_acct_path(&mut self, mapping: AcctPathMapping) -> Result<(), Error>; + + /// Iterate over account names stored in backend + fn acct_path_iter(&self) -> Box>; + + /// Save an output as locked in the backend + fn lock_output(&mut self, out: &mut OutputData) -> Result<(), Error>; + + /// Saves the private context associated with a slate id + fn save_private_context(&mut self, slate_id: &[u8], ctx: &Context) -> Result<(), Error>; + + /// Delete the private context associated with the slate id + fn delete_private_context(&mut self, slate_id: &[u8]) -> Result<(), Error>; + + /// Write the wallet data to backend file + fn commit(&self) -> Result<(), Error>; +} + +/// Encapsulate all wallet-node communication functions. No functions within libwallet +/// should care about communication details +pub trait NodeClient: Send + Sync + Clone { + /// Return the URL of the check node + fn node_url(&self) -> &str; + + /// Set the node URL + fn set_node_url(&mut self, node_url: &str); + + /// Return the node api secret + fn node_api_secret(&self) -> Option; + + /// Change the API secret + fn set_node_api_secret(&mut self, node_api_secret: Option); + + /// Posts a transaction to a grin node + fn post_tx(&self, tx: &Transaction, fluff: bool) -> Result<(), Error>; + + /// Returns the api version string and block header version as reported + /// by the node. Result can be cached for later use + fn get_version_info(&mut self) -> Option; + + /// retrieves the current tip (height, hash) from the specified grin node + fn get_chain_tip(&self) -> Result<(u64, String), Error>; + + /// Get a kernel and the height of the block it's included in. Returns + /// (tx_kernel, height, mmr_index) + fn get_kernel( + &mut self, + excess: &pedersen::Commitment, + min_height: Option, + max_height: Option, + ) -> Result, Error>; + + /// retrieve a list of outputs from the specified grin node + /// need "by_height" and "by_id" variants + fn get_outputs_from_node( + &self, + wallet_outputs: Vec, + ) -> Result, Error>; + + /// Get a list of outputs from the node by traversing the UTXO + /// set in PMMR index order. + /// Returns + /// (last available output index, last insertion index retrieved, + /// outputs(commit, proof, is_coinbase, height, mmr_index)) + fn get_outputs_by_pmmr_index( + &self, + start_height: u64, + end_height: Option, + max_outputs: u64, + ) -> Result< + ( + u64, + u64, + Vec<(pedersen::Commitment, pedersen::RangeProof, bool, u64, u64)>, + ), + Error, + >; + + /// Return the pmmr indices representing the outputs between a given + /// set of block heights + /// (start pmmr index, end pmmr index) + fn height_range_to_pmmr_indices( + &self, + start_height: u64, + end_height: Option, + ) -> Result<(u64, u64), Error>; +} + +/// Node version info +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct NodeVersionInfo { + /// Semver version string + pub node_version: String, + /// block header verson + pub block_header_version: u16, + /// Whether this version info was successfully verified from a node + pub verified: Option, +} + +/// Information about an output that's being tracked by the wallet. Must be +/// enough to reconstruct the commitment associated with the ouput when the +/// root private key is known. + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, PartialOrd, Eq, Ord)] +pub struct OutputData { + /// Root key_id that the key for this output is derived from + pub root_key_id: Identifier, + /// Derived key for this output + pub key_id: Identifier, + /// How many derivations down from the root key + pub n_child: u32, + /// The actual commit, optionally stored + pub commit: Option, + /// PMMR Index, used on restore in case of duplicate wallets using the same + /// key_id (2 wallets using same seed, for instance + #[serde(with = "secp_ser::opt_string_or_u64")] + pub mmr_index: Option, + /// Value of the output, necessary to rebuild the commitment + #[serde(with = "secp_ser::string_or_u64")] + pub value: u64, + /// Current status of the output + pub status: OutputStatus, + /// Height of the output + #[serde(with = "secp_ser::string_or_u64")] + pub height: u64, + /// Height we are locked until + #[serde(with = "secp_ser::string_or_u64")] + pub lock_height: u64, + /// Is this a coinbase output? Is it subject to coinbase locktime? + pub is_coinbase: bool, + /// Optional corresponding internal entry in tx entry log + pub tx_log_entry: Option, +} + +impl ser::Writeable for OutputData { + fn write(&self, writer: &mut W) -> Result<(), ser::Error> { + writer.write_bytes(&serde_json::to_vec(self).map_err(|_| ser::Error::CorruptedData)?) + } +} + +impl ser::Readable for OutputData { + fn read(reader: &mut R) -> Result { + let data = reader.read_bytes_len_prefix()?; + serde_json::from_slice(&data[..]).map_err(|_| ser::Error::CorruptedData) + } +} + +impl OutputData { + /// Lock a given output to avoid conflicting use + pub fn lock(&mut self) { + self.status = OutputStatus::Locked; + } + + /// How many confirmations has this output received? + /// If height == 0 then we are either Unconfirmed or the output was + /// cut-through + /// so we do not actually know how many confirmations this output had (and + /// never will). + pub fn num_confirmations(&self, current_height: u64) -> u64 { + if self.height > current_height { + return 0; + } + if self.status == OutputStatus::Unconfirmed { + 0 + } else { + // if an output has height n and we are at block n + // then we have a single confirmation (the block it originated in) + 1 + (current_height - self.height) + } + } + + /// Check if output is eligible to spend based on state and height and + /// confirmations + pub fn eligible_to_spend(&self, current_height: u64, minimum_confirmations: u64) -> bool { + if [OutputStatus::Spent, OutputStatus::Locked].contains(&self.status) + || self.status == OutputStatus::Unconfirmed && self.is_coinbase + || self.lock_height > current_height + { + false + } else { + (self.status == OutputStatus::Unspent + && self.num_confirmations(current_height) >= minimum_confirmations) + || self.status == OutputStatus::Unconfirmed && minimum_confirmations == 0 + } + } + + /// Marks this output as unspent if it was previously unconfirmed + pub fn mark_unspent(&mut self) { + match self.status { + OutputStatus::Unconfirmed | OutputStatus::Reverted => { + self.status = OutputStatus::Unspent + } + _ => {} + } + } + + /// Mark an output as spent + pub fn mark_spent(&mut self) { + match self.status { + OutputStatus::Unspent | OutputStatus::Locked => self.status = OutputStatus::Spent, + _ => (), + } + } + + /// Mark an output as reverted + pub fn mark_reverted(&mut self) { + match self.status { + OutputStatus::Unspent => self.status = OutputStatus::Reverted, + _ => (), + } + } +} +/// Status of an output that's being tracked by the wallet. Can either be +/// unconfirmed, spent, unspent, or locked (when it's been used to generate +/// a transaction but we don't have confirmation that the transaction was +/// broadcasted or mined). +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Ord, PartialOrd)] +pub enum OutputStatus { + /// Unconfirmed + Unconfirmed, + /// Unspent + Unspent, + /// Locked + Locked, + /// Spent + Spent, + /// Reverted + Reverted, +} + +impl fmt::Display for OutputStatus { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + OutputStatus::Unconfirmed => write!(f, "Unconfirmed"), + OutputStatus::Unspent => write!(f, "Unspent"), + OutputStatus::Locked => write!(f, "Locked"), + OutputStatus::Spent => write!(f, "Spent"), + OutputStatus::Reverted => write!(f, "Reverted"), + } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +/// Holds the context for a single aggsig transaction +pub struct Context { + /// Parent key id + pub parent_key_id: Identifier, + /// Secret key (of which public is shared) + pub sec_key: SecretKey, + /// Secret nonce (of which public is shared) + /// (basically a SecretKey) + pub sec_nonce: SecretKey, + /// only used if self-sending an invoice + pub initial_sec_key: SecretKey, + /// as above + pub initial_sec_nonce: SecretKey, + /// store my outputs + amounts between invocations + /// Id, mmr_index (if known), amount + pub output_ids: Vec<(Identifier, Option, u64)>, + /// store my inputs + /// Id, mmr_index (if known), amount + pub input_ids: Vec<(Identifier, Option, u64)>, + /// store amount, so we can remove from slate if not + /// needed by the other party + pub amount: u64, + /// store the calculated fee + pub fee: Option, + /// Payment proof sender address derivation path, if needed + pub payment_proof_derivation_index: Option, + /// If late-locking, store my tranasction creation prefs + /// for later + pub late_lock_args: Option, + /// for invoice I2 Only, store the tx excess so we can + /// remove it from the slate on return + pub calculated_excess: Option, +} + +impl Context { + /// Create a new context with defaults + pub fn new( + secp: &secp::Secp256k1, + parent_key_id: &Identifier, + use_test_rng: bool, + is_initiator: bool, + ) -> Self { + let sec_key = match use_test_rng { + false => SecretKey::new(secp, &mut thread_rng()), + true => { + // allow for consistent test results + let mut test_rng = if is_initiator { + StepRng::new(1_234_567_890_u64, 1) + } else { + StepRng::new(1_234_567_891_u64, 1) + }; + SecretKey::new(secp, &mut test_rng) + } + }; + Self::with_excess(secp, sec_key, parent_key_id, use_test_rng) + } + + /// Create a new context with a specific excess + pub fn with_excess( + secp: &secp::Secp256k1, + sec_key: SecretKey, + parent_key_id: &Identifier, + use_test_rng: bool, + ) -> Self { + let sec_nonce = match use_test_rng { + false => aggsig::create_secnonce(secp).unwrap(), + true => SecretKey::from_slice(secp, &[1; 32]).unwrap(), + }; + Self { + parent_key_id: parent_key_id.clone(), + sec_key: sec_key.clone(), + sec_nonce: sec_nonce.clone(), + initial_sec_key: sec_key, + initial_sec_nonce: sec_nonce, + input_ids: vec![], + output_ids: vec![], + amount: 0, + fee: None, + payment_proof_derivation_index: None, + late_lock_args: None, + calculated_excess: None, + } + } +} + +impl Context { + /// Tracks an output contributing to my excess value (if it needs to + /// be kept between invocations + pub fn add_output(&mut self, output_id: &Identifier, mmr_index: &Option, amount: u64) { + self.output_ids + .push((output_id.clone(), *mmr_index, amount)); + } + + /// Returns all stored outputs + pub fn get_outputs(&self) -> Vec<(Identifier, Option, u64)> { + self.output_ids.clone() + } + + /// Tracks IDs of my inputs into the transaction + /// be kept between invocations + pub fn add_input(&mut self, input_id: &Identifier, mmr_index: &Option, amount: u64) { + self.input_ids.push((input_id.clone(), *mmr_index, amount)); + } + + /// Returns all stored input identifiers + pub fn get_inputs(&self) -> Vec<(Identifier, Option, u64)> { + self.input_ids.clone() + } + + /// Returns private key, private nonce + pub fn get_private_keys(&self) -> (SecretKey, SecretKey) { + (self.sec_key.clone(), self.sec_nonce.clone()) + } + + /// Returns public key, public nonce + pub fn get_public_keys(&self, secp: &Secp256k1) -> (PublicKey, PublicKey) { + ( + PublicKey::from_secret_key(secp, &self.sec_key).unwrap(), + PublicKey::from_secret_key(secp, &self.sec_nonce).unwrap(), + ) + } +} + +impl ser::Writeable for Context { + fn write(&self, writer: &mut W) -> Result<(), ser::Error> { + writer.write_bytes(&serde_json::to_vec(self).map_err(|_| ser::Error::CorruptedData)?) + } +} + +impl ser::Readable for Context { + fn read(reader: &mut R) -> Result { + let data = reader.read_bytes_len_prefix()?; + serde_json::from_slice(&data[..]).map_err(|_| ser::Error::CorruptedData) + } +} + +/// Block Identifier +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord)] +pub struct BlockIdentifier(pub Hash); + +impl BlockIdentifier { + /// return hash + pub fn hash(&self) -> Hash { + self.0 + } + + /// convert to hex string + pub fn from_hex(hex: &str) -> Result { + let hash = + Hash::from_hex(hex).map_err(|e| Error::GenericError(format!("Invalid hex: {}", e)))?; + Ok(BlockIdentifier(hash)) + } +} + +impl serde::ser::Serialize for BlockIdentifier { + fn serialize(&self, serializer: S) -> Result + where + S: serde::ser::Serializer, + { + serializer.serialize_str(&self.0.to_hex()) + } +} + +impl<'de> serde::de::Deserialize<'de> for BlockIdentifier { + fn deserialize(deserializer: D) -> Result + where + D: serde::de::Deserializer<'de>, + { + deserializer.deserialize_str(BlockIdentifierVisitor) + } +} + +struct BlockIdentifierVisitor; + +impl<'de> serde::de::Visitor<'de> for BlockIdentifierVisitor { + type Value = BlockIdentifier; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("a block hash") + } + + fn visit_str(self, s: &str) -> Result + where + E: serde::de::Error, + { + let block_hash = Hash::from_hex(s).unwrap(); + Ok(BlockIdentifier(block_hash)) + } +} + +/// a contained wallet info struct, so automated tests can parse wallet info +/// can add more fields here over time as needed +#[derive(Serialize, Eq, PartialEq, Deserialize, Debug, Clone)] +pub struct WalletInfo { + /// height from which info was taken + #[serde(with = "secp_ser::string_or_u64")] + pub last_confirmed_height: u64, + /// Minimum number of confirmations for an output to be treated as "spendable". + #[serde(with = "secp_ser::string_or_u64")] + pub minimum_confirmations: u64, + /// total amount in the wallet + #[serde(with = "secp_ser::string_or_u64")] + pub total: u64, + /// amount awaiting finalization + #[serde(with = "secp_ser::string_or_u64")] + pub amount_awaiting_finalization: u64, + /// amount awaiting confirmation + #[serde(with = "secp_ser::string_or_u64")] + pub amount_awaiting_confirmation: u64, + /// coinbases waiting for lock height + #[serde(with = "secp_ser::string_or_u64")] + pub amount_immature: u64, + /// amount currently spendable + #[serde(with = "secp_ser::string_or_u64")] + pub amount_currently_spendable: u64, + /// amount locked via previous transactions + #[serde(with = "secp_ser::string_or_u64")] + pub amount_locked: u64, + /// amount previously confirmed, now reverted + #[serde(with = "secp_ser::string_or_u64")] + pub amount_reverted: u64, +} + +/// Types of transactions that can be contained within a TXLog entry +#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] +pub enum TxLogEntryType { + /// A coinbase transaction becomes confirmed + ConfirmedCoinbase, + /// Outputs created when a transaction is received + TxReceived, + /// Inputs locked + change outputs when a transaction is created + TxSent, + /// Received transaction that was rolled back by user + TxReceivedCancelled, + /// Sent transaction that was rolled back by user + TxSentCancelled, + /// Received transaction that was reverted on-chain + TxReverted, +} + +impl fmt::Display for TxLogEntryType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + TxLogEntryType::ConfirmedCoinbase => write!(f, "Confirmed \nCoinbase"), + TxLogEntryType::TxReceived => write!(f, "Received Tx"), + TxLogEntryType::TxSent => write!(f, "Sent Tx"), + TxLogEntryType::TxReceivedCancelled => write!(f, "Received Tx\n- Cancelled"), + TxLogEntryType::TxSentCancelled => write!(f, "Sent Tx\n- Cancelled"), + TxLogEntryType::TxReverted => write!(f, "Received Tx\n- Reverted"), + } + } +} + +/// Optional transaction information, recorded when an event happens +/// to add or remove funds from a wallet. One Transaction log entry +/// maps to one or many outputs +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct TxLogEntry { + /// BIP32 account path used for creating this tx + pub parent_key_id: Identifier, + /// Local id for this transaction (distinct from a slate transaction id) + pub id: u32, + /// Slate transaction this entry is associated with, if any + pub tx_slate_id: Option, + /// Transaction type (as above) + pub tx_type: TxLogEntryType, + /// Time this tx entry was created + /// #[serde(with = "tx_date_format")] + pub creation_ts: DateTime, + /// Time this tx was confirmed (by this wallet) + /// #[serde(default, with = "opt_tx_date_format")] + pub confirmation_ts: Option>, + /// Whether the inputs+outputs involved in this transaction have been + /// confirmed (In all cases either all outputs involved in a tx should be + /// confirmed, or none should be; otherwise there's a deeper problem) + pub confirmed: bool, + /// number of inputs involved in TX + pub num_inputs: usize, + /// number of outputs involved in TX + pub num_outputs: usize, + /// Amount credited via this transaction + #[serde(with = "secp_ser::string_or_u64")] + pub amount_credited: u64, + /// Amount debited via this transaction + #[serde(with = "secp_ser::string_or_u64")] + pub amount_debited: u64, + /// Fee + pub fee: Option, + /// Cutoff block height + #[serde(with = "secp_ser::opt_string_or_u64")] + #[serde(default)] + pub ttl_cutoff_height: Option, + /// Location of the store transaction, (reference or resending) + pub stored_tx: Option, + /// Associated kernel excess, for later lookup if necessary + #[serde(with = "secp_ser::option_commitment_serde")] + #[serde(default)] + pub kernel_excess: Option, + /// Height reported when transaction was created, if lookup + /// of kernel is necessary + #[serde(default)] + pub kernel_lookup_min_height: Option, + /// Additional info needed to stored payment proof + #[serde(default)] + pub payment_proof: Option, + /// Track the time it took for a transaction to get reverted + #[serde(with = "option_duration_as_secs", default)] + pub reverted_after: Option, +} + +impl ser::Writeable for TxLogEntry { + fn write(&self, writer: &mut W) -> Result<(), ser::Error> { + writer.write_bytes(&serde_json::to_vec(self).map_err(|_| ser::Error::CorruptedData)?) + } +} + +impl ser::Readable for TxLogEntry { + fn read(reader: &mut R) -> Result { + let data = reader.read_bytes_len_prefix()?; + serde_json::from_slice(&data[..]).map_err(|_| ser::Error::CorruptedData) + } +} + +impl TxLogEntry { + /// Return a new blank with TS initialised with next entry + pub fn new(parent_key_id: Identifier, t: TxLogEntryType, id: u32) -> Self { + TxLogEntry { + parent_key_id: parent_key_id, + tx_type: t, + id: id, + tx_slate_id: None, + creation_ts: Utc::now(), + confirmation_ts: None, + confirmed: false, + amount_credited: 0, + amount_debited: 0, + num_inputs: 0, + num_outputs: 0, + fee: None, + ttl_cutoff_height: None, + stored_tx: None, + kernel_excess: None, + kernel_lookup_min_height: None, + payment_proof: None, + reverted_after: None, + } + } + + /// Given a vec of TX log entries, return credited + debited sums + pub fn sum_confirmed(txs: &[TxLogEntry]) -> (u64, u64) { + txs.iter().fold((0, 0), |acc, tx| match tx.confirmed { + true => (acc.0 + tx.amount_credited, acc.1 + tx.amount_debited), + false => acc, + }) + } + + /// Update confirmation TS with now + pub fn update_confirmation_ts(&mut self) { + self.confirmation_ts = Some(Utc::now()); + } +} + +/// Payment proof information. Differs from what is sent via +/// the slate +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct StoredProofInfo { + /// receiver address + #[serde(with = "dalek_ser::dalek_pubkey_serde")] + pub receiver_address: DalekPublicKey, + #[serde(with = "dalek_ser::option_dalek_sig_serde")] + /// receiver signature + pub receiver_signature: Option, + /// sender address derivation path index + pub sender_address_path: u32, + /// sender address + #[serde(with = "dalek_ser::dalek_pubkey_serde")] + pub sender_address: DalekPublicKey, + /// sender signature + #[serde(with = "dalek_ser::option_dalek_sig_serde")] + pub sender_signature: Option, +} + +impl ser::Writeable for StoredProofInfo { + fn write(&self, writer: &mut W) -> Result<(), ser::Error> { + writer.write_bytes(&serde_json::to_vec(self).map_err(|_| ser::Error::CorruptedData)?) + } +} + +impl ser::Readable for StoredProofInfo { + fn read(reader: &mut R) -> Result { + let data = reader.read_bytes_len_prefix()?; + serde_json::from_slice(&data[..]).map_err(|_| ser::Error::CorruptedData) + } +} + +/// Map of named accounts to BIP32 paths +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct AcctPathMapping { + /// label used by user + pub label: String, + /// Corresponding parent BIP32 derivation path + pub path: Identifier, +} + +impl ser::Writeable for AcctPathMapping { + fn write(&self, writer: &mut W) -> Result<(), ser::Error> { + writer.write_bytes(&serde_json::to_vec(self).map_err(|_| ser::Error::CorruptedData)?) + } +} + +impl ser::Readable for AcctPathMapping { + fn read(reader: &mut R) -> Result { + let data = reader.read_bytes_len_prefix()?; + serde_json::from_slice(&data[..]).map_err(|_| ser::Error::CorruptedData) + } +} + +/// Dummy wrapper for the hex-encoded serialized transaction. +#[derive(Serialize, Deserialize)] +pub struct TxWrapper { + /// hex representation of transaction + pub tx_hex: String, +} + +/// Store details of the last scanned block +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct ScannedBlockInfo { + /// Node chain height (corresponding to the last PMMR index scanned) + pub height: u64, + /// Hash of tip + pub hash: String, + /// Starting PMMR Index + pub start_pmmr_index: u64, + /// Last PMMR Index + pub last_pmmr_index: u64, +} + +impl ser::Writeable for ScannedBlockInfo { + fn write(&self, writer: &mut W) -> Result<(), ser::Error> { + writer.write_bytes(&serde_json::to_vec(self).map_err(|_| ser::Error::CorruptedData)?) + } +} + +impl ser::Readable for ScannedBlockInfo { + fn read(reader: &mut R) -> Result { + let data = reader.read_bytes_len_prefix()?; + serde_json::from_slice(&data[..]).map_err(|_| ser::Error::CorruptedData) + } +} + +/// Wrapper for reward output and kernel used when building a coinbase for a mining node. +/// Note: Not serializable, must be converted to necesssary "versioned" representation +/// before serializing to json to ensure compatibility with mining node. +#[derive(Debug, Clone)] +pub struct CbData { + /// Output + pub output: Output, + /// Kernel + pub kernel: TxKernel, + /// Key Id + pub key_id: Option, +} + +/// Enum to determine what amount of scanning is required for a new wallet +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum WalletInitStatus { + /// Wallet is newly created and needs scanning + InitNeedsScanning, + /// Wallet is new but doesn't need scanning + InitNoScanning, + /// Wallet scan checks have been completed + InitComplete, +} + +impl ser::Writeable for WalletInitStatus { + fn write(&self, writer: &mut W) -> Result<(), ser::Error> { + writer.write_bytes(&serde_json::to_vec(self).map_err(|_| ser::Error::CorruptedData)?) + } +} + +impl ser::Readable for WalletInitStatus { + fn read(reader: &mut R) -> Result { + let data = reader.read_bytes_len_prefix()?; + serde_json::from_slice(&data[..]).map_err(|_| ser::Error::CorruptedData) + } +} + +/// Utility struct for return values from below +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ViewWallet { + /// Rewind Hash used to retrieve the outputs + pub rewind_hash: String, + /// All outputs information that belongs to the rewind hash + pub output_result: Vec, + /// total balance + pub total_balance: u64, + /// last pmmr index + pub last_pmmr_index: u64, +} +/// Utility struct for return values from below +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ViewWalletOutputResult { + /// + pub commit: String, + /// + pub value: u64, + /// + pub height: u64, + /// + pub mmr_index: u64, + /// + pub is_coinbase: bool, + /// + pub lock_height: u64, +} + +impl ViewWalletOutputResult { + pub fn num_confirmations(&self, tip_height: u64) -> u64 { + if self.height > tip_height { + return 0; + } else { + 1 + (tip_height - self.height) + } + } +} + +/// Serializes an Option to and from a string +pub mod option_duration_as_secs { + use serde::de::Error; + use serde::{Deserialize, Deserializer, Serializer}; + use std::time::Duration; + + /// + pub fn serialize(dur: &Option, serializer: S) -> Result + where + S: Serializer, + { + match dur { + Some(dur) => serializer.serialize_str(&format!("{}", dur.as_secs())), + None => serializer.serialize_none(), + } + } + + /// + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + match Option::::deserialize(deserializer)? { + Some(s) => { + let secs = s + .parse::() + .map_err(|err| Error::custom(err.to_string()))?; + Ok(Some(Duration::from_secs(secs))) + } + None => Ok(None), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::Value; + + #[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] + struct TestSer { + #[serde(with = "option_duration_as_secs", default)] + dur: Option, + } + + #[test] + fn duration_serde() { + let some = TestSer { + dur: Some(Duration::from_secs(100)), + }; + let val = serde_json::to_value(some.clone()).unwrap(); + if let Value::Object(o) = &val { + if let Value::String(s) = o.get("dur").unwrap() { + assert_eq!(s, "100"); + } else { + panic!("Invalid type"); + } + } else { + panic!("Invalid type") + } + assert_eq!(some, serde_json::from_value(val).unwrap()); + + let none = TestSer { dur: None }; + let val = serde_json::to_value(none.clone()).unwrap(); + if let Value::Object(o) = &val { + if let Value::Null = o.get("dur").unwrap() { + // ok + } else { + panic!("Invalid type"); + } + } else { + panic!("Invalid type") + } + assert_eq!(none, serde_json::from_value(val).unwrap()); + + let none2 = serde_json::from_str::("{}").unwrap(); + assert_eq!(none, none2); + } +} diff --git a/libwallet/tests/libwallet.rs b/libwallet/tests/libwallet.rs new file mode 100644 index 0000000..6d392a9 --- /dev/null +++ b/libwallet/tests/libwallet.rs @@ -0,0 +1,527 @@ +// Copyright 2021 The Grin Developers +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! core::libtx specific tests +use grin_core::core::transaction; +use grin_core::core::FeeFields; +use grin_core::libtx::{aggsig, proof}; +use grin_keychain::{ + BlindSum, BlindingFactor, ExtKeychain, ExtKeychainPath, Keychain, SwitchCommitmentType, +}; +use grin_util::secp; +use grin_util::secp::key::{PublicKey, SecretKey}; +use grin_wallet_libwallet::Context; +use rand::thread_rng; + +fn kernel_sig_msg() -> secp::Message { + transaction::KernelFeatures::Plain { + fee: FeeFields::zero(), + } + .kernel_sig_msg() + .unwrap() +} + +#[test] +fn aggsig_sender_receiver_interaction() { + let parent = ExtKeychainPath::new(1, 1, 0, 0, 0).to_identifier(); + let switch = SwitchCommitmentType::Regular; + let sender_keychain = ExtKeychain::from_random_seed(true).unwrap(); + let receiver_keychain = ExtKeychain::from_random_seed(true).unwrap(); + + // Calculate the kernel excess here for convenience. + // Normally this would happen during transaction building. + let kernel_excess = { + let id1 = ExtKeychain::derive_key_id(1, 1, 0, 0, 0); + let skey1 = sender_keychain.derive_key(0, &id1, switch).unwrap(); + let skey2 = receiver_keychain.derive_key(0, &id1, switch).unwrap(); + + let keychain = ExtKeychain::from_random_seed(true).unwrap(); + let blinding_factor = keychain + .blind_sum( + &BlindSum::new() + .sub_blinding_factor(BlindingFactor::from_secret_key(skey1)) + .add_blinding_factor(BlindingFactor::from_secret_key(skey2)), + ) + .unwrap(); + + keychain + .secp() + .commit(0, blinding_factor.secret_key(&keychain.secp()).unwrap()) + .unwrap() + }; + + let s_cx; + let mut rx_cx; + // sender starts the tx interaction + let (sender_pub_excess, _sender_pub_nonce) = { + let keychain = sender_keychain.clone(); + let id1 = ExtKeychain::derive_key_id(1, 1, 0, 0, 0); + let skey = keychain.derive_key(0, &id1, switch).unwrap(); + + // dealing with an input here so we need to negate the blinding_factor + // rather than use it as is + let bs = BlindSum::new(); + let blinding_factor = keychain + .blind_sum(&bs.sub_blinding_factor(BlindingFactor::from_secret_key(skey))) + .unwrap(); + + let blind = blinding_factor.secret_key(&keychain.secp()).unwrap(); + + s_cx = Context::with_excess(&keychain.secp(), blind, &parent, false); + s_cx.get_public_keys(&keychain.secp()) + }; + + let pub_nonce_sum; + let pub_key_sum; + // receiver receives partial tx + let (receiver_pub_excess, _receiver_pub_nonce, rx_sig_part) = { + let keychain = receiver_keychain.clone(); + let key_id = ExtKeychain::derive_key_id(1, 1, 0, 0, 0); + + // let blind = blind_sum.secret_key(&keychain.secp())?; + let blind = keychain.derive_key(0, &key_id, switch).unwrap(); + + rx_cx = Context::with_excess(&keychain.secp(), blind, &parent, false); + let (pub_excess, pub_nonce) = rx_cx.get_public_keys(&keychain.secp()); + rx_cx.add_output(&key_id, &None, 0); + + pub_nonce_sum = PublicKey::from_combination( + keychain.secp(), + vec![ + &s_cx.get_public_keys(keychain.secp()).1, + &rx_cx.get_public_keys(keychain.secp()).1, + ], + ) + .unwrap(); + + pub_key_sum = PublicKey::from_combination( + keychain.secp(), + vec![ + &s_cx.get_public_keys(keychain.secp()).0, + &rx_cx.get_public_keys(keychain.secp()).0, + ], + ) + .unwrap(); + + let msg = kernel_sig_msg(); + let sig_part = aggsig::calculate_partial_sig( + &keychain.secp(), + &rx_cx.sec_key, + &rx_cx.sec_nonce, + &pub_nonce_sum, + Some(&pub_key_sum), + &msg, + ) + .unwrap(); + (pub_excess, pub_nonce, sig_part) + }; + + // check the sender can verify the partial signature + // received in the response back from the receiver + { + let keychain = sender_keychain.clone(); + let msg = kernel_sig_msg(); + let sig_verifies = aggsig::verify_partial_sig( + &keychain.secp(), + &rx_sig_part, + &pub_nonce_sum, + &receiver_pub_excess, + Some(&pub_key_sum), + &msg, + ); + assert!(!sig_verifies.is_err()); + } + + // now sender signs with their key + let sender_sig_part = { + let keychain = sender_keychain.clone(); + let msg = kernel_sig_msg(); + let sig_part = aggsig::calculate_partial_sig( + &keychain.secp(), + &s_cx.sec_key, + &s_cx.sec_nonce, + &pub_nonce_sum, + Some(&pub_key_sum), + &msg, + ) + .unwrap(); + sig_part + }; + + // check the receiver can verify the partial signature + // received by the sender + { + let keychain = receiver_keychain.clone(); + let msg = kernel_sig_msg(); + let sig_verifies = aggsig::verify_partial_sig( + &keychain.secp(), + &sender_sig_part, + &pub_nonce_sum, + &sender_pub_excess, + Some(&pub_key_sum), + &msg, + ); + assert!(!sig_verifies.is_err()); + } + + // Receiver now builds final signature from sender and receiver parts + let (final_sig, final_pubkey) = { + let keychain = receiver_keychain.clone(); + + let msg = kernel_sig_msg(); + let our_sig_part = aggsig::calculate_partial_sig( + &keychain.secp(), + &rx_cx.sec_key, + &rx_cx.sec_nonce, + &pub_nonce_sum, + Some(&pub_key_sum), + &msg, + ) + .unwrap(); + + // Receiver now generates final signature from the two parts + let final_sig = aggsig::add_signatures( + &keychain.secp(), + vec![&sender_sig_part, &our_sig_part], + &pub_nonce_sum, + ) + .unwrap(); + + // Receiver calculates the final public key (to verify sig later) + let final_pubkey = PublicKey::from_combination( + keychain.secp(), + vec![ + &s_cx.get_public_keys(keychain.secp()).0, + &rx_cx.get_public_keys(keychain.secp()).0, + ], + ) + .unwrap(); + + (final_sig, final_pubkey) + }; + + // Receiver checks the final signature verifies + { + let keychain = receiver_keychain.clone(); + let msg = kernel_sig_msg(); + + // Receiver check the final signature verifies + let sig_verifies = aggsig::verify_completed_sig( + &keychain.secp(), + &final_sig, + &final_pubkey, + Some(&final_pubkey), + &msg, + ); + assert!(!sig_verifies.is_err()); + } + + // Check we can verify the sig using the kernel excess + { + let keychain = ExtKeychain::from_random_seed(true).unwrap(); + let msg = kernel_sig_msg(); + let sig_verifies = + aggsig::verify_single_from_commit(&keychain.secp(), &final_sig, &msg, &kernel_excess); + + assert!(!sig_verifies.is_err()); + } +} + +#[test] +fn aggsig_sender_receiver_interaction_offset() { + let parent = ExtKeychainPath::new(1, 1, 0, 0, 0).to_identifier(); + let switch = SwitchCommitmentType::Regular; + let sender_keychain = ExtKeychain::from_random_seed(true).unwrap(); + let receiver_keychain = ExtKeychain::from_random_seed(true).unwrap(); + + // This is the kernel offset that we use to split the key + // Summing these at the block level prevents the + // kernels from being used to reconstruct (or identify) individual transactions + let kernel_offset = SecretKey::new(&sender_keychain.secp(), &mut thread_rng()); + + // Calculate the kernel excess here for convenience. + // Normally this would happen during transaction building. + let kernel_excess = { + let id1 = ExtKeychain::derive_key_id(1, 1, 0, 0, 0); + let skey1 = sender_keychain.derive_key(0, &id1, switch).unwrap(); + let skey2 = receiver_keychain.derive_key(0, &id1, switch).unwrap(); + + let keychain = ExtKeychain::from_random_seed(true).unwrap(); + let blinding_factor = keychain + .blind_sum( + &BlindSum::new() + .sub_blinding_factor(BlindingFactor::from_secret_key(skey1)) + .add_blinding_factor(BlindingFactor::from_secret_key(skey2)) + // subtract the kernel offset here like as would when + // verifying a kernel signature + .sub_blinding_factor(BlindingFactor::from_secret_key(kernel_offset.clone())), + ) + .unwrap(); + + keychain + .secp() + .commit(0, blinding_factor.secret_key(&keychain.secp()).unwrap()) + .unwrap() + }; + + let s_cx; + let mut rx_cx; + // sender starts the tx interaction + let (sender_pub_excess, _sender_pub_nonce) = { + let keychain = sender_keychain.clone(); + let id1 = ExtKeychain::derive_key_id(1, 1, 0, 0, 0); + let skey = keychain.derive_key(0, &id1, switch).unwrap(); + + // dealing with an input here so we need to negate the blinding_factor + // rather than use it as is + let blinding_factor = keychain + .blind_sum( + &BlindSum::new() + .sub_blinding_factor(BlindingFactor::from_secret_key(skey)) + // subtract the kernel offset to create an aggsig context + // with our "split" key + .sub_blinding_factor(BlindingFactor::from_secret_key(kernel_offset)), + ) + .unwrap(); + + let blind = blinding_factor.secret_key(&keychain.secp()).unwrap(); + + s_cx = Context::with_excess(&keychain.secp(), blind, &parent, false); + s_cx.get_public_keys(&keychain.secp()) + }; + + // receiver receives partial tx + let pub_nonce_sum; + let pub_key_sum; + let (receiver_pub_excess, _receiver_pub_nonce, sig_part) = { + let keychain = receiver_keychain.clone(); + let key_id = ExtKeychain::derive_key_id(1, 1, 0, 0, 0); + + let blind = keychain.derive_key(0, &key_id, switch).unwrap(); + + rx_cx = Context::with_excess(&keychain.secp(), blind, &parent, false); + let (pub_excess, pub_nonce) = rx_cx.get_public_keys(&keychain.secp()); + rx_cx.add_output(&key_id, &None, 0); + + pub_nonce_sum = PublicKey::from_combination( + keychain.secp(), + vec![ + &s_cx.get_public_keys(keychain.secp()).1, + &rx_cx.get_public_keys(keychain.secp()).1, + ], + ) + .unwrap(); + + pub_key_sum = PublicKey::from_combination( + keychain.secp(), + vec![ + &s_cx.get_public_keys(keychain.secp()).0, + &rx_cx.get_public_keys(keychain.secp()).0, + ], + ) + .unwrap(); + + let msg = kernel_sig_msg(); + let sig_part = aggsig::calculate_partial_sig( + &keychain.secp(), + &rx_cx.sec_key, + &rx_cx.sec_nonce, + &pub_nonce_sum, + Some(&pub_key_sum), + &msg, + ) + .unwrap(); + (pub_excess, pub_nonce, sig_part) + }; + + // check the sender can verify the partial signature + // received in the response back from the receiver + { + let keychain = sender_keychain.clone(); + let msg = kernel_sig_msg(); + let sig_verifies = aggsig::verify_partial_sig( + &keychain.secp(), + &sig_part, + &pub_nonce_sum, + &receiver_pub_excess, + Some(&pub_key_sum), + &msg, + ); + assert!(!sig_verifies.is_err()); + } + + // now sender signs with their key + let sender_sig_part = { + let keychain = sender_keychain.clone(); + let msg = kernel_sig_msg(); + let sig_part = aggsig::calculate_partial_sig( + &keychain.secp(), + &s_cx.sec_key, + &s_cx.sec_nonce, + &pub_nonce_sum, + Some(&pub_key_sum), + &msg, + ) + .unwrap(); + sig_part + }; + + // check the receiver can verify the partial signature + // received by the sender + { + let keychain = receiver_keychain.clone(); + let msg = kernel_sig_msg(); + let sig_verifies = aggsig::verify_partial_sig( + &keychain.secp(), + &sender_sig_part, + &pub_nonce_sum, + &sender_pub_excess, + Some(&pub_key_sum), + &msg, + ); + assert!(!sig_verifies.is_err()); + } + + // Receiver now builds final signature from sender and receiver parts + let (final_sig, final_pubkey) = { + let keychain = receiver_keychain.clone(); + let msg = kernel_sig_msg(); + let our_sig_part = aggsig::calculate_partial_sig( + &keychain.secp(), + &rx_cx.sec_key, + &rx_cx.sec_nonce, + &pub_nonce_sum, + Some(&pub_key_sum), + &msg, + ) + .unwrap(); + + // Receiver now generates final signature from the two parts + let final_sig = aggsig::add_signatures( + &keychain.secp(), + vec![&sender_sig_part, &our_sig_part], + &pub_nonce_sum, + ) + .unwrap(); + + // Receiver calculates the final public key (to verify sig later) + let final_pubkey = PublicKey::from_combination( + keychain.secp(), + vec![ + &s_cx.get_public_keys(keychain.secp()).0, + &rx_cx.get_public_keys(keychain.secp()).0, + ], + ) + .unwrap(); + + (final_sig, final_pubkey) + }; + + // Receiver checks the final signature verifies + { + let keychain = receiver_keychain.clone(); + let msg = kernel_sig_msg(); + + // Receiver check the final signature verifies + let sig_verifies = aggsig::verify_completed_sig( + &keychain.secp(), + &final_sig, + &final_pubkey, + Some(&final_pubkey), + &msg, + ); + assert!(!sig_verifies.is_err()); + } + + // Check we can verify the sig using the kernel excess + { + let keychain = ExtKeychain::from_random_seed(true).unwrap(); + let msg = kernel_sig_msg(); + let sig_verifies = + aggsig::verify_single_from_commit(&keychain.secp(), &final_sig, &msg, &kernel_excess); + + assert!(!sig_verifies.is_err()); + } +} + +#[test] +fn test_rewind_range_proof() { + let keychain = ExtKeychain::from_random_seed(true).unwrap(); + let builder = proof::ProofBuilder::new(&keychain); + let key_id = ExtKeychain::derive_key_id(1, 1, 0, 0, 0); + let key_id2 = ExtKeychain::derive_key_id(1, 2, 0, 0, 0); + let switch = SwitchCommitmentType::Regular; + let commit = keychain.commit(5, &key_id, switch).unwrap(); + let extra_data = [99u8; 64]; + + let proof = proof::create( + &keychain, + &builder, + 5, + &key_id, + switch, + commit, + Some(extra_data.to_vec().clone()), + ) + .unwrap(); + let proof_info = proof::rewind( + keychain.secp(), + &builder, + commit, + Some(extra_data.to_vec().clone()), + proof, + ) + .unwrap(); + + assert!(proof_info.is_some()); + let (r_amount, r_key_id, r_switch) = proof_info.unwrap(); + assert_eq!(r_amount, 5); + assert_eq!(r_key_id, key_id); + assert_eq!(r_switch, switch); + + // cannot rewind with a different commit + let commit2 = keychain.commit(5, &key_id2, switch).unwrap(); + let proof_info = proof::rewind( + keychain.secp(), + &builder, + commit2, + Some(extra_data.to_vec().clone()), + proof, + ) + .unwrap(); + assert!(proof_info.is_none()); + + // cannot rewind with a commitment to a different value + let commit3 = keychain.commit(4, &key_id, switch).unwrap(); + let proof_info = proof::rewind( + keychain.secp(), + &builder, + commit3, + Some(extra_data.to_vec().clone()), + proof, + ) + .unwrap(); + assert!(proof_info.is_none()); + + // cannot rewind with wrong extra committed data + let wrong_extra_data = [98u8; 64]; + let proof_info = proof::rewind( + keychain.secp(), + &builder, + commit, + Some(wrong_extra_data.to_vec().clone()), + proof, + ) + .unwrap(); + assert!(proof_info.is_none()); +} diff --git a/libwallet/tests/slate_versioning.rs b/libwallet/tests/slate_versioning.rs new file mode 100644 index 0000000..2db6a8c --- /dev/null +++ b/libwallet/tests/slate_versioning.rs @@ -0,0 +1,96 @@ +// Copyright 2021 The Grin Developers +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! core::libtx specific tests +//use grin_wallet_libwallet::Slate; + +// test all slate conversions +/* TODO: Turn back on upon release of new slate version +#[test] +fn slate_conversions() { + // Test V0 to V2 + let v0 = include_str!("slates/v0.slate"); + let res = Slate::deserialize_upgrade(&v0); + assert!(res.is_ok()); + // should serialize as latest + let mut res = res.unwrap(); + assert_eq!(res.version_info.orig_version, 0); + res.version_info.orig_version = 2; + let s = serde_json::to_string(&res); + assert!(s.is_ok()); + let s = s.unwrap(); + let v = Slate::parse_slate_version(&s); + assert!(v.is_ok()); + assert_eq!(v.unwrap(), 2); + println!("v0 -> v2: {}", s); + + // Test V1 to V2 + let v1 = include_str!("slates/v1.slate"); + let res = Slate::deserialize_upgrade(&v1); + assert!(res.is_ok()); + // should serialize as latest + let mut res = res.unwrap(); + assert_eq!(res.version_info.orig_version, 1); + res.version_info.orig_version = 2; + let s = serde_json::to_string(&res); + assert!(s.is_ok()); + let s = s.unwrap(); + let v = Slate::parse_slate_version(&s); + assert!(v.is_ok()); + assert_eq!(v.unwrap(), 2); + println!("v1 -> v2: {}", s); + + // V2 -> V2, check version + let v2 = include_str!("slates/v2.slate"); + let res = Slate::deserialize_upgrade(&v2); + assert!(res.is_ok()); + let res = res.unwrap(); + assert_eq!(res.version_info.orig_version, 2); + let s = serde_json::to_string(&res); + assert!(s.is_ok()); + let s = s.unwrap(); + let v = Slate::parse_slate_version(&s); + assert!(v.is_ok()); + assert_eq!(v.unwrap(), 2); + + // Downgrade to V1 + let v2 = include_str!("slates/v2.slate"); + let res = Slate::deserialize_upgrade(&v2); + assert!(res.is_ok()); + let mut res = res.unwrap(); + // downgrade + res.version_info.orig_version = 1; + let s = serde_json::to_string(&res); + assert!(s.is_ok()); + let s = s.unwrap(); + let v = Slate::parse_slate_version(&s); + assert!(v.is_ok()); + assert_eq!(v.unwrap(), 1); + println!("v2 -> v1: {}", s); + + // Downgrade to V0 + let v2 = include_str!("slates/v2.slate"); + let res = Slate::deserialize_upgrade(&v2); + assert!(res.is_ok()); + let mut res = res.unwrap(); + // downgrade + res.version_info.orig_version = 0; + let s = serde_json::to_string(&res); + assert!(s.is_ok()); + let s = s.unwrap(); + let v = Slate::parse_slate_version(&s); + assert!(v.is_ok()); + assert_eq!(v.unwrap(), 0); + println!("v2 -> v0: {}", s); +} +*/ diff --git a/libwallet/tests/slates/v2.slate b/libwallet/tests/slates/v2.slate new file mode 100644 index 0000000..d55bf12 --- /dev/null +++ b/libwallet/tests/slates/v2.slate @@ -0,0 +1,54 @@ +{ + "version_info": { + "version": 2, + "orig_version": 2, + "block_header_version": 1 + }, + "num_participants": 2, + "id": "e0c69803-db50-40d9-a968-496e86660cd4", + "tx": { + "offset": "a853afebf15d8c111f654059940945b4782c38660397257707b53ebfdb403a52", + "body": { + "inputs": [ + { + "features": "Plain", + "commit": "09d304aed6300f8124eb8b2d46cc1e0a7b7a9b9042b9cb35e020dd9552df9c697c" + }, + { + "features": "Plain", + "commit": "09d3cc915dc317485dc8bbf5ec4669a40bb9d3300c96df3384d116ddad498d0db1" + } + ], + "outputs": [ + { + "features": "Plain", + "commit": "08d3453eb5ce35a1b6bbc2a7a9afe32483774c011f9975f42393468fa5cd4349a7", + "proof": "db206834c022eec1f346a67b571941f1b6867ae4bd8189ca064b690b32367e454a4a5add51761c472b0e0994ce7f00578bc06ae7b9afdf8ce2118546771976d900464214d3b831fe74a94876980a928315afb5c2af018f5d595e56fd740658b0c4f2d4f463e401cbec2704b31005cd8d7d87458290a3668cc2e82c2b0867d991072544f9e8c805056c97ff66cc052cf2a9666768d0d68acdc6ea1fc80fb9b5e6e19366c7b49ada38b368c0c3e3f73977df003f0c6744737b31b058c7d4e2766e97ee04147ef04be22906f087842205813c7d817598c689c840087d35cc9ce9a98f52e68c66bdde0521acf814737efd072654728f418e6494a7eb7fa6305ec7d572abb91d3bfabf7215e77e0c9cf33769572ff9a8671a24e0a04302e6ac5cee9928ec11d7c9861ed18718142a1563967955e428e4134c6dde88bdbea11248ae99d784a56592a065122948b2c2fb8be25c119345b9fa7db2efbdfcf846e9ba47efff3d0024bdb998e93bcabe1a00222ba36b88ec4f7c2a2151bf00b225f6a14b4de66658daecaa219813f51a9239eec961c6713106b64c4f1ff851e54795220ee3cdc59531f0acc050e17c848b21b916b571b2f6b093fccec046587d0a1718c82bd7a78e22223fe1484dec841820139950dce84c97659b0eac1bfa5fce85d5602f480d714dcab1459c4f29e2746bccb4494d800935ddc630f53257649f1544702003a583d55422e957192faebffcb8d883ec6bb2132c86249d6b50edae84f3c06842b2714267249c8df58e2edc3aca69dff66ee32fb5d93db9156df373ab51df2c094742517b46ff95298caec3464151ea91c8a8fe74bb60ffb94c7c974aa6cb2e47dd1ee05f471e2d2f0b555efe17302769139760bc110c979453f7bfab43b3f3cba4d94c8a5eeb58264bb5c16de6acbbc9c56cb069e7e1ac1f7838d0a6424017b8d563" + } + ], + "kernels": [ + { + "features": "HeightLocked", + "fee": "7000000", + "lock_height": "70194", + "excess": "000000000000000000000000000000000000000000000000000000000000000000", + "excess_sig": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + } + ] + } + }, + "amount": "84825921007", + "fee": "7000000", + "height": "70194", + "lock_height": "70194", + "participant_data": [ + { + "id": "0", + "public_blind_excess": "0391f8fc74bb5ff4de373352e7dee00860d4fb78ed7a99765585af980d8a31c615", + "public_nonce": "0206562c21a7f3a003622722ee93c4ecbbecead4a6ad8ee5d930b51ca4a6ca6d01", + "part_sig": null, + "message": null, + "message_sig": null + } + ] +} diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..b2b2431 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,2 @@ +hard_tabs = true +edition = "2018" diff --git a/src/bin/grin-wallet.rs b/src/bin/grin-wallet.rs new file mode 100644 index 0000000..9189f02 --- /dev/null +++ b/src/bin/grin-wallet.rs @@ -0,0 +1,166 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Main for building the binary of a Grin Reference Wallet + +#[macro_use] +extern crate clap; +#[macro_use] +extern crate log; +use crate::config::ConfigError; +use crate::core::global; +use crate::util::init_logger; +use clap::App; +use grin_core as core; +use grin_util as util; +use grin_wallet::cmd; +use grin_wallet_config as config; +use grin_wallet_impls::HTTPNodeClient; +use std::env; +use std::path::PathBuf; +use std::path::MAIN_SEPARATOR; + +// include build information +pub mod built_info { + include!(concat!(env!("OUT_DIR"), "/built.rs")); +} + +pub fn info_strings() -> (String, String) { + ( + format!( + "This is Grin Wallet version {}{}, built for {} by {}.", + built_info::PKG_VERSION, + built_info::GIT_VERSION.map_or_else(|| "".to_owned(), |v| format!(" (git {})", v)), + built_info::TARGET, + built_info::RUSTC_VERSION, + ) + .to_string(), + format!( + "Built with profile \"{}\", features \"{}\".", + built_info::PROFILE, + built_info::FEATURES_STR, + ) + .to_string(), + ) +} + +// Helper fuction to format paths according to OS, avoids bugs on Linux +pub fn fmt_path(path: String) -> String { + let sep = &MAIN_SEPARATOR.to_string(); + let path = path.replace("/", &sep).replace("\\", &sep); + path +} + +fn log_build_info() { + let (basic_info, detailed_info) = info_strings(); + info!("{}", basic_info); + debug!("{}", detailed_info); +} + +fn main() { + let exit_code = real_main(); + std::process::exit(exit_code); +} + +fn real_main() -> i32 { + let yml = load_yaml!("grin-wallet.yml"); + let args = App::from_yaml(yml) + .version(built_info::PKG_VERSION) + .get_matches(); + + let chain_type = if args.is_present("testnet") { + global::ChainTypes::Testnet + } else if args.is_present("usernet") { + global::ChainTypes::UserTesting + } else { + global::ChainTypes::Mainnet + }; + + let mut current_dir = None; + let mut create_path = false; + if args.is_present("top_level_dir") { + let res = args.value_of("top_level_dir"); + match res { + Some(d) => { + let d = fmt_path(d.to_owned().to_string()); // Fix for fs to work with paths on Linux + current_dir = Some(PathBuf::from(d)); + } + None => { + warn!("Argument --top_level_dir needs a value. Defaulting to current directory") + } + } + } + + // special cases for certain lifecycle commands + match args.subcommand() { + ("init", Some(init_args)) => { + if init_args.is_present("here") { + current_dir = Some(env::current_dir().unwrap_or_else(|e| { + panic!("Error creating config file: {}", e); + })); + } + create_path = true; + } + _ => {} + } + + // Load relevant config, try and load a wallet config file + // Use defaults for configuration if config file not found anywhere + let mut config = match config::initial_setup_wallet(&chain_type, current_dir, create_path) { + Ok(c) => c, + Err(e) => match e { + ConfigError::PathNotFoundError(m) => { + println!("Wallet configuration not found at {}. (Run `grin-wallet init` to create a new wallet)", m); + return 0; + } + m => { + println!("Unable to load wallet configuration: {} (Run `grin-wallet init` to create a new wallet)", m); + return 0; + } + }, + }; + + //config.members.as_mut().unwrap().wallet.chain_type = Some(chain_type); + + // Load logging config + let mut l = config.members.as_mut().unwrap().logging.clone().unwrap(); + // no logging to stdout if we're running cli + match args.subcommand() { + ("cli", _) => l.log_to_stdout = true, + _ => {} + }; + init_logger(Some(l), None); + info!( + "Using wallet configuration file at {}", + config.config_file_path.as_ref().unwrap().to_str().unwrap() + ); + log_build_info(); + + global::init_global_chain_type( + config + .members + .as_ref() + .unwrap() + .wallet + .chain_type + .as_ref() + .unwrap() + .clone(), + ); + + global::init_global_accept_fee_base(config.members.as_ref().unwrap().wallet.accept_fee_base()); + let wallet_config = config.clone().members.unwrap().wallet; + let node_client = HTTPNodeClient::new(&wallet_config.check_node_api_http_addr, None).unwrap(); + cmd::wallet_command(&args, config, node_client) +} diff --git a/src/bin/grin-wallet.yml b/src/bin/grin-wallet.yml new file mode 100644 index 0000000..39a8962 --- /dev/null +++ b/src/bin/grin-wallet.yml @@ -0,0 +1,448 @@ +name: grin-wallet +about: Reference Grin Wallet +author: The Grin Team + +args: + - testnet: + help: Run grin against the Testnet (as opposed to mainnet) + long: testnet + takes_value: false + - usernet: + help: Run grin as a local-only network. Doesn't block peer connections but will not connect to any peer or seed + long: usernet + takes_value: false + - pass: + help: Wallet passphrase used to encrypt wallet seed + short: p + long: pass + takes_value: true + - account: + help: Wallet account to use for this operation + short: a + long: account + takes_value: true + default_value: default + - top_level_dir: + help: Top directory in which wallet files are stored (location of 'grin-wallet.toml') + short: t + long: top_level_dir + takes_value: true + - show_spent: + help: Show spent outputs on wallet output commands + short: s + long: show_spent + takes_value: false + - api_server_address: + help: Api address of running node on which to check inputs and post transactions + short: r + long: api_server_address + takes_value: true +subcommands: + - cli: + about: Start the wallet in interactive CLI mode (EXPERIMENTAL and UNDER DEVELOPMENT) + - account: + about: List wallet accounts or create a new account + args: + - create: + help: Create a new wallet account with provided name + short: c + long: create + takes_value: true + - rewind_hash: + about: Return the hash of the wallet root public key. + - scan_rewind_hash: + about: Scan the UTXO set and return the outputs and the total of grin owned by a view wallet rewind hash. + args: + - rewind_hash: + help: Rewind hash of the wallet to be scanned in order to retrieve all the outputs and balance. + index: 1 + - start_height: + help: If given, the first block from which to start the scan (default 1) + short: h + long: start_height + takes_value: true + - backwards_from_tip: + help: If given, start scan b blocks back from the tip + short: b + long: backwards_from_tip, + takes_value: true + - listen: + about: Runs the wallet in listening mode waiting for transactions + args: + - port: + help: Port on which to run the wallet listener + short: l + long: port + takes_value: true + - no_tor: + help: Don't start Tor listener when starting HTTP listener + short: n + long: no_tor + takes_value: false + - bridge: + help: Enable bridge relay with TOR listener + short: g + long: bridge + takes_value: true + - owner_api: + about: Runs the wallet's local web API + args: + - port: + help: Port on which to run the wallet owner listener + short: l + long: port + takes_value: true + - run_foreign: + help: Also run the Foreign API + long: run_foreign + takes_value: false + - send: + about: Builds a transaction to send coins and sends to the recipient via the Slatepack workflow + args: + - amount: + help: Number of coins to send with optional fraction, e.g. 12.423. Keyword 'max' will send maximum amount. + index: 1 + - minimum_confirmations: + help: Minimum number of confirmations required for an output to be spendable + short: c + long: min_conf + default_value: "10" + takes_value: true + - selection_strategy: + help: Coin/Output selection strategy. + short: s + long: selection + possible_values: + - all + - smallest + default_value: smallest + takes_value: true + - estimate_selection_strategies: + help: Estimates all possible Coin/Output selection strategies. + short: e + long: estimate-selection + - late_lock: + help: EXPERIMENTAL - Do not lock the coins immediately, instead only lock them during finalization. + short: l + long: late-lock + - change_outputs: + help: Number of change outputs to generate (mainly for testing) + short: o + long: change_outputs + default_value: "1" + takes_value: true + - dest: + help: Intended recipient's Slatepack Address (or http listener address (DEPRECATED)) + short: d + long: dest + takes_value: true + - no_payment_proof: + help: Don't request a payment proof, even if the Recipient's Slatepack address is provided in the -dest argument + short: n + long: no_payment_proof + - fluff: + help: Fluff the transaction (ignore Dandelion relay protocol) + short: f + long: fluff + - stored_tx: + help: If present, use the previously stored Unconfirmed transaction with given id + short: t + long: stored_tx + takes_value: true + - ttl_blocks: + help: If present, the number of blocks from the current after which wallets should refuse to process transactions further + short: b + long: ttl_blocks + takes_value: true + - manual: + help: If present, don't attempt to send the resulting Slatepack via TOR + short: m + long: manual + - outfile: + help: If present, overrides the filename and location of the output Slatepack file. + short: u + long: outfile + takes_value: true + - bridge: + help: Enable tor bridge relay when sending via Slatepack workflow + short: g + long: bridge + takes_value: true + - slatepack_qr: + help: Show slatepack data as QR code + short: q + long: slatepack_qr + - amount_includes_fee: + help: Transaction amount includes transaction fee. Recipient will receive (amount - fee). + long: amount_includes_fee + - unpack: + about: Unpack and display an armored Slatepack Message, decrypting if possible + args: + - input: + help: File containing a Slatepack Message + short: i + long: input + takes_value: true + - receive: + about: Processes a Slatepack Message to accept a transfer from a sender + args: + - input: + help: File containing a Slatepack Message + short: i + long: input + takes_value: true + - manual: + help: If present, don't attempt to send the resulting Slatepack via TOR + short: m + long: manual + - outfile: + help: If present, overrides the filename and location of the output Slatepack file. + short: u + long: outfile + takes_value: true + - bridge: + help: Enable tor bridge relay when receiving via Slatepack workflow + short: g + long: bridge + takes_value: true + - slatepack_qr: + help: Show slatepack data as QR code + short: q + long: slatepack_qr + - finalize: + about: Processes a Slatepack Message to finalize a transfer. + args: + - input: + help: Partial transaction to process, expects the receiver's transaction file. + short: i + long: input + takes_value: true + - fluff: + help: Fluff the transaction (ignore Dandelion relay protocol) + short: f + long: fluff + - nopost: + help: Do not post the transaction. + short: n + long: nopost + - outfile: + help: If present, overrides the filename and location of the output Slatepack file. + short: u + long: outfile + takes_value: true + - slatepack_qr: + help: Show slatepack data as QR code + short: q + long: slatepack_qr + - invoice: + about: Initialize an invoice transaction, outputting a Slatepack Message with the result + args: + - amount: + help: Number of coins to invoice with optional fraction, e.g. 12.423 + index: 1 + - dest: + help: Intended recipient's Slatepack Address + short: d + long: dest + takes_value: true + - outfile: + help: If present, overrides the filename and location of the output Slatepack file. + short: u + long: outfile + takes_value: true + - slatepack_qr: + help: Show slatepack data as QR code + short: q + long: slatepack_qr + - pay: + about: Spend coins to pay the provided invoice transaction + args: + - minimum_confirmations: + help: Minimum number of confirmations required for an output to be spendable + short: c + long: min_conf + default_value: "10" + takes_value: true + - selection_strategy: + help: Coin/Output selection strategy. + short: s + long: selection + possible_values: + - all + - smallest + default_value: smallest + takes_value: true + - estimate_selection_strategies: + help: Estimates all possible Coin/Output selection strategies. + short: e + long: estimate-selection + - dest: + help: The Slatepack address of the invoicing party's wallet (will override the address contained in the Slatepack) + short: d + long: dest + takes_value: true + - input: + help: Incoming Slatepack Message to process + short: i + long: input + takes_value: true + - ttl_blocks: + help: If present, the number of blocks from the current after which wallets should refuse to process transactions further + short: b + long: ttl_blocks + takes_value: true + - manual: + help: If present, don't attempt to send the resulting Slatepack via TOR + short: m + long: manual + - outfile: + help: If present, overrides the filename and location of the output Slatepack file. + short: u + long: outfile + takes_value: true + - bridge: + help: Enable tor bridge relay when paying invoice. + short: g + long: bridge + takes_value: true + - slatepack_qr: + help: Show slatepack data as QR code + short: q + long: slatepack_qr + - outputs: + about: Raw wallet output info (list of outputs) + - txs: + about: Display transaction information + args: + - id: + help: If specified, display transaction with given Id and all associated Inputs/Outputs + short: i + long: id + takes_value: true + - txid: + help: If specified, display transaction with given TxID UUID and all associated Inputs/Outputs + short: t + long: txid + takes_value: true + - count: + help: Maximum number of transactions to show + short: c + long: count + takes_value: true + - post: + about: Posts a finalized transaction to the chain + args: + - input: + help: File name of the transaction to post + short: i + long: input + takes_value: true + - fluff: + help: Fluff the transaction (ignore Dandelion relay protocol) + short: f + long: fluff + - repost: + about: Reposts a stored, completed but unconfirmed transaction to the chain, or dumps it to a file + args: + - id: + help: Transaction ID containing the stored completed transaction + short: i + long: id + takes_value: true + - dumpfile: + help: File name to duMp the transaction to instead of posting + short: m + long: dumpfile + takes_value: true + - fluff: + help: Fluff the transaction (ignore Dandelion relay protocol) + short: f + long: fluff + - cancel: + about: Cancels a previously created transaction, freeing previously locked outputs for use again + args: + - id: + help: The ID of the transaction to cancel + short: i + long: id + takes_value: true + - txid: + help: The TxID UUID of the transaction to cancel + short: t + long: txid + takes_value: true + - info: + about: Basic wallet contents summary + args: + - minimum_confirmations: + help: Minimum number of confirmations required for an output to be spendable + short: c + long: min_conf + default_value: "10" + takes_value: true + - init: + about: Initialize a new wallet seed file and database + args: + - here: + help: Create wallet files in the current directory instead of the default ~/.grin directory + short: h + long: here + takes_value: false + - short_wordlist: + help: Generate a 12-word recovery phrase/seed instead of default 24 + short: s + long: short_wordlist + takes_value: false + - recover: + help: Initialize new wallet using a recovery phrase + short: r + long: recover + takes_value: false + - open: + about: Opens a wallet (interactive mode only) + - close: + about: Closes the wallet (interactive mode only) + - recover: + about: Displays a recovery phrase for the wallet. (use `init -r` to perform recovery) + - address: + about: Display the wallet's Slatepack address + - scan: + about: Checks a wallet's outputs against a live node, repairing and restoring missing outputs if required + args: + - delete_unconfirmed: + help: Delete any unconfirmed outputsm unlock any locked outputs and delete associated transactions while doing the check. + short: d + long: delete_unconfirmed + takes_value: false + - start_height: + help: If given, the first block from which to start the scan (default 1) + short: h + long: start_height + takes_value: true + - backwards_from_tip: + help: If given, start scan b blocks back from the tip + short: b + long: backwards_from_tip, + takes_value: true + - export_proof: + about: Export a payment proof from a completed transaction + args: + - output: + help: Output proof file + index: 1 + - id: + help: If specified, retrieve the proof for the given transaction ID + short: i + long: id + takes_value: true + - txid: + help: If specified, retrieve the proof for the given Slate ID + short: t + long: txid + takes_value: true + - verify_proof: + about: Verify a payment proof + args: + - input: + help: Filename of a proof file + index: 1 diff --git a/src/build/build.rs b/src/build/build.rs new file mode 100644 index 0000000..1d3d9bb --- /dev/null +++ b/src/build/build.rs @@ -0,0 +1,48 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Build hooks to spit out version+build time info + +use built; +use std::env; +use std::path::{Path, PathBuf}; +use std::process::Command; + +fn main() { + // Setting up git hooks in the project: rustfmt and so on. + let git_hooks = format!( + "git config core.hooksPath {}", + PathBuf::from("./.hooks").to_str().unwrap() + ); + + if cfg!(target_os = "windows") { + Command::new("cmd") + .args(&["/C", &git_hooks]) + .output() + .expect("failed to execute git config for hooks"); + } else { + Command::new("sh") + .args(&["-c", &git_hooks]) + .output() + .expect("failed to execute git config for hooks"); + } + + // build and versioning information + let out_dir_path = format!("{}{}", env::var("OUT_DIR").unwrap(), "/built.rs"); + // don't fail the build if something's missing, may just be cargo release + let _ = built::write_built_file_with_opts( + Some(Path::new(env!("CARGO_MANIFEST_DIR"))), + Path::new(&out_dir_path), + ); +} diff --git a/src/cli/cli.rs b/src/cli/cli.rs new file mode 100644 index 0000000..c14f3d4 --- /dev/null +++ b/src/cli/cli.rs @@ -0,0 +1,312 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::cmd::wallet_args; +use crate::util::secp::key::SecretKey; +use crate::util::Mutex; +use clap::App; +//use colored::Colorize; +use grin_keychain as keychain; +use grin_wallet_api::Owner; +use grin_wallet_config::{TorConfig, WalletConfig}; +use grin_wallet_controller::command::GlobalArgs; +use grin_wallet_controller::Error; +use grin_wallet_impls::DefaultWalletImpl; +use grin_wallet_libwallet::{NodeClient, StatusMessage, WalletInst, WalletLCProvider}; +use rustyline::completion::{Completer, FilenameCompleter, Pair}; +use rustyline::error::ReadlineError; +use rustyline::highlight::{Highlighter, MatchingBracketHighlighter}; +use rustyline::hint::Hinter; +use rustyline::validate::Validator; +use rustyline::{CompletionType, Config, Context, EditMode, Editor, Helper, OutputStreamType}; +use std::borrow::Cow::{self, Borrowed, Owned}; +use std::sync::mpsc::{channel, Receiver}; +use std::sync::Arc; +use std::thread; +use std::time::Duration; + +const COLORED_PROMPT: &'static str = "\x1b[36mgrin-wallet>\x1b[0m "; +const PROMPT: &'static str = "grin-wallet> "; +//const HISTORY_PATH: &str = ".history"; + +// static for keeping track of current stdin buffer contents +lazy_static! { + static ref STDIN_CONTENTS: Mutex = Mutex::new(String::from("")); +} + +#[macro_export] +macro_rules! cli_message_inline { + ($fmt_string:expr, $( $arg:expr ),+) => { + { + use std::io::Write; + let contents = STDIN_CONTENTS.lock(); + /* use crate::common::{is_cli, COLORED_PROMPT}; */ + /* if is_cli() { */ + print!("\r"); + print!($fmt_string, $( $arg ),*); + print!(" {}", COLORED_PROMPT); + print!("\x1B[J"); + print!("{}", *contents); + std::io::stdout().flush().unwrap(); + /*} else { + info!($fmt_string, $( $arg ),*); + }*/ + } + }; +} + +#[macro_export] +macro_rules! cli_message { + ($fmt_string:expr, $( $arg:expr ),+) => { + { + use std::io::Write; + /* use crate::common::{is_cli, COLORED_PROMPT}; */ + /* if is_cli() { */ + //print!("\r"); + print!($fmt_string, $( $arg ),*); + println!(); + std::io::stdout().flush().unwrap(); + /*} else { + info!($fmt_string, $( $arg ),*); + }*/ + } + }; +} + +/// function to catch updates +pub fn start_updater_thread(rx: Receiver) -> Result<(), Error> { + let _ = thread::Builder::new() + .name("wallet-updater-status".to_string()) + .spawn(move || loop { + while let Ok(m) = rx.recv() { + match m { + StatusMessage::UpdatingOutputs(s) => cli_message_inline!("{}", s), + StatusMessage::UpdatingTransactions(s) => cli_message_inline!("{}", s), + StatusMessage::FullScanWarn(s) => cli_message_inline!("{}", s), + StatusMessage::Scanning(_, m) => { + //debug!("{}", s); + cli_message_inline!("Scanning - {}% complete - Please Wait", m); + } + StatusMessage::ScanningComplete(s) => cli_message_inline!("{}", s), + StatusMessage::UpdateWarning(s) => cli_message_inline!("{}", s), + } + } + }); + Ok(()) +} + +pub fn command_loop( + wallet_inst: Arc>>>, + keychain_mask: Option, + wallet_config: &WalletConfig, + tor_config: &TorConfig, + global_wallet_args: &GlobalArgs, + test_mode: bool, +) -> Result<(), Error> +where + DefaultWalletImpl<'static, C>: WalletInst<'static, L, C, K>, + L: WalletLCProvider<'static, C, K> + 'static, + C: NodeClient + 'static, + K: keychain::Keychain + 'static, +{ + let editor = Config::builder() + .history_ignore_space(true) + .completion_type(CompletionType::List) + .edit_mode(EditMode::Emacs) + .output_stream(OutputStreamType::Stdout) + .build(); + + let mut reader = Editor::with_config(editor); + reader.set_helper(Some(EditorHelper( + FilenameCompleter::new(), + MatchingBracketHighlighter::new(), + ))); + + /*let history_file = self + .api + .config() + .get_data_path() + .unwrap() + .parent() + .unwrap() + .join(HISTORY_PATH); + if history_file.exists() { + let _ = reader.load_history(&history_file); + }*/ + + let yml = load_yaml!("../bin/grin-wallet.yml"); + let mut app = App::from_yaml(yml).version(crate_version!()); + let mut keychain_mask = keychain_mask; + + // catch updater messages + let (tx, rx) = channel(); + let mut owner_api = Owner::new(wallet_inst, Some(tx)); + start_updater_thread(rx)?; + + // start the automatic updater + owner_api.start_updater((&keychain_mask).as_ref(), Duration::from_secs(30))?; + let mut wallet_opened = false; + loop { + match reader.readline(PROMPT) { + Ok(command) => { + if command.is_empty() { + continue; + } + // TODO tidy up a bit + if command.to_lowercase() == "exit" { + break; + } + /* use crate::common::{is_cli, COLORED_PROMPT}; */ + + // reset buffer + { + let mut contents = STDIN_CONTENTS.lock(); + *contents = String::from(""); + } + + // Just add 'grin-wallet' to each command behind the scenes + // so we don't need to maintain a separate definition file + let augmented_command = format!("grin-wallet {}", command); + let args = + app.get_matches_from_safe_borrow(augmented_command.trim().split_whitespace()); + let done = match args { + Ok(args) => { + // handle opening /closing separately + keychain_mask = match args.subcommand() { + ("open", Some(_)) => { + let mut wallet_lock = owner_api.wallet_inst.lock(); + let lc = wallet_lock.lc_provider().unwrap(); + let mask = match lc.open_wallet( + None, + wallet_args::prompt_password(&global_wallet_args.password), + false, + false, + ) { + Ok(m) => { + wallet_opened = true; + m + } + Err(e) => { + cli_message!("{}", e); + None + } + }; + if let Some(account) = args.value_of("account") { + if wallet_opened { + let wallet_inst = lc.wallet_inst()?; + wallet_inst.set_parent_key_id_by_name(account)?; + } + } + mask + } + ("close", Some(_)) => { + let mut wallet_lock = owner_api.wallet_inst.lock(); + let lc = wallet_lock.lc_provider().unwrap(); + lc.close_wallet(None)?; + None + } + _ => keychain_mask, + }; + match wallet_args::parse_and_execute( + &mut owner_api, + keychain_mask.clone(), + &wallet_config, + &tor_config, + &global_wallet_args, + &args, + test_mode, + true, + ) { + Ok(_) => { + cli_message!("Command '{}' completed", args.subcommand().0); + false + } + Err(err) => { + cli_message!("{}", err); + false + } + } + } + Err(err) => { + cli_message!("{}", err); + false + } + }; + reader.add_history_entry(command); + if done { + println!(); + break; + } + } + Err(err) => { + println!("Unable to read line: {}", err); + break; + } + } + } + Ok(()) + + //let _ = reader.save_history(&history_file); +} + +struct EditorHelper(FilenameCompleter, MatchingBracketHighlighter); + +impl Completer for EditorHelper { + type Candidate = Pair; + + fn complete( + &self, + line: &str, + pos: usize, + ctx: &Context<'_>, + ) -> std::result::Result<(usize, Vec), ReadlineError> { + self.0.complete(line, pos, ctx) + } +} + +impl Hinter for EditorHelper { + fn hint(&self, line: &str, _pos: usize, _ctx: &Context<'_>) -> Option { + let mut contents = STDIN_CONTENTS.lock(); + *contents = line.into(); + None + } +} + +impl Highlighter for EditorHelper { + fn highlight<'l>(&self, line: &'l str, pos: usize) -> Cow<'l, str> { + self.1.highlight(line, pos) + } + + fn highlight_prompt<'b, 's: 'b, 'p: 'b>( + &'s self, + prompt: &'p str, + default: bool, + ) -> Cow<'b, str> { + if default { + Borrowed(COLORED_PROMPT) + } else { + Borrowed(prompt) + } + } + + fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> { + Owned("\x1b[1m".to_owned() + hint + "\x1b[m") + } + + fn highlight_char(&self, line: &str, pos: usize) -> bool { + self.1.highlight_char(line, pos) + } +} +impl Validator for EditorHelper {} +impl Helper for EditorHelper {} diff --git a/src/cli/mod.rs b/src/cli/mod.rs new file mode 100644 index 0000000..7d24895 --- /dev/null +++ b/src/cli/mod.rs @@ -0,0 +1,17 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +mod cli; + +pub use cli::command_loop; diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs new file mode 100644 index 0000000..00fc0fb --- /dev/null +++ b/src/cmd/mod.rs @@ -0,0 +1,18 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +mod wallet; +pub mod wallet_args; + +pub use self::wallet::wallet_command; diff --git a/src/cmd/wallet.rs b/src/cmd/wallet.rs new file mode 100644 index 0000000..2a76b98 --- /dev/null +++ b/src/cmd/wallet.rs @@ -0,0 +1,78 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::cmd::wallet_args; +use crate::config::GlobalWalletConfig; +use clap::ArgMatches; +use grin_wallet_libwallet::NodeClient; +use semver::Version; +use std::thread; +use std::time::Duration; + +const MIN_COMPAT_NODE_VERSION: &str = "4.0.0-alpha.1"; + +pub fn wallet_command( + wallet_args: &ArgMatches<'_>, + config: GlobalWalletConfig, + mut node_client: C, +) -> i32 +where + C: NodeClient + 'static, +{ + // just get defaults from the global config + let wallet_config = config.members.clone().unwrap().wallet; + + let tor_config = config.members.unwrap().tor; + + // Check the node version info, and exit with report if we're not compatible + let global_wallet_args = wallet_args::parse_global_args(&wallet_config, &wallet_args) + .expect("Can't read configuration file"); + node_client.set_node_api_secret(global_wallet_args.node_api_secret.clone()); + + // This will also cache the node version info for calls to foreign API check middleware + if let Some(v) = node_client.clone().get_version_info() { + if Version::parse(&v.node_version) < Version::parse(MIN_COMPAT_NODE_VERSION) { + println!("The Grin Node in use (version {}) is outdated and incompatible with this wallet version.", v.node_version); + println!( + "Please update the node to version {} or later and try again.", + MIN_COMPAT_NODE_VERSION + ); + return 1; + } + } + // ... if node isn't available, allow offline functions + + let res = wallet_args::wallet_command( + wallet_args, + wallet_config, + tor_config, + node_client, + false, + |_| {}, + ); + + // we need to give log output a chance to catch up before exiting + thread::sleep(Duration::from_millis(100)); + + if let Err(e) = res { + println!("Wallet command failed: {}", e); + 1 + } else { + println!( + "Command '{}' completed successfully", + wallet_args.subcommand().0 + ); + 0 + } +} diff --git a/src/cmd/wallet_args.rs b/src/cmd/wallet_args.rs new file mode 100644 index 0000000..205cc0e --- /dev/null +++ b/src/cmd/wallet_args.rs @@ -0,0 +1,1299 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::api::TLSConfig; +use crate::cli::command_loop; +use crate::config::GRIN_WALLET_DIR; +use crate::util::file::get_first_line; +use crate::util::secp::key::SecretKey; +use crate::util::{Mutex, ZeroingString}; +/// Argument parsing and error handling for wallet commands +use clap::ArgMatches; +use grin_core as core; +use grin_core::core::amount_to_hr_string; +use grin_keychain as keychain; +use grin_wallet_api::Owner; +use grin_wallet_config::{TorConfig, WalletConfig}; +use grin_wallet_controller::{command, Error}; +use grin_wallet_impls::{DefaultLCProvider, DefaultWalletImpl}; +use grin_wallet_libwallet::{self, Slate, SlatepackAddress, SlatepackArmor}; +use grin_wallet_libwallet::{IssueInvoiceTxArgs, NodeClient, WalletInst, WalletLCProvider}; +use linefeed::terminal::Signal; +use linefeed::{Interface, ReadResult}; +use rpassword; +use std::convert::TryFrom; +use std::path::{Path, PathBuf}; +use std::sync::Arc; + +// define what to do on argument error +macro_rules! arg_parse { + ( $r:expr ) => { + match $r { + Ok(res) => res, + Err(e) => { + return Err(Error::ArgumentError(format!("{}", e))); + } + } + }; +} +/// Simple error definition, just so we can return errors from all commands +/// and let the caller figure out what to do +#[derive(Clone, Eq, PartialEq, Debug, thiserror::Error)] +pub enum ParseError { + #[error("Invalid Arguments: {0}")] + ArgumentError(String), + #[error("Parsing IO error: {0}")] + IOError(String), + #[error("Wallet configuration already exists: {0}")] + WalletExists(String), + #[error("User Cancelled")] + CancelledError, +} + +impl From for ParseError { + fn from(e: std::io::Error) -> ParseError { + ParseError::IOError(format!("{}", e)) + } +} + +fn prompt_password_stdout(prompt: &str) -> ZeroingString { + ZeroingString::from(rpassword::prompt_password_stdout(prompt).unwrap()) +} + +pub fn prompt_password(password: &Option) -> ZeroingString { + match password { + None => prompt_password_stdout("Password: "), + Some(p) => p.clone(), + } +} + +fn prompt_password_confirm() -> ZeroingString { + let mut first = ZeroingString::from("first"); + let mut second = ZeroingString::from("second"); + while first != second { + first = prompt_password_stdout("Password: "); + second = prompt_password_stdout("Confirm Password: "); + } + first +} + +fn prompt_recovery_phrase( + wallet: Arc>>>, +) -> Result +where + DefaultWalletImpl<'static, C>: WalletInst<'static, L, C, K>, + L: WalletLCProvider<'static, C, K>, + C: NodeClient + 'static, + K: keychain::Keychain + 'static, +{ + let interface = Arc::new(Interface::new("recover")?); + let mut phrase = ZeroingString::from(""); + interface.set_report_signal(Signal::Interrupt, true); + interface.set_prompt("phrase> ")?; + loop { + println!("Please enter your recovery phrase:"); + let res = interface.read_line()?; + match res { + ReadResult::Eof => break, + ReadResult::Signal(sig) => { + if sig == Signal::Interrupt { + interface.cancel_read_line()?; + return Err(ParseError::CancelledError); + } + } + ReadResult::Input(line) => { + let mut w_lock = wallet.lock(); + let p = w_lock.lc_provider().unwrap(); + if p.validate_mnemonic(ZeroingString::from(line.clone())) + .is_ok() + { + phrase = ZeroingString::from(line); + break; + } else { + println!(); + println!("Recovery word phrase is invalid."); + println!(); + interface.set_buffer(&line)?; + } + } + } + } + Ok(phrase) +} + +fn prompt_slatepack() -> Result { + let interface = Arc::new(Interface::new("slatepack_input")?); + let mut message = String::from(""); + interface.set_report_signal(Signal::Interrupt, true); + interface.set_prompt("")?; + loop { + println!("Please paste your encoded slatepack message:"); + let res = interface.read_line()?; + match res { + ReadResult::Eof => break, + ReadResult::Signal(sig) => { + if sig == Signal::Interrupt { + interface.cancel_read_line()?; + return Err(ParseError::CancelledError); + } + } + ReadResult::Input(line) => { + if SlatepackArmor::decode(line.as_bytes()).is_ok() { + message = line; + break; + } else { + println!(); + println!("Input is not a valid slatepack."); + println!(); + interface.set_buffer(&line)?; + } + } + } + } + Ok(message) +} + +fn prompt_pay_invoice(slate: &Slate, dest: &str) -> Result { + let interface = Arc::new(Interface::new("pay")?); + let amount = amount_to_hr_string(slate.amount, false); + interface.set_report_signal(Signal::Interrupt, true); + interface.set_prompt( + "To proceed, type the exact amount of the invoice as displayed above (or Q/q to quit) > ", + )?; + println!(); + println!( + "This command will pay the amount specified in the invoice using your wallet's funds." + ); + println!("After you confirm, the following will occur: "); + println!(); + println!( + "* {} of your wallet funds will be added to the transaction to pay this invoice.", + amount + ); + if dest.len() > 0 { + println!("* The wallet will IMMEDIATELY attempt to send the resulting transaction to the wallet listening at: '{}'.", dest); + println!("* If other wallet is not listening, the resulting transaction will output as a slatepack which you can manually send back to the invoice creator."); + } else { + println!("* The resulting transaction will output as a slatepack which you can manually send back to the invoice creator."); + } + println!(); + println!("Please review the above information carefully before proceeding"); + println!(); + loop { + let res = interface.read_line()?; + match res { + ReadResult::Eof => return Ok(false), + ReadResult::Signal(sig) => { + if sig == Signal::Interrupt { + interface.cancel_read_line()?; + return Err(ParseError::CancelledError); + } + } + ReadResult::Input(line) => { + match line.trim() { + "Q" | "q" => return Err(ParseError::CancelledError), + result => { + if result == amount { + return Ok(true); + } else { + println!("Please enter exact amount of the invoice as shown above or Q to quit"); + println!(); + } + } + } + } + } + } +} + +// instantiate wallet (needed by most functions) + +pub fn inst_wallet( + config: WalletConfig, + node_client: C, +) -> Result>>>, ParseError> +where + DefaultWalletImpl<'static, C>: WalletInst<'static, L, C, K>, + L: WalletLCProvider<'static, C, K>, + C: NodeClient + 'static, + K: keychain::Keychain + 'static, +{ + let mut wallet = Box::new(DefaultWalletImpl::<'static, C>::new(node_client.clone()).unwrap()) + as Box>; + let lc = wallet.lc_provider().unwrap(); + let _ = lc.set_top_level_directory(&config.data_file_dir); + Ok(Arc::new(Mutex::new(wallet))) +} + +// parses a required value, or throws error with message otherwise +fn parse_required<'a>(args: &'a ArgMatches, name: &str) -> Result<&'a str, ParseError> { + let arg = args.value_of(name); + match arg { + Some(ar) => Ok(ar), + None => { + let msg = format!("Value for argument '{}' is required in this context", name,); + Err(ParseError::ArgumentError(msg)) + } + } +} + +// parses an optional value, throws error if value isn't provided +fn parse_optional(args: &ArgMatches, name: &str) -> Result, ParseError> { + if !args.is_present(name) { + return Ok(None); + } + let arg = args.value_of(name); + match arg { + Some(ar) => Ok(Some(ar.into())), + None => { + let msg = format!("Value for argument '{}' is required in this context", name,); + Err(ParseError::ArgumentError(msg)) + } + } +} + +// parses a number, or throws error with message otherwise +fn parse_u64(arg: &str, name: &str) -> Result { + let val = arg.parse::(); + match val { + Ok(v) => Ok(v), + Err(e) => { + let msg = format!("Could not parse {} as a whole number. e={}", name, e); + Err(ParseError::ArgumentError(msg)) + } + } +} + +// As above, but optional +fn parse_u64_or_none(arg: Option<&str>) -> Option { + let val = match arg { + Some(a) => a.parse::(), + None => return None, + }; + match val { + Ok(v) => Some(v), + Err(_) => None, + } +} + +pub fn parse_global_args( + config: &WalletConfig, + args: &ArgMatches, +) -> Result { + let account = parse_required(args, "account")?; + let mut show_spent = false; + if args.is_present("show_spent") { + show_spent = true; + } + let api_secret = get_first_line(config.api_secret_path.clone()); + let node_api_secret = get_first_line(config.node_api_secret_path.clone()); + let password = match args.value_of("pass") { + None => None, + Some(p) => Some(ZeroingString::from(p)), + }; + + let tls_conf = match config.tls_certificate_file.clone() { + None => None, + Some(file) => { + let key = match config.tls_certificate_key.clone() { + Some(k) => k, + None => { + let msg = format!("Private key for certificate is not set"); + return Err(ParseError::ArgumentError(msg)); + } + }; + Some(TLSConfig::new(file, key)) + } + }; + + Ok(command::GlobalArgs { + account: account.to_owned(), + show_spent: show_spent, + api_secret: api_secret, + node_api_secret: node_api_secret, + password: password, + tls_conf: tls_conf, + }) +} + +pub fn parse_init_args( + wallet: Arc>>>, + config: &WalletConfig, + g_args: &command::GlobalArgs, + args: &ArgMatches, + _test_mode: bool, +) -> Result +where + DefaultWalletImpl<'static, C>: WalletInst<'static, L, C, K>, + L: WalletLCProvider<'static, C, K>, + C: NodeClient + 'static, + K: keychain::Keychain + 'static, +{ + let list_length = match args.is_present("short_wordlist") { + false => 32, + true => 16, + }; + let recovery_phrase = match args.is_present("recover") { + true => Some(prompt_recovery_phrase(wallet)?), + false => None, + }; + + if recovery_phrase.is_some() { + println!("Please provide a new password for the recovered wallet"); + } else { + println!("Please enter a password for your new wallet"); + } + + let password = match g_args.password.clone() { + Some(p) => p, + None => prompt_password_confirm(), + }; + + Ok(command::InitArgs { + list_length: list_length, + password: password, + config: config.clone(), + recovery_phrase: recovery_phrase, + restore: false, + }) +} + +pub fn parse_recover_args( + g_args: &command::GlobalArgs, +) -> Result +where +{ + let passphrase = prompt_password(&g_args.password); + Ok(command::RecoverArgs { + passphrase: passphrase, + }) +} + +pub fn parse_listen_args( + config: &mut WalletConfig, + tor_config: &mut TorConfig, + args: &ArgMatches, +) -> Result { + if let Some(port) = args.value_of("port") { + config.api_listen_port = port.parse().unwrap(); + } + if let Some(bridge) = args.value_of("bridge") { + tor_config.bridge.bridge_line = Some(bridge.into()); + } + if args.is_present("no_tor") { + tor_config.use_tor_listener = false; + } + Ok(command::ListenArgs {}) +} + +pub fn parse_owner_api_args( + config: &mut WalletConfig, + args: &ArgMatches, +) -> Result<(), ParseError> { + if let Some(port) = args.value_of("port") { + config.owner_api_listen_port = Some(port.parse().unwrap()); + } + if args.is_present("run_foreign") { + config.owner_api_include_foreign = Some(true); + } + Ok(()) +} + +pub fn parse_scan_rewind_hash_args( + args: &ArgMatches, +) -> Result { + let rewind_hash = parse_required(args, "rewind_hash")?; + let start_height = parse_u64_or_none(args.value_of("start_height")); + let backwards_from_tip = parse_u64_or_none(args.value_of("backwards_from_tip")); + if backwards_from_tip.is_some() && start_height.is_some() { + let msg = format!("backwards_from tip and start_height cannot both be present"); + return Err(ParseError::ArgumentError(msg)); + } + Ok(command::ViewWalletScanArgs { + rewind_hash: rewind_hash.into(), + start_height, + backwards_from_tip, + }) +} + +pub fn parse_account_args(account_args: &ArgMatches) -> Result { + let create = match account_args.value_of("create") { + None => None, + Some(s) => Some(s.to_owned()), + }; + Ok(command::AccountArgs { create: create }) +} + +pub fn parse_send_args(args: &ArgMatches) -> Result { + // amount + let amount = parse_required(args, "amount")?; + let (amount, spend_max) = if amount.eq_ignore_ascii_case("max") { + (Ok(0), true) + } else { + (core::core::amount_from_hr_string(amount), false) + }; + let amount = match amount { + Ok(a) => a, + Err(e) => { + let msg = format!( + "Could not parse amount as a number with optional decimal point. e={}", + e + ); + return Err(ParseError::ArgumentError(msg)); + } + }; + let amount_includes_fee = args.is_present("amount_includes_fee") || spend_max; + + // minimum_confirmations + let min_c = parse_required(args, "minimum_confirmations")?; + let min_c = parse_u64(min_c, "minimum_confirmations")?; + + // selection_strategy + let selection_strategy = parse_required(args, "selection_strategy")?; + + // estimate_selection_strategies + let estimate_selection_strategies = args.is_present("estimate_selection_strategies"); + + let late_lock = args.is_present("late_lock"); + + // dest + let dest = match args.value_of("dest") { + Some(d) => d, + None => "default", + }; + + // change_outputs + let change_outputs = parse_required(args, "change_outputs")?; + let change_outputs = parse_u64(change_outputs, "change_outputs")? as usize; + + // fluff + let fluff = args.is_present("fluff"); + + // ttl_blocks + let ttl_blocks = parse_u64_or_none(args.value_of("ttl_blocks")); + + // max_outputs + let max_outputs = 500; + + // target slate version to create/send + let target_slate_version = { + match args.is_present("slate_version") { + true => { + let v = parse_required(args, "slate_version")?; + Some(parse_u64(v, "slate_version")? as u16) + } + false => None, + } + }; + + let payment_proof_address = { + match args.is_present("no_payment_proof") { + false => match SlatepackAddress::try_from(dest) { + Ok(a) => Some(a), + Err(_) => { + if !estimate_selection_strategies { + println!("No recipient Slatepack address or provided address invalid. No payment proof will be requested."); + } + None + } + }, + true => None, + } + }; + + let outfile = parse_optional(args, "outfile")?; + + let bridge = match args.value_of("bridge") { + Some(b) => Some(b.to_string()), + None => None, + }; + + let slatepack_qr = args.is_present("slatepack_qr"); + + Ok(command::SendArgs { + amount: amount, + amount_includes_fee: amount_includes_fee, + use_max_amount: spend_max, + minimum_confirmations: min_c, + selection_strategy: selection_strategy.to_owned(), + estimate_selection_strategies, + late_lock, + dest: dest.to_owned(), + change_outputs: change_outputs, + fluff: fluff, + max_outputs: max_outputs, + payment_proof_address, + ttl_blocks, + target_slate_version: target_slate_version, + outfile, + skip_tor: args.is_present("manual"), + bridge: bridge, + slatepack_qr: slatepack_qr, + }) +} + +pub fn parse_receive_args(args: &ArgMatches) -> Result { + // input file + let input_file = match args.is_present("input") { + true => { + let file = args.value_of("input").unwrap().to_owned(); + // validate input + if !Path::new(&file).is_file() { + let msg = format!("File {} not found.", &file); + return Err(ParseError::ArgumentError(msg)); + } + Some(file) + } + false => None, + }; + + let mut input_slatepack_message = None; + if input_file.is_none() { + input_slatepack_message = Some(prompt_slatepack()?); + } + + let outfile = parse_optional(args, "outfile")?; + + let bridge = parse_optional(args, "bridge")?; + + let slatepack_qr = args.is_present("slatepack_qr"); + + Ok(command::ReceiveArgs { + input_file, + input_slatepack_message, + skip_tor: args.is_present("manual"), + outfile, + bridge, + slatepack_qr: slatepack_qr, + }) +} + +pub fn parse_unpack_args(args: &ArgMatches) -> Result { + // input file + let input_file = match args.is_present("input") { + true => { + let file = args.value_of("input").unwrap().to_owned(); + // validate input + if !Path::new(&file).is_file() { + let msg = format!("File {} not found.", &file); + return Err(ParseError::ArgumentError(msg)); + } + Some(file) + } + false => None, + }; + + let mut input_slatepack_message = None; + if input_file.is_none() { + input_slatepack_message = Some(prompt_slatepack()?); + } + + let outfile = parse_optional(args, "outfile")?; + + let bridge = parse_optional(args, "bridge")?; + + let slatepack_qr = args.is_present("slatepack_qr"); + + Ok(command::ReceiveArgs { + input_file, + input_slatepack_message, + skip_tor: args.is_present("manual"), + outfile, + bridge, + slatepack_qr: slatepack_qr, + }) +} + +pub fn parse_finalize_args(args: &ArgMatches) -> Result { + let fluff = args.is_present("fluff"); + let nopost = args.is_present("nopost"); + + let input_file = match args.is_present("input") { + true => { + let file = args.value_of("input").unwrap().to_owned(); + // validate input + if !Path::new(&file).is_file() { + let msg = format!("File {} not found.", &file); + return Err(ParseError::ArgumentError(msg)); + } + Some(file) + } + false => None, + }; + + let mut input_slatepack_message = None; + if input_file.is_none() { + input_slatepack_message = Some(prompt_slatepack()?); + } + + let outfile = parse_optional(args, "outfile")?; + + let slatepack_qr = args.is_present("slatepack_qr"); + + Ok(command::FinalizeArgs { + input_file, + input_slatepack_message, + fluff: fluff, + nopost: nopost, + outfile, + slatepack_qr: slatepack_qr, + }) +} + +pub fn parse_issue_invoice_args( + args: &ArgMatches, +) -> Result { + let amount = parse_required(args, "amount")?; + let amount = core::core::amount_from_hr_string(amount); + let amount = match amount { + Ok(a) => a, + Err(e) => { + let msg = format!( + "Could not parse amount as a number with optional decimal point. e={}", + e + ); + return Err(ParseError::ArgumentError(msg)); + } + }; + + // target slate version to create + let target_slate_version = { + match args.is_present("slate_version") { + true => { + let v = parse_required(args, "slate_version")?; + Some(parse_u64(v, "slate_version")? as u16) + } + false => None, + } + }; + + // dest, for encryption + let dest = match args.value_of("dest") { + Some(d) => d, + None => "default", + }; + + let outfile = parse_optional(args, "outfile")?; + + let slatepack_qr = args.is_present("slatepack_qr"); + + Ok(command::IssueInvoiceArgs { + dest: dest.into(), + issue_args: IssueInvoiceTxArgs { + dest_acct_name: None, + amount, + target_slate_version, + }, + outfile, + slatepack_qr: slatepack_qr, + }) +} + +fn get_slate( + owner_api: &mut Owner, + keychain_mask: Option<&SecretKey>, + args: &ArgMatches, +) -> Result<(Slate, Option), Error> +where + L: WalletLCProvider<'static, C, K>, + C: NodeClient + 'static, + K: keychain::Keychain + 'static, +{ + let input_file = match args.is_present("input") { + true => { + let file = args.value_of("input").unwrap().to_owned(); + // validate input + if !Path::new(&file).is_file() { + let msg = format!("File {} not found.", &file); + return Err(Error::GenericError(msg)); + } + Some(file) + } + false => None, + }; + + let mut input_slatepack_message = None; + if input_file.is_none() { + input_slatepack_message = Some(prompt_slatepack().map_err(|e| { + let msg = format!("{}", e); + Error::GenericError(msg) + })?) + } + + command::parse_slatepack( + owner_api, + keychain_mask, + input_file, + input_slatepack_message, + ) +} + +pub fn parse_process_invoice_args( + args: &ArgMatches, + prompt: bool, + slate: Slate, + ret_address: Option, +) -> Result { + // minimum_confirmations + let min_c = parse_required(args, "minimum_confirmations")?; + let min_c = parse_u64(min_c, "minimum_confirmations")?; + + // selection_strategy + let selection_strategy = parse_required(args, "selection_strategy")?; + + // estimate_selection_strategies + let estimate_selection_strategies = args.is_present("estimate_selection_strategies"); + + // ttl_blocks + let ttl_blocks = parse_u64_or_none(args.value_of("ttl_blocks")); + + // max_outputs + let max_outputs = 500; + + if prompt { + let dest = match ret_address.clone() { + Some(a) => String::try_from(&a).unwrap(), + None => String::from(""), + }; + // Now we need to prompt the user whether they want to do this, + prompt_pay_invoice(&slate, &dest)?; + } + + let outfile = parse_optional(args, "outfile")?; + + let bridge = parse_optional(args, "bridge")?; + + let slatepack_qr = args.is_present("slatepack_qr"); + + Ok(command::ProcessInvoiceArgs { + minimum_confirmations: min_c, + selection_strategy: selection_strategy.to_owned(), + estimate_selection_strategies, + ret_address, + slate, + max_outputs, + ttl_blocks, + skip_tor: args.is_present("manual"), + outfile, + bridge, + slatepack_qr: slatepack_qr, + }) +} + +pub fn parse_info_args(args: &ArgMatches) -> Result { + // minimum_confirmations + let mc = parse_required(args, "minimum_confirmations")?; + let mc = parse_u64(mc, "minimum_confirmations")?; + Ok(command::InfoArgs { + minimum_confirmations: mc, + }) +} + +pub fn parse_check_args(args: &ArgMatches) -> Result { + let delete_unconfirmed = args.is_present("delete_unconfirmed"); + let start_height = parse_u64_or_none(args.value_of("start_height")); + let backwards_from_tip = parse_u64_or_none(args.value_of("backwards_from_tip")); + if backwards_from_tip.is_some() && start_height.is_some() { + let msg = format!("backwards_from tip and start_height cannot both be present"); + return Err(ParseError::ArgumentError(msg)); + } + Ok(command::CheckArgs { + start_height, + backwards_from_tip, + delete_unconfirmed, + }) +} + +pub fn parse_txs_args(args: &ArgMatches) -> Result { + let tx_id = match args.value_of("id") { + None => None, + Some(tx) => Some(parse_u64(tx, "id")? as u32), + }; + let tx_slate_id = match args.value_of("txid") { + None => None, + Some(tx) => match tx.parse() { + Ok(t) => Some(t), + Err(e) => { + let msg = format!("Could not parse txid parameter. e={}", e); + return Err(ParseError::ArgumentError(msg)); + } + }, + }; + if tx_id.is_some() && tx_slate_id.is_some() { + let msg = format!("At most one of 'id' (-i) or 'txid' (-t) may be provided."); + return Err(ParseError::ArgumentError(msg)); + } + let count = match args.value_of("count") { + None => None, + Some(c) => Some(parse_u64(c, "count")? as u32), + }; + Ok(command::TxsArgs { + id: tx_id, + tx_slate_id: tx_slate_id, + count: count, + }) +} + +pub fn parse_post_args(args: &ArgMatches) -> Result { + let fluff = args.is_present("fluff"); + + // input file + let input_file = match args.is_present("input") { + true => { + let file = args.value_of("input").unwrap().to_owned(); + // validate input + if !Path::new(&file).is_file() { + let msg = format!("File {} not found.", &file); + return Err(ParseError::ArgumentError(msg)); + } + Some(file) + } + false => None, + }; + + let mut input_slatepack_message = None; + if input_file.is_none() { + input_slatepack_message = Some(prompt_slatepack()?); + } + + Ok(command::PostArgs { + input_file, + input_slatepack_message, + fluff, + }) +} + +pub fn parse_repost_args(args: &ArgMatches) -> Result { + let tx_id = match args.value_of("id") { + None => None, + Some(tx) => Some(parse_u64(tx, "id")? as u32), + }; + + let fluff = args.is_present("fluff"); + let dump_file = match args.value_of("dumpfile") { + None => None, + Some(d) => Some(d.to_owned()), + }; + + Ok(command::RepostArgs { + id: tx_id.unwrap(), + dump_file: dump_file, + fluff: fluff, + }) +} + +pub fn parse_cancel_args(args: &ArgMatches) -> Result { + let mut tx_id_string = ""; + let tx_id = match args.value_of("id") { + None => None, + Some(tx) => Some(parse_u64(tx, "id")? as u32), + }; + let tx_slate_id = match args.value_of("txid") { + None => None, + Some(tx) => match tx.parse() { + Ok(t) => { + tx_id_string = tx; + Some(t) + } + Err(e) => { + let msg = format!("Could not parse txid parameter. e={}", e); + return Err(ParseError::ArgumentError(msg)); + } + }, + }; + if (tx_id.is_none() && tx_slate_id.is_none()) || (tx_id.is_some() && tx_slate_id.is_some()) { + let msg = format!("'id' (-i) or 'txid' (-t) argument is required."); + return Err(ParseError::ArgumentError(msg)); + } + Ok(command::CancelArgs { + tx_id: tx_id, + tx_slate_id: tx_slate_id, + tx_id_string: tx_id_string.to_owned(), + }) +} + +pub fn parse_export_proof_args(args: &ArgMatches) -> Result { + let output_file = parse_required(args, "output")?; + let tx_id = match args.value_of("id") { + None => None, + Some(tx) => Some(parse_u64(tx, "id")? as u32), + }; + let tx_slate_id = match args.value_of("txid") { + None => None, + Some(tx) => match tx.parse() { + Ok(t) => Some(t), + Err(e) => { + let msg = format!("Could not parse txid parameter. e={}", e); + return Err(ParseError::ArgumentError(msg)); + } + }, + }; + if tx_id.is_some() && tx_slate_id.is_some() { + let msg = format!("At most one of 'id' (-i) or 'txid' (-t) may be provided."); + return Err(ParseError::ArgumentError(msg)); + } + if tx_id.is_none() && tx_slate_id.is_none() { + let msg = format!("Either 'id' (-i) or 'txid' (-t) must be provided."); + return Err(ParseError::ArgumentError(msg)); + } + Ok(command::ProofExportArgs { + output_file: output_file.to_owned(), + id: tx_id, + tx_slate_id: tx_slate_id, + }) +} + +pub fn parse_verify_proof_args(args: &ArgMatches) -> Result { + let input_file = parse_required(args, "input")?; + Ok(command::ProofVerifyArgs { + input_file: input_file.to_owned(), + }) +} + +pub fn wallet_command( + wallet_args: &ArgMatches, + mut wallet_config: WalletConfig, + tor_config: Option, + mut node_client: C, + test_mode: bool, + wallet_inst_cb: F, +) -> Result +where + C: NodeClient + 'static + Clone, + F: FnOnce( + Arc< + Mutex< + Box< + dyn WalletInst< + 'static, + DefaultLCProvider<'static, C, keychain::ExtKeychain>, + C, + keychain::ExtKeychain, + >, + >, + >, + >, + ), +{ + if let Some(dir) = wallet_args.value_of("top_level_dir") { + wallet_config.data_file_dir = dir.to_string().clone(); + } + + if let Some(sa) = wallet_args.value_of("api_server_address") { + wallet_config.check_node_api_http_addr = sa.to_string().clone(); + } + + let global_wallet_args = arg_parse!(parse_global_args(&wallet_config, &wallet_args)); + + node_client.set_node_url(&wallet_config.check_node_api_http_addr); + node_client.set_node_api_secret(global_wallet_args.node_api_secret.clone()); + + // legacy hack to avoid the need for changes in existing grin-wallet.toml files + // remove `wallet_data` from end of path + // new lifecycle provider assumes grin_wallet.toml is in root of data directory + let mut top_level_wallet_dir = PathBuf::from(wallet_config.clone().data_file_dir); + if top_level_wallet_dir.ends_with(GRIN_WALLET_DIR) { + top_level_wallet_dir.pop(); + wallet_config.data_file_dir = top_level_wallet_dir.to_str().unwrap().into(); + } + + // for backwards compatibility: If tor config doesn't exist in the file, assume + // the top level directory for data + let tor_config = tor_config.unwrap_or_else(|| TorConfig { + send_config_dir: wallet_config.data_file_dir.clone(), + ..Default::default() + }); + + // Instantiate wallet (doesn't open the wallet) + let wallet = + inst_wallet::, C, keychain::ExtKeychain>( + wallet_config.clone(), + node_client, + ) + .unwrap_or_else(|e| { + println!("{}", e); + std::process::exit(1); + }); + + { + let mut wallet_lock = wallet.lock(); + let lc = wallet_lock.lc_provider().unwrap(); + let _ = lc.set_top_level_directory(&wallet_config.data_file_dir); + } + + // provide wallet instance back to the caller (handy for testing with + // local wallet proxy, etc) + wallet_inst_cb(wallet.clone()); + + // don't open wallet for certain lifecycle commands + let mut open_wallet = true; + match wallet_args.subcommand() { + ("init", Some(_)) => open_wallet = false, + ("recover", _) => open_wallet = false, + ("cli", _) => open_wallet = false, + ("owner_api", _) => { + // If wallet exists and password is present then open it. Otherwise, that's fine too. + let mut wallet_lock = wallet.lock(); + let lc = wallet_lock.lc_provider().unwrap(); + open_wallet = wallet_args.is_present("pass") && lc.wallet_exists(None)?; + } + _ => {} + } + + let keychain_mask = match open_wallet { + true => { + let mut wallet_lock = wallet.lock(); + let lc = wallet_lock.lc_provider().unwrap(); + let mask = lc.open_wallet( + None, + prompt_password(&global_wallet_args.password), + false, + false, + )?; + if let Some(account) = wallet_args.value_of("account") { + let wallet_inst = lc.wallet_inst()?; + wallet_inst.set_parent_key_id_by_name(account)?; + } + mask + } + false => None, + }; + + let res = match wallet_args.subcommand() { + ("cli", Some(_)) => command_loop( + wallet, + keychain_mask, + &wallet_config, + &tor_config, + &global_wallet_args, + test_mode, + ), + _ => { + let mut owner_api = Owner::new(wallet, None); + parse_and_execute( + &mut owner_api, + keychain_mask, + &wallet_config, + &tor_config, + &global_wallet_args, + &wallet_args, + test_mode, + false, + ) + } + }; + + if let Err(e) = res { + Err(e) + } else { + Ok(wallet_args.subcommand().0.to_owned()) + } +} + +pub fn parse_and_execute( + owner_api: &mut Owner, + keychain_mask: Option, + wallet_config: &WalletConfig, + tor_config: &TorConfig, + global_wallet_args: &command::GlobalArgs, + wallet_args: &ArgMatches, + test_mode: bool, + cli_mode: bool, +) -> Result<(), Error> +where + DefaultWalletImpl<'static, C>: WalletInst<'static, L, C, K>, + L: WalletLCProvider<'static, C, K> + 'static, + C: NodeClient + 'static, + K: keychain::Keychain + 'static, +{ + let km = (&keychain_mask).as_ref(); + + if test_mode { + owner_api.doctest_mode = true; + owner_api.doctest_retain_tld = true; + } + + match wallet_args.subcommand() { + ("init", Some(args)) => { + let a = arg_parse!(parse_init_args( + owner_api.wallet_inst.clone(), + wallet_config, + global_wallet_args, + &args, + test_mode, + )); + command::init(owner_api, &global_wallet_args, a, test_mode) + } + ("recover", Some(_)) => { + let a = arg_parse!(parse_recover_args(&global_wallet_args,)); + command::recover(owner_api, a) + } + ("listen", Some(args)) => { + let mut c = wallet_config.clone(); + let mut t = tor_config.clone(); + let a = arg_parse!(parse_listen_args(&mut c, &mut t, &args)); + command::listen( + owner_api, + Arc::new(Mutex::new(keychain_mask)), + &c, + &t, + &a, + &global_wallet_args.clone(), + cli_mode, + test_mode, + ) + } + ("owner_api", Some(args)) => { + let mut c = wallet_config.clone(); + let mut g = global_wallet_args.clone(); + g.tls_conf = None; + arg_parse!(parse_owner_api_args(&mut c, &args)); + command::owner_api(owner_api, keychain_mask, &c, &tor_config, &g, test_mode) + } + ("web", Some(_)) => command::owner_api( + owner_api, + keychain_mask, + wallet_config, + tor_config, + global_wallet_args, + test_mode, + ), + ("rewind_hash", Some(_)) => command::rewind_hash(owner_api, km), + ("scan_rewind_hash", Some(args)) => { + let a = arg_parse!(parse_scan_rewind_hash_args(&args)); + command::scan_rewind_hash( + owner_api, + a, + wallet_config.dark_background_color_scheme.unwrap_or(true), + ) + } + ("account", Some(args)) => { + let a = arg_parse!(parse_account_args(&args)); + command::account(owner_api, km, a) + } + ("send", Some(args)) => { + let a = arg_parse!(parse_send_args(&args)); + command::send( + owner_api, + km, + Some(tor_config.clone()), + a, + wallet_config.dark_background_color_scheme.unwrap_or(true), + test_mode, + ) + } + ("receive", Some(args)) => { + let a = arg_parse!(parse_receive_args(&args)); + command::receive( + owner_api, + km, + &global_wallet_args, + a, + Some(tor_config.clone()), + test_mode, + ) + } + ("unpack", Some(args)) => { + let a = arg_parse!(parse_unpack_args(&args)); + command::unpack(owner_api, km, a) + } + ("finalize", Some(args)) => { + let a = arg_parse!(parse_finalize_args(&args)); + command::finalize(owner_api, km, a) + } + ("invoice", Some(args)) => { + let a = arg_parse!(parse_issue_invoice_args(&args)); + command::issue_invoice_tx(owner_api, km, a) + } + ("pay", Some(args)) => { + // get slate first + let (slate, address) = get_slate(owner_api, km, args)?; + + let a = arg_parse!(parse_process_invoice_args( + &args, !test_mode, slate, address + )); + command::process_invoice( + owner_api, + km, + Some(tor_config.clone()), + a, + wallet_config.dark_background_color_scheme.unwrap_or(true), + test_mode, + ) + } + ("info", Some(args)) => { + let a = arg_parse!(parse_info_args(&args)); + command::info( + owner_api, + km, + global_wallet_args, + a, + wallet_config.dark_background_color_scheme.unwrap_or(true), + ) + } + ("outputs", Some(_)) => command::outputs( + owner_api, + km, + &global_wallet_args, + wallet_config.dark_background_color_scheme.unwrap_or(true), + ), + ("txs", Some(args)) => { + let a = arg_parse!(parse_txs_args(&args)); + command::txs( + owner_api, + km, + &global_wallet_args, + a, + wallet_config.dark_background_color_scheme.unwrap_or(true), + ) + } + ("post", Some(args)) => { + let a = arg_parse!(parse_post_args(&args)); + command::post(owner_api, km, a) + } + ("repost", Some(args)) => { + let a = arg_parse!(parse_repost_args(&args)); + command::repost(owner_api, km, a) + } + ("cancel", Some(args)) => { + let a = arg_parse!(parse_cancel_args(&args)); + command::cancel(owner_api, km, a) + } + ("export_proof", Some(args)) => { + let a = arg_parse!(parse_export_proof_args(&args)); + command::proof_export(owner_api, km, a) + } + ("verify_proof", Some(args)) => { + let a = arg_parse!(parse_verify_proof_args(&args)); + command::proof_verify(owner_api, km, a) + } + ("address", Some(_)) => command::address(owner_api, &global_wallet_args, km), + ("scan", Some(args)) => { + let a = arg_parse!(parse_check_args(&args)); + command::scan(owner_api, km, a) + } + ("open", Some(_)) => { + // for CLI mode only, should be handled externally + Ok(()) + } + ("close", Some(_)) => { + // for CLI mode only, should be handled externally + Ok(()) + } + _ => { + let msg = format!("Unknown wallet command, use 'grin-wallet help' for details"); + return Err(Error::ArgumentError(msg)); + } + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..00aea64 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,24 @@ +// Copyright 2021 The Grin Developers +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#[macro_use] +extern crate lazy_static; +#[macro_use] +extern crate clap; + +use grin_api as api; +use grin_util as util; +use grin_wallet_config as config; + +mod cli; +pub mod cmd; diff --git a/tests/cmd_line_basic.rs b/tests/cmd_line_basic.rs new file mode 100644 index 0000000..bce4a8d --- /dev/null +++ b/tests/cmd_line_basic.rs @@ -0,0 +1,726 @@ +// Copyright 2021 The Grin Developers +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Test wallet command line works as expected +#[macro_use] +extern crate clap; + +#[macro_use] +extern crate log; + +extern crate grin_wallet; + +use grin_wallet_impls::test_framework::{self, LocalWalletClient, WalletProxy}; + +use clap::App; +use std::thread; +use std::time::Duration; + +use grin_keychain::ExtKeychain; +use grin_wallet_impls::DefaultLCProvider; + +mod common; +use common::{clean_output_dir, execute_command, initial_setup_wallet, instantiate_wallet, setup}; + +/// command line tests +fn command_line_test_impl(test_dir: &str) -> Result<(), grin_wallet_controller::Error> { + setup(test_dir); + // Create a new proxy to simulate server and wallet responses + let mut wallet_proxy: WalletProxy< + DefaultLCProvider, + LocalWalletClient, + ExtKeychain, + > = WalletProxy::new(test_dir); + let chain = wallet_proxy.chain.clone(); + + // load app yaml. If it don't exist, just say so and exit + let yml = load_yaml!("../src/bin/grin-wallet.yml"); + let app = App::from_yaml(yml); + + // wallet init + let arg_vec = vec!["grin-wallet", "-p", "password1", "init", "-h"]; + // should create new wallet file + let client1 = LocalWalletClient::new("wallet1", wallet_proxy.tx.clone()); + execute_command(&app, test_dir, "wallet1", &client1, arg_vec.clone())?; + + // trying to init twice - should fail + assert!(execute_command(&app, test_dir, "wallet1", &client1, arg_vec.clone()).is_err()); + let client1 = LocalWalletClient::new("wallet1", wallet_proxy.tx.clone()); + + // add wallet to proxy + //let wallet1 = test_framework::create_wallet(&format!("{}/wallet1", test_dir), client1.clone()); + let config1 = initial_setup_wallet(test_dir, "wallet1"); + let wallet_config1 = config1.clone().members.unwrap().wallet; + let (wallet1, mask1_i) = instantiate_wallet( + wallet_config1.clone(), + client1.clone(), + "password1", + "default", + )?; + wallet_proxy.add_wallet( + "wallet1", + client1.get_send_instance(), + wallet1.clone(), + mask1_i.clone(), + ); + + // Create wallet 2 + let arg_vec = vec!["grin-wallet", "-p", "password2", "init", "-h"]; + let client2 = LocalWalletClient::new("wallet2", wallet_proxy.tx.clone()); + execute_command(&app, test_dir, "wallet2", &client2, arg_vec.clone())?; + + let config2 = initial_setup_wallet(test_dir, "wallet2"); + let wallet_config2 = config2.clone().members.unwrap().wallet; + let (wallet2, mask2_i) = instantiate_wallet( + wallet_config2.clone(), + client2.clone(), + "password2", + "default", + )?; + wallet_proxy.add_wallet( + "wallet2", + client2.get_send_instance(), + wallet2.clone(), + mask2_i.clone(), + ); + + // Set the wallet proxy listener running + thread::spawn(move || { + if let Err(e) = wallet_proxy.run() { + error!("Wallet Proxy error: {}", e); + } + }); + + // Create some accounts in wallet 1 + let arg_vec = vec!["grin-wallet", "-p", "password1", "account", "-c", "mining"]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + let arg_vec = vec![ + "grin-wallet", + "-p", + "password1", + "account", + "-c", + "account_1", + ]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + // Create some accounts in wallet 2 + let arg_vec = vec![ + "grin-wallet", + "-p", + "password2", + "account", + "-c", + "account_1", + ]; + execute_command(&app, test_dir, "wallet2", &client2, arg_vec.clone())?; + // already exists + assert!(execute_command(&app, test_dir, "wallet2", &client2, arg_vec).is_err()); + + let arg_vec = vec![ + "grin-wallet", + "-p", + "password2", + "account", + "-c", + "account_2", + ]; + execute_command(&app, test_dir, "wallet2", &client2, arg_vec)?; + + // let's see those accounts + let arg_vec = vec!["grin-wallet", "-p", "password1", "account"]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + // let's see those accounts + let arg_vec = vec!["grin-wallet", "-p", "password2", "account"]; + execute_command(&app, test_dir, "wallet2", &client2, arg_vec)?; + + // Mine a bit into wallet 1 so we have something to send + // (TODO: Be able to stop listeners so we can test this better) + let wallet_config1 = config1.clone().members.unwrap().wallet; + let (wallet1, mask1_i) = + instantiate_wallet(wallet_config1, client1.clone(), "password1", "default")?; + let mask1 = (&mask1_i).as_ref(); + grin_wallet_controller::controller::owner_single_use( + Some(wallet1.clone()), + mask1, + None, + |api, m| { + api.set_active_account(m, "mining")?; + Ok(()) + }, + )?; + + let mut bh = 10u64; + let _ = + test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, bh as usize, false); + + // Update info and check + let arg_vec = vec!["grin-wallet", "-p", "password1", "-a", "mining", "info"]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + // try a file exchange + let file_name = format!( + "{}/wallet1/slatepack/0436430c-2b02-624c-2032-570501212b00.S1.slatepack", + test_dir + ); + + let arg_vec = vec![ + "grin-wallet", + "-p", + "password1", + "-a", + "mining", + "send", + "10", + ]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + let arg_vec = vec!["grin-wallet", "-a", "mining", "-p", "password1", "txs"]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + let arg_vec = vec![ + "grin-wallet", + "-p", + "password2", + "-a", + "account_1", + "receive", + "-i", + &file_name, + ]; + execute_command(&app, test_dir, "wallet2", &client2, arg_vec.clone())?; + + // shouldn't be allowed to receive twice + assert!(execute_command(&app, test_dir, "wallet2", &client2, arg_vec).is_err()); + + let file_name = format!( + "{}/wallet2/slatepack/0436430c-2b02-624c-2032-570501212b00.S2.slatepack", + test_dir + ); + + let arg_vec = vec![ + "grin-wallet", + "-a", + "mining", + "-p", + "password1", + "finalize", + "-i", + &file_name, + ]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + bh += 1; + + let wallet_config1 = config1.clone().members.unwrap().wallet; + let (wallet1, mask1_i) = instantiate_wallet( + wallet_config1.clone(), + client1.clone(), + "password1", + "default", + )?; + let mask1 = (&mask1_i).as_ref(); + + // Check our transaction log, should have 10 entries + grin_wallet_controller::controller::owner_single_use( + Some(wallet1.clone()), + mask1, + None, + |api, m| { + api.set_active_account(m, "mining")?; + let (refreshed, txs) = api.retrieve_txs(m, true, None, None, None)?; + assert!(refreshed); + assert_eq!(txs.len(), bh as usize); + for t in txs { + assert!(t.kernel_excess.is_some()); + } + Ok(()) + }, + )?; + + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 10, false); + bh += 10; + + // update info for each + let arg_vec = vec!["grin-wallet", "-p", "password1", "-a", "mining", "info"]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + let arg_vec = vec!["grin-wallet", "-p", "password2", "-a", "account_1", "info"]; + execute_command(&app, test_dir, "wallet2", &client1, arg_vec)?; + + // check results in wallet 2 + let wallet_config2 = config2.clone().members.unwrap().wallet; + let (wallet2, mask2_i) = instantiate_wallet( + wallet_config2.clone(), + client2.clone(), + "password2", + "default", + )?; + let mask2 = (&mask2_i).as_ref(); + + grin_wallet_controller::controller::owner_single_use( + Some(wallet2.clone()), + mask2, + None, + |api, m| { + api.set_active_account(m, "account_1")?; + let (_, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + assert_eq!(wallet1_info.last_confirmed_height, bh); + assert_eq!(wallet1_info.amount_currently_spendable, 10_000_000_000); + Ok(()) + }, + )?; + + // Send to wallet 2 with --amount_includes_fee + let mut old_balance = 0; + grin_wallet_controller::controller::owner_single_use( + Some(wallet1.clone()), + mask1, + None, + |api, m| { + api.set_active_account(m, "mining")?; + let (_, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + old_balance = wallet1_info.amount_currently_spendable; + Ok(()) + }, + )?; + let arg_vec = vec![ + "grin-wallet", + "-p", + "password1", + "-a", + "mining", + "send", + "--amount_includes_fee", + "10", + ]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + let file_name = format!( + "{}/wallet1/slatepack/0436430c-2b02-624c-2032-570501212b01.S1.slatepack", + test_dir + ); + let arg_vec = vec![ + "grin-wallet", + "-p", + "password2", + "-a", + "account_1", + "receive", + "-i", + &file_name, + ]; + execute_command(&app, test_dir, "wallet2", &client2, arg_vec.clone())?; + let file_name = format!( + "{}/wallet2/slatepack/0436430c-2b02-624c-2032-570501212b01.S2.slatepack", + test_dir + ); + let arg_vec = vec![ + "grin-wallet", + "-a", + "mining", + "-p", + "password1", + "finalize", + "-i", + &file_name, + ]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + bh += 1; + + // Mine some blocks to confirm the transaction + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 10, false); + bh += 10; + + // Check the new balance of wallet 1 reduced by EXACTLY the tx amount (instead of amount + fee) + // This confirms that the TX amount was correctly computed to allow for the fee + grin_wallet_controller::controller::owner_single_use( + Some(wallet1.clone()), + mask1, + None, + |api, m| { + api.set_active_account(m, "mining")?; + let (_, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + // make sure the new balance is exactly equal to the old balance - the tx amount + the amount mined since then + let amt_mined = 10 * 60_000_000_000; + assert_eq!( + wallet1_info.amount_currently_spendable + 10_000_000_000, + old_balance + amt_mined + ); + Ok(()) + }, + )?; + + // Send encrypted from wallet 1 to wallet 2 + // output wallet 2's address for test creation purposes, + let arg_vec = vec![ + "grin-wallet", + "-p", + "password2", + "-a", + "account_1", + "address", + ]; + execute_command(&app, test_dir, "wallet2", &client2, arg_vec)?; + + // Send encrypted to wallet 2 + let arg_vec = vec![ + "grin-wallet", + "-p", + "password1", + "-a", + "mining", + "send", + "-d", + "tgrin1ak8aaxpjg6ct5uje4lgzvjp65l0nrmgxndp5xjy74sumzp7wasysje3kmf", + "10", + ]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + let file_name = format!( + "{}/wallet1/slatepack/0436430c-2b02-624c-2032-570501212b02.S1.slatepack", + test_dir + ); + let arg_vec = vec![ + "grin-wallet", + "-p", + "password2", + "-a", + "account_1", + "receive", + "-i", + &file_name, + ]; + execute_command(&app, test_dir, "wallet2", &client2, arg_vec.clone())?; + + let file_name = format!( + "{}/wallet2/slatepack/0436430c-2b02-624c-2032-570501212b02.S2.slatepack", + test_dir + ); + + let arg_vec = vec![ + "grin-wallet", + "-a", + "mining", + "-p", + "password1", + "finalize", + "-i", + &file_name, + ]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + bh += 1; + + // Check our transaction log, should have bh entries + let wallet_config1 = config1.clone().members.unwrap().wallet; + let (wallet1, mask1_i) = instantiate_wallet( + wallet_config1.clone(), + client1.clone(), + "password1", + "default", + )?; + let mask1 = (&mask1_i).as_ref(); + + grin_wallet_controller::controller::owner_single_use( + Some(wallet1.clone()), + mask1, + None, + |api, m| { + api.set_active_account(m, "mining")?; + let (refreshed, txs) = api.retrieve_txs(m, true, None, None, None)?; + assert!(refreshed); + assert_eq!(txs.len(), bh as usize); + Ok(()) + }, + )?; + + // Send to self + let arg_vec = vec![ + "grin-wallet", + "-p", + "password1", + "-a", + "mining", + "send", + "-o", + "3", + "-s", + "smallest", + "10", + ]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + let file_name = format!( + "{}/wallet1/slatepack/0436430c-2b02-624c-2032-570501212b03.S1.slatepack", + test_dir + ); + let arg_vec = vec![ + "grin-wallet", + "-p", + "password1", + "-a", + "mining", + "receive", + "-i", + &file_name, + ]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec.clone())?; + + let file_name = format!( + "{}/wallet1/slatepack/0436430c-2b02-624c-2032-570501212b03.S2.slatepack", + test_dir + ); + + let arg_vec = vec![ + "grin-wallet", + "-a", + "mining", + "-p", + "password1", + "finalize", + "-i", + &file_name, + ]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + bh += 1; + + // Check our transaction log, should have bh entries + 1 for self-seld + let wallet_config1 = config1.clone().members.unwrap().wallet; + let (wallet1, mask1_i) = instantiate_wallet( + wallet_config1.clone(), + client1.clone(), + "password1", + "default", + )?; + let mask1 = (&mask1_i).as_ref(); + + grin_wallet_controller::controller::owner_single_use( + Some(wallet1.clone()), + mask1, + None, + |api, m| { + api.set_active_account(m, "mining")?; + let (refreshed, txs) = api.retrieve_txs(m, true, None, None, None)?; + assert!(refreshed); + assert_eq!(txs.len(), bh as usize + 1); + Ok(()) + }, + )?; + + // Another file exchange, don't send, but unlock with repair command + let arg_vec = vec![ + "grin-wallet", + "-p", + "password1", + "-a", + "mining", + "send", + "10", + ]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + let arg_vec = vec!["grin-wallet", "-p", "password1", "scan", "-d"]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + // Another file exchange, cancel this time + let arg_vec = vec![ + "grin-wallet", + "-p", + "password1", + "-a", + "mining", + "send", + "10", + ]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + let arg_vec = vec!["grin-wallet", "-a", "mining", "-p", "password1", "txs"]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + let arg_vec = vec![ + "grin-wallet", + "-p", + "password1", + "-a", + "mining", + "cancel", + "-i", + "36", + ]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + // issue an invoice tx, wallet 2 + let arg_vec = vec!["grin-wallet", "-p", "password2", "invoice", "65"]; + execute_command(&app, test_dir, "wallet2", &client2, arg_vec)?; + let file_name = format!( + "{}/wallet2/slatepack/0436430c-2b02-624c-2032-570501212b06.I1.slatepack", + test_dir + ); + + // now pay the invoice tx, wallet 1 + let arg_vec = vec![ + "grin-wallet", + "-a", + "mining", + "-p", + "password1", + "pay", + "-i", + &file_name, + ]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + let file_name = format!( + "{}/wallet1/slatepack/0436430c-2b02-624c-2032-570501212b06.I2.slatepack", + test_dir + ); + + // and finalize, wallet 2 + let arg_vec = vec![ + "grin-wallet", + "-p", + "password2", + "finalize", + "-i", + &file_name, + ]; + execute_command(&app, test_dir, "wallet2", &client2, arg_vec)?; + + // bit more mining + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 5, false); + //bh += 5; + + // txs and outputs (mostly spit out for a visual in test logs) + let arg_vec = vec!["grin-wallet", "-p", "password1", "-a", "mining", "txs"]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + // message output (mostly spit out for a visual in test logs) + let arg_vec = vec![ + "grin-wallet", + "-p", + "password1", + "-a", + "mining", + "txs", + "-i", + "10", + ]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + // txs and outputs (mostly spit out for a visual in test logs) + let arg_vec = vec!["grin-wallet", "-p", "password1", "-a", "mining", "outputs"]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + let arg_vec = vec!["grin-wallet", "-p", "password2", "txs"]; + execute_command(&app, test_dir, "wallet2", &client2, arg_vec)?; + + let arg_vec = vec!["grin-wallet", "-p", "password2", "outputs"]; + execute_command(&app, test_dir, "wallet2", &client2, arg_vec)?; + + // get tx output via -tx parameter + let mut tx_id = "".to_string(); + grin_wallet_controller::controller::owner_single_use( + Some(wallet2.clone()), + mask2, + None, + |api, m| { + api.set_active_account(m, "default")?; + let (_, txs) = api.retrieve_txs(m, true, None, None, None)?; + let some_tx_id = txs[0].tx_slate_id.clone(); + assert!(some_tx_id.is_some()); + tx_id = some_tx_id.unwrap().to_hyphenated().to_string().clone(); + Ok(()) + }, + )?; + let arg_vec = vec!["grin-wallet", "-p", "password2", "txs", "-t", &tx_id[..]]; + execute_command(&app, test_dir, "wallet2", &client2, arg_vec)?; + + // bit of mining + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 10, false); + + // Test wallet sweep + let arg_vec = vec![ + "grin-wallet", + "-p", + "password1", + "-a", + "mining", + "send", + "max", + ]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + let file_name = format!( + "{}/wallet1/slatepack/0436430c-2b02-624c-2032-570501212b07.S1.slatepack", + test_dir + ); + let arg_vec = vec![ + "grin-wallet", + "-p", + "password2", + "-a", + "account_1", + "receive", + "-i", + &file_name, + ]; + execute_command(&app, test_dir, "wallet2", &client2, arg_vec.clone())?; + let file_name = format!( + "{}/wallet2/slatepack/0436430c-2b02-624c-2032-570501212b07.S2.slatepack", + test_dir + ); + let arg_vec = vec![ + "grin-wallet", + "-a", + "mining", + "-p", + "password1", + "finalize", + "-i", + &file_name, + ]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + // Mine some blocks to confirm the transaction + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 10, false); + + // Check wallet 1 is now empty, except for immature coinbase outputs from recent mining), + // and recently matured coinbase outputs, which were not mature at time of spending. + // This confirms that the TX amount was correctly computed to allow for the fee + grin_wallet_controller::controller::owner_single_use( + Some(wallet1.clone()), + mask1, + None, + |api, m| { + api.set_active_account(m, "mining")?; + let (_, wallet1_info) = api.retrieve_summary_info(m, true, 10)?; + // Entire 'spendable' wallet balance should have been swept, except the coinbase outputs + // which matured in the last batch of mining. Check that the new spendable balance is + // exactly equal to those matured coins. + let amt_mined = 10 * 60_000_000_000; + assert_eq!(wallet1_info.amount_currently_spendable, amt_mined); + Ok(()) + }, + )?; + + // let logging finish + thread::sleep(Duration::from_millis(200)); + clean_output_dir(test_dir); + Ok(()) +} + +#[test] +fn wallet_command_line() { + let test_dir = "target/test_output/command_line"; + if let Err(e) = command_line_test_impl(test_dir) { + panic!("Libwallet Error: {}", e); + } +} diff --git a/tests/common/mod.rs b/tests/common/mod.rs new file mode 100644 index 0000000..569fd33 --- /dev/null +++ b/tests/common/mod.rs @@ -0,0 +1,497 @@ +// Copyright 2021 The Grin Developers +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Common functions for wallet integration tests +extern crate grin_wallet; + +use grin_util as util; +use grin_wallet_config as config; +use grin_wallet_impls::test_framework::LocalWalletClient; + +use clap::{App, ArgMatches}; +use std::path::PathBuf; +use std::sync::Arc; +use std::{env, fs}; +use util::{Mutex, ZeroingString}; + +use grin_core::global::{self, ChainTypes}; +use grin_keychain::ExtKeychain; +use grin_util::{from_hex, static_secp_instance}; +use grin_wallet_api::{EncryptedRequest, EncryptedResponse, JsonId}; +use grin_wallet_config::{GlobalWalletConfig, WalletConfig, GRIN_WALLET_DIR}; +use grin_wallet_impls::{DefaultLCProvider, DefaultWalletImpl}; +use grin_wallet_libwallet::{NodeClient, WalletInfo, WalletInst}; +use util::secp::key::{PublicKey, SecretKey}; + +use grin_api as api; +use grin_wallet::cmd::wallet_args; + +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; +use std::thread; +use std::time::Duration; +use url::Url; + +// Set up 2 wallets and launch the test proxy behind them +#[macro_export] +macro_rules! setup_proxy { + ($test_dir: expr, $chain: ident, $wallet1: ident, $client1: ident, $mask1: ident, $wallet2: ident, $client2: ident, $mask2: ident) => { + // Create a new proxy to simulate server and wallet responses + let mut wallet_proxy: WalletProxy< + DefaultLCProvider, + LocalWalletClient, + ExtKeychain, + > = WalletProxy::new($test_dir); + let $chain = wallet_proxy.chain.clone(); + + // load app yaml. If it don't exist, just say so and exit + let yml = load_yaml!("../src/bin/grin-wallet.yml"); + let app = App::from_yaml(yml); + + // wallet init + let arg_vec = vec!["grin-wallet", "-p", "password", "init", "-h"]; + // should create new wallet file + let $client1 = LocalWalletClient::new("wallet1", wallet_proxy.tx.clone()); + + let target = std::path::PathBuf::from(format!("{}/wallet1/grin-wallet.toml", $test_dir)); + println!("{:?}", target); + if !target.exists() { + execute_command(&app, $test_dir, "wallet1", &$client1, arg_vec.clone())?; + } + + // add wallet to proxy + let config1 = initial_setup_wallet($test_dir, "wallet1"); + let wallet_config1 = config1.clone().members.unwrap().wallet; + //config1.owner_api_listen_port = Some(13420); + let ($wallet1, mask1_i) = instantiate_wallet( + wallet_config1.clone(), + $client1.clone(), + "password", + "default", + )?; + let $mask1 = (&mask1_i).as_ref(); + wallet_proxy.add_wallet( + "wallet1", + $client1.get_send_instance(), + $wallet1.clone(), + mask1_i.clone(), + ); + + // Create wallet 2, which will run a listener + let $client2 = LocalWalletClient::new("wallet2", wallet_proxy.tx.clone()); + + let target = std::path::PathBuf::from(format!("{}/wallet2/grin-wallet.toml", $test_dir)); + if !target.exists() { + execute_command(&app, $test_dir, "wallet2", &$client2, arg_vec.clone())?; + } + + let config2 = initial_setup_wallet($test_dir, "wallet2"); + let wallet_config2 = config2.clone().members.unwrap().wallet; + //config2.api_listen_port = 23415; + let ($wallet2, mask2_i) = instantiate_wallet( + wallet_config2.clone(), + $client2.clone(), + "password", + "default", + )?; + let $mask2 = (&mask2_i).as_ref(); + wallet_proxy.add_wallet( + "wallet2", + $client2.get_send_instance(), + $wallet2.clone(), + mask2_i.clone(), + ); + + // Set the wallet proxy listener running + thread::spawn(move || { + if let Err(e) = wallet_proxy.run() { + error!("Wallet Proxy error: {}", e); + } + }); + }; +} + +#[allow(dead_code)] +pub fn clean_output_dir(test_dir: &str) { + let _ = remove_dir_all::remove_dir_all(test_dir); +} + +#[allow(dead_code)] +pub fn setup(test_dir: &str) { + util::init_test_logger(); + clean_output_dir(test_dir); + global::set_local_chain_type(global::ChainTypes::AutomatedTesting); +} + +/// Some tests require the global chain_type to be configured. +/// If tokio is used in any tests we need to ensure any threads spawned +/// have the chain_type configured correctly. +/// It is recommended to avoid relying on this if at all possible as global chain_type +/// leaks across multiple tests and will likely have unintended consequences. +#[allow(dead_code)] +pub fn setup_global_chain_type() { + global::init_global_chain_type(global::ChainTypes::AutomatedTesting); +} + +/// Create a wallet config file in the given current directory +pub fn config_command_wallet( + dir_name: &str, + wallet_name: &str, +) -> Result<(), grin_wallet_controller::Error> { + let mut current_dir; + let mut default_config = GlobalWalletConfig::default(); + current_dir = env::current_dir().unwrap_or_else(|e| { + panic!("Error creating config file: {}", e); + }); + current_dir.push(dir_name); + current_dir.push(wallet_name); + let _ = fs::create_dir_all(current_dir.clone()); + let mut config_file_name = current_dir.clone(); + config_file_name.push("grin-wallet.toml"); + if config_file_name.exists() { + return Err(grin_wallet_controller::Error::ArgumentError( + "grin-wallet.toml already exists in the target directory. Please remove it first" + .to_owned(), + ))?; + } + default_config.update_paths(¤t_dir, ¤t_dir); + default_config + .write_to_file(config_file_name.to_str().unwrap(), false, None, None) + .unwrap_or_else(|e| { + panic!("Error creating config file: {}", e); + }); + + println!( + "File {} configured and created", + config_file_name.to_str().unwrap(), + ); + Ok(()) +} + +/// Handles setup and detection of paths for wallet +#[allow(dead_code)] +pub fn initial_setup_wallet(dir_name: &str, wallet_name: &str) -> GlobalWalletConfig { + let mut current_dir; + current_dir = env::current_dir().unwrap_or_else(|e| { + panic!("Error creating config file: {}", e); + }); + current_dir.push(dir_name); + current_dir.push(wallet_name); + let _ = fs::create_dir_all(current_dir.clone()); + let mut config_file_name = current_dir.clone(); + config_file_name.push("grin-wallet.toml"); + GlobalWalletConfig::new(config_file_name.to_str().unwrap()).unwrap() +} + +fn get_wallet_subcommand<'a>( + wallet_dir: &str, + wallet_name: &str, + args: ArgMatches<'a>, +) -> ArgMatches<'a> { + match args.subcommand() { + ("init", Some(init_args)) => { + // wallet init command should spit out its config file then continue + // (if desired) + if init_args.is_present("here") { + let _ = config_command_wallet(wallet_dir, wallet_name); + } + init_args.to_owned() + } + _ => ArgMatches::new(), + } +} +// +// Helper to create an instance of the LMDB wallet +#[allow(dead_code)] +pub fn instantiate_wallet( + mut wallet_config: WalletConfig, + node_client: LocalWalletClient, + passphrase: &str, + account: &str, +) -> Result< + ( + Arc< + Mutex< + Box< + dyn WalletInst< + 'static, + DefaultLCProvider<'static, LocalWalletClient, ExtKeychain>, + LocalWalletClient, + ExtKeychain, + >, + >, + >, + >, + Option, + ), + grin_wallet_controller::Error, +> { + wallet_config.chain_type = None; + let mut wallet = Box::new(DefaultWalletImpl::::new(node_client).unwrap()) + as Box< + dyn WalletInst< + DefaultLCProvider<'static, LocalWalletClient, ExtKeychain>, + LocalWalletClient, + ExtKeychain, + >, + >; + let lc = wallet.lc_provider().unwrap(); + // legacy hack to avoid the need for changes in existing grin-wallet.toml files + // remove `wallet_data` from end of path as + // new lifecycle provider assumes grin_wallet.toml is in root of data directory + let mut top_level_wallet_dir = PathBuf::from(wallet_config.clone().data_file_dir); + if top_level_wallet_dir.ends_with(GRIN_WALLET_DIR) { + top_level_wallet_dir.pop(); + wallet_config.data_file_dir = top_level_wallet_dir.to_str().unwrap().into(); + } + let _ = lc.set_top_level_directory(&wallet_config.data_file_dir); + let keychain_mask = lc + .open_wallet(None, ZeroingString::from(passphrase), true, false) + .unwrap(); + let wallet_inst = lc.wallet_inst()?; + wallet_inst.set_parent_key_id_by_name(account)?; + Ok((Arc::new(Mutex::new(wallet)), keychain_mask)) +} + +#[allow(dead_code)] +pub fn execute_command( + app: &App, + test_dir: &str, + wallet_name: &str, + client: &LocalWalletClient, + arg_vec: Vec<&str>, +) -> Result { + let args = app.clone().get_matches_from(arg_vec); + let _ = get_wallet_subcommand(test_dir, wallet_name, args.clone()); + let config = initial_setup_wallet(test_dir, wallet_name); + let mut wallet_config = config.clone().members.unwrap().wallet; + let tor_config = config.clone().members.unwrap().tor; + //unset chain type so it doesn't get reset + wallet_config.chain_type = None; + wallet_args::wallet_command( + &args, + wallet_config.clone(), + tor_config, + client.clone(), + true, + |_| {}, + ) +} + +// as above, but without necessarily setting up the wallet +#[allow(dead_code)] +pub fn execute_command_no_setup( + app: &App, + test_dir: &str, + wallet_name: &str, + client: &C, + arg_vec: Vec<&str>, + f: F, +) -> Result +where + C: NodeClient + 'static + Clone, + F: FnOnce( + Arc< + Mutex< + Box< + dyn WalletInst< + 'static, + DefaultLCProvider<'static, C, ExtKeychain>, + C, + ExtKeychain, + >, + >, + >, + >, + ), +{ + let args = app.clone().get_matches_from(arg_vec); + let _ = get_wallet_subcommand(test_dir, wallet_name, args.clone()); + let config = config::initial_setup_wallet(&ChainTypes::AutomatedTesting, None, true).unwrap(); + let mut wallet_config = config.clone().members.unwrap().wallet; + wallet_config.chain_type = None; + wallet_config.api_secret_path = None; + wallet_config.node_api_secret_path = None; + let tor_config = config.members.unwrap().tor.clone(); + wallet_args::wallet_command(&args, wallet_config, tor_config, client.clone(), true, f) +} + +pub fn post(url: &Url, api_secret: Option, input: &IN) -> Result +where + IN: Serialize, +{ + // TODO: change create_post_request to accept a url instead of a &str + let req = api::client::create_post_request(url.as_str(), api_secret, input)?; + let res = api::client::send_request(req, api::client::TimeOut::default())?; + Ok(res) +} + +#[allow(dead_code)] +pub fn send_request( + id: u64, + dest: &str, + req: &str, +) -> Result, api::Error> +where + OUT: DeserializeOwned, +{ + let url = Url::parse(dest).unwrap(); + let req_val: Value = serde_json::from_str(req).unwrap(); + let res = post(&url, None, &req_val).map_err(|e| { + let err_string = format!("{}", e); + println!("{}", err_string); + thread::sleep(Duration::from_millis(200)); + e + })?; + + let res_val: Value = serde_json::from_str(&res).unwrap(); + // encryption error, just return the string + if res_val["error"] != json!(null) { + return Ok(Err(WalletAPIReturnError { + message: res_val["error"]["message"].as_str().unwrap().to_owned(), + code: res_val["error"]["code"].as_i64().unwrap() as i32, + })); + } + + let res = serde_json::from_str(&res).unwrap(); + let res = easy_jsonrpc_mw::Response::from_json_response(res).unwrap(); + let res = res.outputs.get(&id).unwrap().clone().unwrap(); + if res["Err"] != json!(null) { + Ok(Err(WalletAPIReturnError { + message: res["Err"].as_str().unwrap().to_owned(), + code: res["error"]["code"].as_i64().unwrap() as i32, + })) + } else { + // deserialize result into expected type + let value: OUT = serde_json::from_value(res["Ok"].clone()).unwrap(); + Ok(Ok(value)) + } +} + +#[allow(dead_code)] +pub fn send_request_enc( + sec_req_id: &JsonId, + internal_request_id: u32, + dest: &str, + req: &str, + shared_key: &SecretKey, +) -> Result, api::Error> +where + OUT: DeserializeOwned, +{ + let url = Url::parse(dest).unwrap(); + let req_val: Value = serde_json::from_str(req).unwrap(); + let req = EncryptedRequest::from_json(sec_req_id, &req_val, &shared_key).unwrap(); + let res = post(&url, None, &req).map_err(|e| { + let err_string = format!("{}", e); + println!("{}", err_string); + thread::sleep(Duration::from_millis(200)); + e + })?; + + let res_val: Value = serde_json::from_str(&res).unwrap(); + //println!("RES_VAL: {}", res_val); + // encryption error, just return the string + if res_val["error"] != json!(null) { + return Ok(Err(WalletAPIReturnError { + message: res_val["error"]["message"].as_str().unwrap().to_owned(), + code: res_val["error"]["code"].as_i64().unwrap() as i32, + })); + } + + let enc_resp: EncryptedResponse = serde_json::from_str(&res).unwrap(); + let res = enc_resp.decrypt(shared_key).unwrap(); + if res["error"] != json!(null) { + return Ok(Err(WalletAPIReturnError { + message: res["error"]["message"].as_str().unwrap().to_owned(), + code: res["error"]["code"].as_i64().unwrap() as i32, + })); + } + let res = easy_jsonrpc_mw::Response::from_json_response(res).unwrap(); + let res = res + .outputs + .get(&(internal_request_id as u64)) + .unwrap() + .clone() + .unwrap(); + + //println!("RES: {}", res); + if res["Err"] != json!(null) { + Ok(Err(WalletAPIReturnError { + message: res["Err"].as_str().unwrap().to_owned(), + code: res_val["error"]["code"].as_i64().unwrap() as i32, + })) + } else { + // deserialize result into expected type + let raw_value = res["Ok"].clone(); + let raw_value_str = serde_json::to_string_pretty(&raw_value).unwrap(); + //println!("Raw value: {}", raw_value_str); + let ok_val = serde_json::from_str(&raw_value_str); + match ok_val { + Ok(v) => { + let value: OUT = v; + Ok(Ok(value)) + } + Err(_) => { + //println!("Error deserializing: {:?}", e); + let value: OUT = serde_json::from_value(json!("Null")).unwrap(); + Ok(Ok(value)) + } + } + } +} + +#[allow(dead_code)] +pub fn derive_ecdh_key(sec_key_str: &str, other_pubkey: &PublicKey) -> SecretKey { + let sec_key_bytes = from_hex(sec_key_str).unwrap(); + let sec_key = { + let secp_inst = static_secp_instance(); + let secp = secp_inst.lock(); + SecretKey::from_slice(&secp, &sec_key_bytes).unwrap() + }; + + let secp_inst = static_secp_instance(); + let secp = secp_inst.lock(); + + let mut shared_pubkey = other_pubkey.clone(); + shared_pubkey.mul_assign(&secp, &sec_key).unwrap(); + + let x_coord = shared_pubkey.serialize_vec(&secp, true); + SecretKey::from_slice(&secp, &x_coord[1..]).unwrap() +} + +// Types to make working with json responses easier +#[derive(Clone, Debug, Serialize, Deserialize, thiserror::Error)] +pub struct WalletAPIReturnError { + pub message: String, + pub code: i32, +} + +impl std::fmt::Display for WalletAPIReturnError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{} - {}", self.code, &self.message) + } +} + +impl From for WalletAPIReturnError { + fn from(error: grin_wallet_controller::Error) -> WalletAPIReturnError { + WalletAPIReturnError { + message: error.to_string(), + code: -1, + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct RetrieveSummaryInfoResp(pub bool, pub WalletInfo); diff --git a/tests/data/v2_reqs/init_send_tx.req.json b/tests/data/v2_reqs/init_send_tx.req.json new file mode 100644 index 0000000..04920cf --- /dev/null +++ b/tests/data/v2_reqs/init_send_tx.req.json @@ -0,0 +1,21 @@ +{ + "jsonrpc": "2.0", + "method": "init_send_tx", + "params": { + "args": { + "src_acct_name": null, + "amount": "600000000000", + "minimum_confirmations": 2, + "max_outputs": 500, + "num_change_outputs": 1, + "selection_strategy_is_use_all": true, + "message": "my message", + "target_slate_version": null, + "payment_proof_recipient_address": null, + "ttl_blocks": null, + "send_args": null + } + }, + "id": 1 +} + diff --git a/tests/data/v2_reqs/retrieve_info.req.json b/tests/data/v2_reqs/retrieve_info.req.json new file mode 100644 index 0000000..7bc9be8 --- /dev/null +++ b/tests/data/v2_reqs/retrieve_info.req.json @@ -0,0 +1,9 @@ +{ + "jsonrpc": "2.0", + "method": "retrieve_summary_info", + "params": [ + true, + 1 + ], + "id": 1 +} diff --git a/tests/data/v3_reqs/change_password.req.json b/tests/data/v3_reqs/change_password.req.json new file mode 100644 index 0000000..14de45b --- /dev/null +++ b/tests/data/v3_reqs/change_password.req.json @@ -0,0 +1,10 @@ +{ + "jsonrpc": "2.0", + "method": "change_password", + "params": { + "name": null, + "old": "passwoid", + "new": "password" + }, + "id": 1 +} diff --git a/tests/data/v3_reqs/close_wallet.req.json b/tests/data/v3_reqs/close_wallet.req.json new file mode 100644 index 0000000..fedb965 --- /dev/null +++ b/tests/data/v3_reqs/close_wallet.req.json @@ -0,0 +1,8 @@ +{ + "jsonrpc": "2.0", + "method": "close_wallet", + "params": { + "name": null + }, + "id": 1 +} diff --git a/tests/data/v3_reqs/create_config.req.json b/tests/data/v3_reqs/create_config.req.json new file mode 100644 index 0000000..fdd11e7 --- /dev/null +++ b/tests/data/v3_reqs/create_config.req.json @@ -0,0 +1,11 @@ +{ + "jsonrpc": "2.0", + "method": "create_config", + "params": { + "chain_type": "AutomatedTesting", + "wallet_config": null, + "logging_config": null, + "tor_config": null + }, + "id": 1 +} diff --git a/tests/data/v3_reqs/create_wallet.req.json b/tests/data/v3_reqs/create_wallet.req.json new file mode 100644 index 0000000..3b5f081 --- /dev/null +++ b/tests/data/v3_reqs/create_wallet.req.json @@ -0,0 +1,11 @@ +{ + "jsonrpc": "2.0", + "method": "create_wallet", + "params": { + "name": null, + "mnemonic": null, + "mnemonic_length": 32, + "password": "passwoid" + }, + "id": 1 +} diff --git a/tests/data/v3_reqs/create_wallet_invalid_mn.req.json b/tests/data/v3_reqs/create_wallet_invalid_mn.req.json new file mode 100644 index 0000000..da6f178 --- /dev/null +++ b/tests/data/v3_reqs/create_wallet_invalid_mn.req.json @@ -0,0 +1,11 @@ +{ + "jsonrpc": "2.0", + "method": "create_wallet", + "params": { + "name": null, + "mnemonic": "this is not valid", + "mnemonic_length": 32, + "password": "passwoid" + }, + "id": 1 +} diff --git a/tests/data/v3_reqs/create_wallet_valid_mn.req.json b/tests/data/v3_reqs/create_wallet_valid_mn.req.json new file mode 100644 index 0000000..332e584 --- /dev/null +++ b/tests/data/v3_reqs/create_wallet_valid_mn.req.json @@ -0,0 +1,11 @@ +{ + "jsonrpc": "2.0", + "method": "create_wallet", + "params": { + "name": null, + "mnemonic": "fat twenty mean degree forget shell check candy immense awful flame next during february bulb bike sun wink theory day kiwi embrace peace lunch", + "mnemonic_length": 32, + "password": "passwoid" + }, + "id": 1 +} diff --git a/tests/data/v3_reqs/delete_wallet.req.json b/tests/data/v3_reqs/delete_wallet.req.json new file mode 100644 index 0000000..ef1aa97 --- /dev/null +++ b/tests/data/v3_reqs/delete_wallet.req.json @@ -0,0 +1,8 @@ +{ + "jsonrpc": "2.0", + "method": "delete_wallet", + "params": { + "name": null + }, + "id": 1 +} diff --git a/tests/data/v3_reqs/get_top_level.req.json b/tests/data/v3_reqs/get_top_level.req.json new file mode 100644 index 0000000..0ebb2ea --- /dev/null +++ b/tests/data/v3_reqs/get_top_level.req.json @@ -0,0 +1,7 @@ +{ + "jsonrpc": "2.0", + "method": "get_top_level_directory", + "params": { + }, + "id": 1 +} diff --git a/tests/data/v3_reqs/init_secure_api.req.json b/tests/data/v3_reqs/init_secure_api.req.json new file mode 100644 index 0000000..11d9e8e --- /dev/null +++ b/tests/data/v3_reqs/init_secure_api.req.json @@ -0,0 +1,8 @@ +{ + "jsonrpc": "2.0", + "method": "init_secure_api", + "params": { + "ecdh_pubkey": "03b3c18c9a38783d105e238953b1638b021ba7456d87a5c085b3bdb75777b4c490" + }, + "id": 1 +} diff --git a/tests/data/v3_reqs/init_send_tx.req.json b/tests/data/v3_reqs/init_send_tx.req.json new file mode 100644 index 0000000..f73710d --- /dev/null +++ b/tests/data/v3_reqs/init_send_tx.req.json @@ -0,0 +1,23 @@ +{ + "jsonrpc": "2.0", + "method": "init_send_tx", + "params": { + "token": null, + "args": { + "src_acct_name": null, + "amount": "600000000000", + "amount_includes_fee": false, + "minimum_confirmations": 2, + "max_outputs": 500, + "num_change_outputs": 1, + "selection_strategy_is_use_all": true, + "message": "my message", + "target_slate_version": null, + "payment_proof_recipient_address": null, + "ttl_blocks": null, + "send_args": null + } + }, + "id": 1 +} + diff --git a/tests/data/v3_reqs/open_wallet.req.json b/tests/data/v3_reqs/open_wallet.req.json new file mode 100644 index 0000000..5eb4a5a --- /dev/null +++ b/tests/data/v3_reqs/open_wallet.req.json @@ -0,0 +1,9 @@ +{ + "jsonrpc": "2.0", + "method": "open_wallet", + "params": { + "name": null, + "password": "passwoid" + }, + "id": 1 +} diff --git a/tests/data/v3_reqs/retrieve_info.req.json b/tests/data/v3_reqs/retrieve_info.req.json new file mode 100644 index 0000000..a70f745 --- /dev/null +++ b/tests/data/v3_reqs/retrieve_info.req.json @@ -0,0 +1,10 @@ +{ + "jsonrpc": "2.0", + "method": "retrieve_summary_info", + "params": { + "token": null, + "refresh_from_node": true, + "minimum_confirmations": 1 + }, + "id": 1 +} diff --git a/tests/owner_v3_init_secure.rs b/tests/owner_v3_init_secure.rs new file mode 100644 index 0000000..a03be00 --- /dev/null +++ b/tests/owner_v3_init_secure.rs @@ -0,0 +1,245 @@ +// Copyright 2021 The Grin Developers +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#[macro_use] +extern crate clap; + +#[macro_use] +extern crate log; + +extern crate grin_wallet; + +use grin_wallet_api::{ECDHPubkey, JsonId}; +use grin_wallet_impls::test_framework::{self, LocalWalletClient, WalletProxy}; + +use clap::App; +use std::thread; +use std::time::Duration; + +use grin_keychain::ExtKeychain; +use grin_util::secp::key::SecretKey; +use grin_util::{from_hex, static_secp_instance}; +use grin_wallet_impls::DefaultLCProvider; +use serde_json; + +#[macro_use] +mod common; +use common::{ + clean_output_dir, derive_ecdh_key, execute_command, initial_setup_wallet, instantiate_wallet, + send_request, send_request_enc, setup, setup_global_chain_type, RetrieveSummaryInfoResp, +}; + +#[test] +fn owner_v3_init_secure() -> Result<(), grin_wallet_controller::Error> { + setup_global_chain_type(); + + let test_dir = "target/test_output/owner_v3_init_secure"; + setup(test_dir); + + // Create a new proxy to simulate server and wallet responses + setup_proxy!(test_dir, chain, wallet1, client1, mask1, wallet2, client2, _mask2); + + // add some blocks manually + let bh = 2u64; + let _ = + test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, bh as usize, false); + + // run a wallet owner listener + let arg_vec = vec!["grin-wallet", "-p", "password", "owner_api", "-l", "33420"]; + thread::spawn(move || { + let yml = load_yaml!("../src/bin/grin-wallet.yml"); + let app = App::from_yaml(yml); + execute_command(&app, test_dir, "wallet1", &client1, arg_vec.clone()).unwrap(); + }); + thread::sleep(Duration::from_millis(200)); + + // use in all tests + let sec_key_str = "e00dcc4a009e3427c6b1e1a550c538179d46f3827a13ed74c759c860761caf1e"; + let _pub_key_str = "03b3c18c9a38783d105e238953b1638b021ba7456d87a5c085b3bdb75777b4c490"; + + let sec_key_bytes = from_hex(sec_key_str).unwrap(); + let sec_key = { + let secp_inst = static_secp_instance(); + let secp = secp_inst.lock(); + SecretKey::from_slice(&secp, &sec_key_bytes).unwrap() + }; + + // 1) Attempt to send an encrypted request before calling `init_secure_api` + let req = include_str!("data/v3_reqs/retrieve_info.req.json"); + let res = send_request_enc::( + &JsonId::IntId(1), + 1, + "http://127.0.0.1:33420/v3/owner", + &req, + &sec_key, + )?; + println!("RES 1: {:?}", res); + assert!(res.is_err()); + assert_eq!(res.unwrap_err().code, -32001); + + // 2) Call any function on the V3 api without calling 'init_secure_api` first + let res = send_request::(1, "http://127.0.0.1:33420/v3/owner", &req)?; + println!("RES 2: {:?}", res); + assert!(res.is_err()); + assert_eq!(res.unwrap_err().code, -32001); + + // 3) Call 'init_secure_api' and negotiate shared key + let req = include_str!("data/v3_reqs/init_secure_api.req.json"); + let res = send_request(1, "http://127.0.0.1:33420/v3/owner", req)?; + println!("RES 3: {:?}", res); + + assert!(res.is_ok()); + let value: ECDHPubkey = res.unwrap(); + let shared_key = derive_ecdh_key(sec_key_str, &value.ecdh_pubkey); + + // 4) A normal request, correct key + let req = include_str!("data/v3_reqs/retrieve_info.req.json"); + let res = send_request_enc::( + &JsonId::StrId(String::from("1")), + 1, + "http://127.0.0.1:33420/v3/owner", + &req, + &shared_key, + )?; + println!("RES 4: {:?}", res); + assert!(res.is_ok()); + + // 5) A normal request, incorrect key + let mut bad_key = shared_key.clone(); + bad_key.0[0] = 0; + let req = include_str!("data/v3_reqs/retrieve_info.req.json"); + let res = send_request_enc::( + &JsonId::StrId(String::from("1")), + 1, + "http://127.0.0.1:33420/v3/owner", + &req, + &bad_key, + )?; + println!("RES 5: {:?}", res); + assert!(res.is_err()); + assert_eq!(res.unwrap_err().code, -32002); + + // 6) A malformed encrypted json request (missing nonce) + let req = serde_json::json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "encrypted_request_v3", + "params": { + "body_enc:": "thisiswrong", + } + }); + let res = send_request::(1, "http://127.0.0.1:33420/v3/owner", &req.to_string())?; + println!("RES 6: {:?}", res); + assert!(res.is_err()); + assert_eq!(res.unwrap_err().code, -32002); + + // 7) A malformed encrypted json request (garbage encrypted content) + let req = serde_json::json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "encrypted_request_v3", + "params": { + "nonce": "32", + "body_enc": "thisiswrong", + } + }); + let res = send_request::(1, "http://127.0.0.1:33420/v3/owner", &req.to_string())?; + println!("RES 7: {:?}", res); + assert!(res.is_err()); + assert_eq!(res.unwrap_err().code, -32002); + + // 8) Encrypted call to `init_secure_api`, followed by re-deriving key + let req = include_str!("data/v3_reqs/init_secure_api.req.json"); + let res = send_request_enc( + &JsonId::StrId(String::from("1")), + 1, + "http://127.0.0.1:33420/v3/owner", + &req.to_string(), + &shared_key, + )?; + println!("RES 8: {:?}", res); + assert!(res.is_ok()); + let value: ECDHPubkey = res.unwrap(); + let shared_key = derive_ecdh_key(sec_key_str, &value.ecdh_pubkey); + + // 9) A normal request, with new correct key + let req = include_str!("data/v3_reqs/retrieve_info.req.json"); + let res = send_request_enc::( + &JsonId::StrId(String::from("1")), + 1, + "http://127.0.0.1:33420/v3/owner", + &req, + &shared_key, + )?; + println!("RES 9: {:?}", res); + assert!(res.is_ok()); + + // 10) Call 'init_secure_api' unencrypted (which we can do) and negotiate new shared key + let req = include_str!("data/v3_reqs/init_secure_api.req.json"); + let res = send_request(1, "http://127.0.0.1:33420/v3/owner", req)?; + println!("RES 10: {:?}", res); + + assert!(res.is_ok()); + let value: ECDHPubkey = res.unwrap(); + let shared_key = derive_ecdh_key(sec_key_str, &value.ecdh_pubkey); + + // 11) A normal request, correct key + let req = include_str!("data/v3_reqs/retrieve_info.req.json"); + let res = send_request_enc::( + &JsonId::StrId(String::from("1")), + 1, + "http://127.0.0.1:33420/v3/owner", + &req, + &shared_key, + )?; + println!("RES 11: {:?}", res); + assert!(res.is_ok()); + + // 12) A request which triggers an API error (not an encryption error) + let req = serde_json::json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "method_dun_exist", + "params": { + "nope": "nope", + } + }) + .to_string(); + let res = send_request_enc::( + &JsonId::IntId(12), + 1, + "http://127.0.0.1:33420/v3/owner", + &req, + &shared_key, + )?; + println!("RES 12: {:?}", res); + assert!(res.is_err()); + assert_eq!(res.unwrap_err().code, -32601); + + // 13) A request which triggers an internal API error (not enough funds) + let req = include_str!("data/v3_reqs/init_send_tx.req.json"); + let res = send_request_enc::( + &JsonId::StrId(String::from("13")), + 1, + "http://127.0.0.1:33420/v3/owner", + &req, + &shared_key, + )?; + println!("RES 13: {:?}", res); + assert!(res.is_err()); + assert_eq!(res.unwrap_err().code, -32099); + + clean_output_dir(test_dir); + + Ok(()) +} diff --git a/tests/owner_v3_lifecycle.rs b/tests/owner_v3_lifecycle.rs new file mode 100644 index 0000000..42295d5 --- /dev/null +++ b/tests/owner_v3_lifecycle.rs @@ -0,0 +1,623 @@ +// Copyright 2021 The Grin Developers +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#[macro_use] +extern crate clap; + +#[macro_use] +extern crate log; + +extern crate grin_wallet; + +use grin_wallet_api::{ECDHPubkey, JsonId}; +use grin_wallet_impls::test_framework::{self, LocalWalletClient, WalletProxy}; + +use clap::App; +use std::thread; +use std::time::Duration; + +use grin_keychain::ExtKeychain; +use grin_wallet_impls::DefaultLCProvider; +use grin_wallet_libwallet::{InitTxArgs, Slate, SlateVersion, VersionedSlate}; +use serde_json; + +use grin_util::Mutex; +use std::path::PathBuf; +use std::sync::Arc; + +#[macro_use] +mod common; +use common::{ + clean_output_dir, derive_ecdh_key, execute_command, execute_command_no_setup, + initial_setup_wallet, instantiate_wallet, send_request, send_request_enc, setup, + setup_global_chain_type, RetrieveSummaryInfoResp, +}; + +#[test] +fn owner_v3_lifecycle() -> Result<(), grin_wallet_controller::Error> { + setup_global_chain_type(); + + let test_dir = "target/test_output/owner_v3_lifecycle"; + setup(test_dir); + + let yml = load_yaml!("../src/bin/grin-wallet.yml"); + let app = App::from_yaml(yml); + + // Create a new proxy to simulate server and wallet responses + let wallet_proxy_a: Arc< + Mutex< + WalletProxy< + DefaultLCProvider<'static, LocalWalletClient, ExtKeychain>, + LocalWalletClient, + ExtKeychain, + >, + >, + > = Arc::new(Mutex::new(WalletProxy::new(test_dir))); + let (chain, wallet2, mask2_i) = { + let mut wallet_proxy = wallet_proxy_a.lock(); + let chain = wallet_proxy.chain.clone(); + + // Create wallet 2 manually, which will mine a bit and insert some + // grins into the equation + let client2 = LocalWalletClient::new("wallet2", wallet_proxy.tx.clone()); + let arg_vec = vec!["grin-wallet", "-p", "password", "init", "-h"]; + execute_command(&app, test_dir, "wallet2", &client2, arg_vec.clone())?; + + let config2 = initial_setup_wallet(test_dir, "wallet2"); + let wallet_config2 = config2.clone().members.unwrap().wallet; + //config2.api_listen_port = 23415; + let (wallet2, mask2_i) = instantiate_wallet( + wallet_config2.clone(), + client2.clone(), + "password", + "default", + )?; + wallet_proxy.add_wallet( + "wallet2", + client2.get_send_instance(), + wallet2.clone(), + mask2_i.clone(), + ); + + // start up the owner api with wallet created + let arg_vec = vec!["grin-wallet", "owner_api", "-l", "43420", "--run_foreign"]; + // should create new wallet file + let client1 = LocalWalletClient::new("wallet1", wallet_proxy.tx.clone()); + + let p = wallet_proxy_a.clone(); + + thread::spawn(move || { + let yml = load_yaml!("../src/bin/grin-wallet.yml"); + let app = App::from_yaml(yml); + execute_command_no_setup( + &app, + test_dir, + "wallet1", + &client1, + arg_vec.clone(), + |wallet_inst| { + let mut wallet_proxy = p.lock(); + wallet_proxy.add_wallet( + "wallet1", + client1.get_send_instance(), + wallet_inst, + None, + ); + }, + ) + .unwrap(); + }); + (chain, wallet2, mask2_i) + }; + // give a bit for wallet to init and populate proxy with wallet via callback in thread above + thread::sleep(Duration::from_millis(500)); + let mask2 = (&mask2_i).as_ref(); + let wallet_proxy = wallet_proxy_a.clone(); + + // Set the wallet proxy listener running + thread::spawn(move || { + let mut p = wallet_proxy.lock(); + if let Err(e) = p.run() { + error!("Wallet Proxy error: {}", e); + } + }); + + // mine into wallet 2 a bit + let bh = 10u64; + let _ = + test_framework::award_blocks_to_wallet(&chain, wallet2.clone(), mask2, bh as usize, false); + + // We have an owner API with no wallet initialized. Init the secure API + let sec_key_str = "e00dcc4a009e3427c6b1e1a550c538179d46f3827a13ed74c759c860761caf1e"; + let req = include_str!("data/v3_reqs/init_secure_api.req.json"); + let res = send_request(1, "http://127.0.0.1:43420/v3/owner", req)?; + println!("RES 1: {:?}", res); + + assert!(res.is_ok()); + let value: ECDHPubkey = res.unwrap(); + let shared_key = derive_ecdh_key(sec_key_str, &value.ecdh_pubkey); + + // 2) get the top level directory, should default to ~/.grin/auto + let req = include_str!("data/v3_reqs/get_top_level.req.json"); + let res = send_request_enc::( + &JsonId::StrId(String::from("1")), + 1, + "http://127.0.0.1:43420/v3/owner", + &req, + &shared_key, + )?; + println!("RES 2: {:?}", res); + assert!(res.is_ok()); + assert!(res.unwrap().contains("auto")); + + // 3) now set the top level directory to our test wallet dir + let req = serde_json::json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "set_top_level_directory", + "params": { + "dir": format!("{}/wallet1", test_dir) + } + }); + let res = send_request_enc::( + &JsonId::StrId(String::from("1")), + 1, + "http://127.0.0.1:43420/v3/owner", + &req.to_string(), + &shared_key, + )?; + println!("RES 3: {:?}", res); + assert!(res.is_ok()); + + // 4) create a configuration file in top level directory + let req = include_str!("data/v3_reqs/create_config.req.json"); + let res = send_request_enc::( + &JsonId::StrId(String::from("1")), + 1, + "http://127.0.0.1:43420/v3/owner", + &req, + &shared_key, + )?; + println!("RES 4: {:?}", res); + assert!(res.is_ok()); + let pb = PathBuf::from(format!("{}/wallet1/grin-wallet.toml", test_dir)); + assert!(pb.exists()); + + // 5) Try and perform an operation without having a wallet open + let req = include_str!("data/v3_reqs/retrieve_info.req.json"); + let res = send_request_enc::( + &JsonId::StrId(String::from("1")), + 1, + "http://127.0.0.1:43420/v3/owner", + &req, + &shared_key, + )?; + println!("RES 5: {:?}", res); + assert!(res.is_err()); + + // 6) Create a wallet + let req = include_str!("data/v3_reqs/create_wallet.req.json"); + let res = send_request_enc::( + &JsonId::StrId(String::from("1")), + 1, + "http://127.0.0.1:43420/v3/owner", + &req, + &shared_key, + )?; + println!("RES 6: {:?}", res); + assert!(res.is_ok()); + + // 7) Try and create a wallet when one exists + let req = include_str!("data/v3_reqs/create_wallet.req.json"); + let res = send_request_enc::( + &JsonId::StrId(String::from("1")), + 1, + "http://127.0.0.1:43420/v3/owner", + &req, + &shared_key, + )?; + println!("RES 7: {:?}", res); + assert!(res.is_err()); + + // 8) Open the wallet + let req = include_str!("data/v3_reqs/open_wallet.req.json"); + let res = send_request_enc::( + &JsonId::StrId(String::from("1")), + 1, + "http://127.0.0.1:43420/v3/owner", + &req, + &shared_key, + )?; + println!("RES 8: {:?}", res); + assert!(res.is_ok()); + let token = res.unwrap(); + + // 9) Send a request with our new token + let req = serde_json::json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "retrieve_summary_info", + "params": { + "token": token, + "refresh_from_node": true, + "minimum_confirmations": 1 + } + }); + + let res = send_request_enc::( + &JsonId::StrId(String::from("1")), + 1, + "http://127.0.0.1:43420/v3/owner", + &req.to_string(), + &shared_key, + )?; + println!("RES 9: {:?}", res); + assert!(res.is_ok()); + + // 10) Send same request with no token (even though one is expected) + let req = serde_json::json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "retrieve_summary_info", + "params": { + "token": null, + "refresh_from_node": true, + "minimum_confirmations": 1 + } + }); + + let res = send_request_enc::( + &JsonId::StrId(String::from("1")), + 1, + "http://127.0.0.1:43420/v3/owner", + &req.to_string(), + &shared_key, + )?; + println!("RES 10: {:?}", res); + assert!(res.is_err()); + + // 11) Close the wallet + let req = include_str!("data/v3_reqs/close_wallet.req.json"); + let res = send_request_enc::( + &JsonId::StrId(String::from("1")), + 1, + "http://127.0.0.1:43420/v3/owner", + &req, + &shared_key, + )?; + println!("RES 11: {:?}", res); + assert!(res.is_ok()); + + // 12) Wallet is closed + let req = serde_json::json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "retrieve_summary_info", + "params": { + "token": token, + "refresh_from_node": true, + "minimum_confirmations": 1 + } + }); + + let res = send_request_enc::( + &JsonId::StrId(String::from("1")), + 1, + "http://127.0.0.1:43420/v3/owner", + &req.to_string(), + &shared_key, + )?; + println!("RES 12: {:?}", res); + assert!(res.is_err()); + + // 13) Open the wallet again + let req = include_str!("data/v3_reqs/open_wallet.req.json"); + let res = send_request_enc::( + &JsonId::StrId(String::from("1")), + 1, + "http://127.0.0.1:43420/v3/owner", + &req, + &shared_key, + )?; + println!("RES 13: {:?}", res); + assert!(res.is_ok()); + let token = res.unwrap(); + + // 14) Send a request with our new token + let req = serde_json::json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "retrieve_summary_info", + "params": { + "token": token, + "refresh_from_node": true, + "minimum_confirmations": 1 + } + }); + let res = send_request_enc::( + &JsonId::StrId(String::from("1")), + 1, + "http://127.0.0.1:43420/v3/owner", + &req.to_string(), + &shared_key, + )?; + println!("RES 14: {:?}", res); + assert!(res.is_ok()); + + //15) Ask wallet 2 for some grins + let req = serde_json::json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "issue_invoice_tx", + "params": { + "token": token, + "args": { + "amount": "6000000000", + "message": "geez a block of grins", + "dest_acct_name": null, + "target_slate_version": null + } + } + }); + let res = send_request_enc::( + &JsonId::StrId(String::from("1")), + 1, + "http://127.0.0.1:43420/v3/owner", + &req.to_string(), + &shared_key, + )?; + println!("RES 15: {:?}", res); + assert!(res.is_ok()); + let mut slate: Slate = res.unwrap().into(); + + // give this slate over to wallet 2 manually + grin_wallet_controller::controller::owner_single_use( + Some(wallet2.clone()), + mask2, + None, + |api, m| { + let args = InitTxArgs { + src_acct_name: None, + amount: slate.amount, + minimum_confirmations: 1, + max_outputs: 500, + num_change_outputs: 1, + selection_strategy_is_use_all: false, + ..Default::default() + }; + let res = api.process_invoice_tx(m, &slate, args); + assert!(res.is_ok()); + slate = res.unwrap(); + api.tx_lock_outputs(m, &slate)?; + Ok(()) + }, + )?; + + //16) Finalize the invoice tx (to foreign api) + // (Tests that foreign API on same port also has its stored mask updated) + let req = serde_json::json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "finalize_tx", + "params": { + "slate": VersionedSlate::into_version(slate, SlateVersion::V4)?, + } + }); + let res = + send_request::(1, "http://127.0.0.1:43420/v2/foreign", &req.to_string())?; + println!("RES 16: {:?}", res); + assert!(res.is_ok()); + + //17) Change the password + let req = include_str!("data/v3_reqs/close_wallet.req.json"); + let res = send_request_enc::( + &JsonId::StrId(String::from("1")), + 1, + "http://127.0.0.1:43420/v3/owner", + &req, + &shared_key, + )?; + println!("RES 17: {:?}", res); + assert!(res.is_ok()); + + let req = include_str!("data/v3_reqs/change_password.req.json"); + let res = send_request_enc::( + &JsonId::StrId(String::from("1")), + 1, + "http://127.0.0.1:43420/v3/owner", + &req, + &shared_key, + )?; + println!("RES 17a: {:?}", res); + assert!(res.is_ok()); + + // 18) trying to open with old password should fail + let req = include_str!("data/v3_reqs/open_wallet.req.json"); + let res = send_request_enc::( + &JsonId::StrId(String::from("1")), + 1, + "http://127.0.0.1:43420/v3/owner", + &req, + &shared_key, + )?; + println!("RES 18: {:?}", res); + assert!(res.is_err()); + + // 19) Open with new password + let req = serde_json::json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "open_wallet", + "params": { + "name": null, + "password": "password" + } + }); + let res = send_request_enc::( + &JsonId::StrId(String::from("1")), + 1, + "http://127.0.0.1:43420/v3/owner", + &req.to_string(), + &shared_key, + )?; + println!("RES 19: {:?}", res); + assert!(res.is_ok()); + let token = res.unwrap(); + + // 20) Send a request with new token with changed password, ensure balances are still there and + // therefore seed is the same + let req = serde_json::json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "retrieve_summary_info", + "params": { + "token": token, + "refresh_from_node": true, + "minimum_confirmations": 1 + } + }); + + let res = send_request_enc::( + &JsonId::StrId(String::from("1")), + 1, + "http://127.0.0.1:43420/v3/owner", + &req.to_string(), + &shared_key, + )?; + println!("RES 20: {:?}", res); + + thread::sleep(Duration::from_millis(200)); + assert_eq!(res.unwrap().1.amount_awaiting_finalization, 6000000000); + + // 21) Start the automatic updater, let it run for a bit + let req = serde_json::json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "start_updater", + "params": { + "token": token, + "frequency": 3000, + } + }); + + let res = send_request_enc::( + &JsonId::StrId(String::from("1")), + 1, + "http://127.0.0.1:43420/v3/owner", + &req.to_string(), + &shared_key, + )?; + assert!(res.is_ok()); + println!("RES 21: {:?}", res); + thread::sleep(Duration::from_millis(5000)); + + // 22) Retrieve some messages about updater status + let req = serde_json::json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "get_updater_messages", + "params": { + "count": 1000, + } + }); + + let res = send_request_enc::( + &JsonId::StrId(String::from("1")), + 1, + "http://127.0.0.1:43420/v3/owner", + &req.to_string(), + &shared_key, + )?; + assert!(res.is_ok()); + println!("RES 22: {:?}", res); + + // 23) Stop Updater + let req = serde_json::json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "stop_updater", + "params": null + }); + + let res = send_request_enc::( + &JsonId::StrId(String::from("1")), + 1, + "http://127.0.0.1:43420/v3/owner", + &req.to_string(), + &shared_key, + )?; + assert!(res.is_ok()); + println!("RES 23: {:?}", res); + + // 24) Delete the wallet (close first) + let req = include_str!("data/v3_reqs/close_wallet.req.json"); + let res = send_request_enc::( + &JsonId::StrId(String::from("1")), + 1, + "http://127.0.0.1:43420/v3/owner", + &req, + &shared_key, + )?; + assert!(res.is_ok()); + + let req = include_str!("data/v3_reqs/delete_wallet.req.json"); + let res = send_request_enc::( + &JsonId::StrId(String::from("1")), + 1, + "http://127.0.0.1:43420/v3/owner", + &req, + &shared_key, + )?; + println!("RES 24: {:?}", res); + assert!(res.is_ok()); + + // 25) Wallet should be gone + let req = include_str!("data/v3_reqs/open_wallet.req.json"); + let res = send_request_enc::( + &JsonId::StrId(String::from("1")), + 1, + "http://127.0.0.1:43420/v3/owner", + &req, + &shared_key, + )?; + println!("RES 25: {:?}", res); + assert!(res.is_err()); + + // 26) Try to create a wallet with an invalid mnemonic + let req = include_str!("data/v3_reqs/create_wallet_invalid_mn.req.json"); + let res = send_request_enc::( + &JsonId::StrId(String::from("1")), + 1, + "http://127.0.0.1:43420/v3/owner", + &req, + &shared_key, + )?; + println!("RES 26: {:?}", res); + assert!(res.is_err()); + + // 27) Try to create a wallet with an valid mnemonic + let req = include_str!("data/v3_reqs/create_wallet_valid_mn.req.json"); + let res = send_request_enc::( + &JsonId::StrId(String::from("1")), + 1, + "http://127.0.0.1:43420/v3/owner", + &req, + &shared_key, + )?; + println!("RES 27: {:?}", res); + assert!(res.is_ok()); + + clean_output_dir(test_dir); + + Ok(()) +} diff --git a/tests/tor_dev_helper.rs b/tests/tor_dev_helper.rs new file mode 100644 index 0000000..e8bf2d8 --- /dev/null +++ b/tests/tor_dev_helper.rs @@ -0,0 +1,100 @@ +// Copyright 2021 The Grin Developers +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#[macro_use] +extern crate clap; + +#[macro_use] +extern crate log; + +extern crate grin_wallet; + +use grin_wallet_impls::test_framework::{self, LocalWalletClient, WalletProxy}; + +use clap::App; +use std::thread; +use std::time::Duration; + +use grin_keychain::ExtKeychain; +use grin_wallet_impls::DefaultLCProvider; + +use grin_util as util; + +#[macro_use] +mod common; +use common::{execute_command, initial_setup_wallet, instantiate_wallet, setup_global_chain_type}; + +// Development testing helper for tor/socks investigation. +// Not (yet) to be run as part of automated testing + +fn setup_no_clean() { + util::init_test_logger(); + setup_global_chain_type(); +} + +#[ignore] +#[test] +fn socks_tor() -> Result<(), grin_wallet_controller::Error> { + let test_dir = "target/test_output/socks_tor"; + let yml = load_yaml!("../src/bin/grin-wallet.yml"); + let app = App::from_yaml(yml); + setup_no_clean(); + + setup_proxy!(test_dir, chain, wallet1, client1, mask1, wallet2, client2, _mask2); + + // Tor should be running at this point for wallet 2, with a hidden service + // bound to the listening port 53415. By default, tor will also be running + // a socks proxy lister at 127.0.0.1 9050 (both wallets can use for now) + // + // Relevant torrc config: + // HiddenServiceDir ./hidden_service/ + // HiddenServicePort 80 127.0.0.1:53415 + // + // tor -f torrc + + // Substitute whatever onion address has been created + let onion_address = "2a6at2obto3uvkpkitqp4wxcg6u36qf534eucbskqciturczzc5suyid"; + + // run the foreign listener for wallet 2 + let arg_vec = vec!["grin-wallet", "-p", "password", "listen"]; + // Set owner listener running + thread::spawn(move || { + let yml = load_yaml!("../src/bin/grin-wallet.yml"); + let app = App::from_yaml(yml); + execute_command(&app, test_dir, "wallet2", &client2, arg_vec.clone()).unwrap(); + }); + + // dumb pause for now, hidden service should already be running + thread::sleep(Duration::from_millis(3000)); + + // mine into wallet 1 a bit + let bh = 5u64; + let _ = + test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, bh as usize, false); + + // now, test send from wallet 1 over tor + let arg_vec = vec![ + "grin-wallet", + "-p", + "password", + "send", + "-c", + "2", + "-d", + onion_address, + "10", + ]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + Ok(()) +} diff --git a/util/Cargo.toml b/util/Cargo.toml new file mode 100644 index 0000000..879866e --- /dev/null +++ b/util/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "grin_wallet_util" +version = "5.4.0-alpha.1" +authors = ["Grin Developers "] +description = "Util, for generic utilities and to re-export grin crates" +license = "Apache-2.0" +repository = "https://github.com/mimblewimble/grin-wallet" +keywords = [ "crypto", "grin", "mimblewimble" ] +workspace = ".." +edition = "2018" + +[dependencies] +rand = "0.6" +serde = "1" +serde_derive = "1" +ed25519-dalek = "1.0.0-pre.4" +data-encoding = "2" +sha3 = "0.8" +thiserror = "1" + +##### Grin Imports + +# For Release +grin_util = "5.3.3" + +# For beta release + +# grin_util = { git = "https://github.com/mimblewimble/grin", tag = "v5.2.0-beta.3" } + +# For bleeding edge +# grin_util = { git = "https://github.com/mimblewimble/grin", branch = "master" } + +# For local testing + +# grin_util = { path = "../../grin/util"} + +##### + +[dev-dependencies] +pretty_assertions = "0.6" diff --git a/util/src/byte_ser.rs b/util/src/byte_ser.rs new file mode 100644 index 0000000..8e5a936 --- /dev/null +++ b/util/src/byte_ser.rs @@ -0,0 +1,384 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Simple serde byte array serializer, assumes target already +//! knows how to serialize itself into binary (because that all +//! this serializer can do) + +use serde::de::Visitor; +use serde::{de, ser, Deserialize, Serialize}; +use std; +use std::fmt::{self, Display}; + +pub type Result = std::result::Result; + +#[derive(Clone, Debug, PartialEq)] +pub enum Error { + Message(String), +} + +impl ser::Error for Error { + fn custom(msg: T) -> Self { + Error::Message(msg.to_string()) + } +} + +impl de::Error for Error { + fn custom(msg: T) -> Self { + Error::Message(msg.to_string()) + } +} + +impl Display for Error { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + match self { + Error::Message(msg) => formatter.write_str(msg), + } + } +} + +impl std::error::Error for Error {} + +pub struct ByteSerializer { + output: Vec, +} + +pub fn to_bytes(value: &T) -> Result> +where + T: Serialize, +{ + let mut serializer = ByteSerializer { output: vec![] }; + value.serialize(&mut serializer)?; + Ok(serializer.output) +} + +impl<'a> ser::Serializer for &'a mut ByteSerializer { + type Ok = (); + type Error = Error; + type SerializeSeq = Self; + type SerializeTuple = Self; + type SerializeTupleStruct = Self; + type SerializeTupleVariant = Self; + type SerializeMap = Self; + type SerializeStruct = Self; + type SerializeStructVariant = Self; + + fn serialize_bool(self, _: bool) -> Result<()> { + unimplemented!() + } + + fn serialize_i8(self, _: i8) -> Result<()> { + unimplemented!() + } + + fn serialize_i16(self, _: i16) -> Result<()> { + unimplemented!() + } + + fn serialize_i32(self, _: i32) -> Result<()> { + unimplemented!() + } + + fn serialize_i64(self, _: i64) -> Result<()> { + unimplemented!() + } + + fn serialize_u8(self, _: u8) -> Result<()> { + unimplemented!() + } + + fn serialize_u16(self, _: u16) -> Result<()> { + unimplemented!() + } + + fn serialize_u32(self, _: u32) -> Result<()> { + unimplemented!() + } + + fn serialize_u64(self, _: u64) -> Result<()> { + unimplemented!() + } + + fn serialize_f32(self, _: f32) -> Result<()> { + unimplemented!() + } + + fn serialize_f64(self, _: f64) -> Result<()> { + unimplemented!() + } + + fn serialize_char(self, _: char) -> Result<()> { + unimplemented!() + } + + fn serialize_str(self, _: &str) -> Result<()> { + unimplemented!() + } + + fn serialize_bytes(self, v: &[u8]) -> Result<()> { + for byte in v { + self.output.push(*byte) + } + Ok(()) + } + + fn serialize_none(self) -> Result<()> { + unimplemented!() + } + + fn serialize_some(self, _value: &T) -> Result<()> + where + T: ?Sized + Serialize, + { + unimplemented!() + } + + fn serialize_unit(self) -> Result<()> { + unimplemented!() + } + + fn serialize_unit_struct(self, _name: &'static str) -> Result<()> { + unimplemented!() + } + + fn serialize_unit_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + ) -> Result<()> { + unimplemented!() + } + + fn serialize_newtype_struct(self, _name: &'static str, _value: &T) -> Result<()> + where + T: ?Sized + Serialize, + { + unimplemented!() + } + + fn serialize_newtype_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _value: &T, + ) -> Result<()> + where + T: ?Sized + Serialize, + { + unimplemented!() + } + + fn serialize_seq(self, _len: Option) -> Result { + unimplemented!() + } + + fn serialize_tuple(self, _len: usize) -> Result { + unimplemented!() + } + + fn serialize_tuple_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + unimplemented!() + } + + fn serialize_tuple_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + unimplemented!() + } + + fn serialize_map(self, _len: Option) -> Result { + unimplemented!() + } + + fn serialize_struct(self, _name: &'static str, _len: usize) -> Result { + unimplemented!() + } + + fn serialize_struct_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + unimplemented!() + } +} + +impl<'a> ser::SerializeSeq for &'a mut ByteSerializer { + type Ok = (); + type Error = Error; + + fn serialize_element(&mut self, _value: &T) -> Result<()> + where + T: ?Sized + Serialize, + { + unimplemented!() + } + + fn end(self) -> Result<()> { + unimplemented!() + } +} + +impl<'a> ser::SerializeTuple for &'a mut ByteSerializer { + type Ok = (); + type Error = Error; + + fn serialize_element(&mut self, _value: &T) -> Result<()> + where + T: ?Sized + Serialize, + { + unimplemented!() + } + + fn end(self) -> Result<()> { + unimplemented!() + } +} + +impl<'a> ser::SerializeTupleStruct for &'a mut ByteSerializer { + type Ok = (); + type Error = Error; + + fn serialize_field(&mut self, _value: &T) -> Result<()> + where + T: ?Sized + Serialize, + { + unimplemented!() + } + + fn end(self) -> Result<()> { + unimplemented!() + } +} + +impl<'a> ser::SerializeTupleVariant for &'a mut ByteSerializer { + type Ok = (); + type Error = Error; + + fn serialize_field(&mut self, _value: &T) -> Result<()> + where + T: ?Sized + Serialize, + { + unimplemented!() + } + + fn end(self) -> Result<()> { + unimplemented!() + } +} + +impl<'a> ser::SerializeMap for &'a mut ByteSerializer { + type Ok = (); + type Error = Error; + + fn serialize_key(&mut self, _key: &T) -> Result<()> + where + T: ?Sized + Serialize, + { + unimplemented!() + } + + fn serialize_value(&mut self, _value: &T) -> Result<()> + where + T: ?Sized + Serialize, + { + unimplemented!() + } + + fn end(self) -> Result<()> { + unimplemented!() + } +} + +impl<'a> ser::SerializeStruct for &'a mut ByteSerializer { + type Ok = (); + type Error = Error; + + fn serialize_field(&mut self, _key: &'static str, _value: &T) -> Result<()> + where + T: ?Sized + Serialize, + { + unimplemented!() + } + + fn end(self) -> Result<()> { + unimplemented!() + } +} + +impl<'a> ser::SerializeStructVariant for &'a mut ByteSerializer { + type Ok = (); + type Error = Error; + + fn serialize_field(&mut self, _key: &'static str, _value: &T) -> Result<()> + where + T: ?Sized + Serialize, + { + unimplemented!() + } + + fn end(self) -> Result<()> { + unimplemented!() + } +} + +// Simple Deserializer + +pub struct ByteDeserializer<'de> { + input: &'de [u8], +} + +impl<'de> ByteDeserializer<'de> { + pub fn from_bytes(input: &'de [u8]) -> Self { + ByteDeserializer { input } + } +} + +pub fn from_bytes<'a, T>(b: &'a [u8]) -> Result +where + T: Deserialize<'a>, +{ + let mut deserializer = ByteDeserializer::from_bytes(b); + let t = T::deserialize(&mut deserializer)?; + Ok(t) +} + +impl<'de, 'a> de::Deserializer<'de> for &'a mut ByteDeserializer<'de> { + type Error = Error; + + fn deserialize_any(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + visitor.visit_bytes(self.input) + } + + serde::forward_to_deserialize_any! { + bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char str string + bytes byte_buf option unit unit_struct newtype_struct seq tuple + tuple_struct map struct enum identifier ignored_any + } +} diff --git a/util/src/lib.rs b/util/src/lib.rs new file mode 100644 index 0000000..f8e905a --- /dev/null +++ b/util/src/lib.rs @@ -0,0 +1,31 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Utilities and re-exports + +#![deny(non_upper_case_globals)] +#![deny(non_camel_case_types)] +#![deny(non_snake_case)] +#![deny(unused_mut)] +#![warn(missing_docs)] + +#[macro_use] +extern crate serde_derive; + +mod ov3; +pub use ov3::OnionV3Address; +pub use ov3::OnionV3Error as OnionV3AddressError; + +#[allow(missing_docs)] +pub mod byte_ser; diff --git a/util/src/ov3.rs b/util/src/ov3.rs new file mode 100644 index 0000000..8c16b3d --- /dev/null +++ b/util/src/ov3.rs @@ -0,0 +1,204 @@ +// Copyright 2021 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use data_encoding::BASE32; +use ed25519_dalek::PublicKey as DalekPublicKey; +use ed25519_dalek::SecretKey as DalekSecretKey; +use grin_util::from_hex; +use sha3::{Digest, Sha3_256}; +use std::convert::TryFrom; +use std::fmt; + +#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error, Serialize, Deserialize)] +/// OnionV3 Address Errors +pub enum OnionV3Error { + /// Error decoding an address from a string + #[error("Address Decoding: {0}")] + AddressDecoding(String), + /// Error with given private key + #[error("Invalid Private Key: {0}")] + InvalidPrivateKey(String), +} + +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +/// Struct to hold an onion V3 address, represented internally as a raw +/// ed25519 public key +pub struct OnionV3Address([u8; 32]); + +impl OnionV3Address { + /// from bytes + pub fn from_bytes(bytes: [u8; 32]) -> Self { + OnionV3Address(bytes) + } + + /// as bytes + pub fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } + + /// populate from a private key + pub fn from_private(key: &[u8; 32]) -> Result { + let d_skey = match DalekSecretKey::from_bytes(key) { + Ok(k) => k, + Err(e) => { + return Err(OnionV3Error::InvalidPrivateKey(format!( + "Unable to create public key: {}", + e + ))); + } + }; + let d_pub_key: DalekPublicKey = (&d_skey).into(); + Ok(OnionV3Address(*d_pub_key.as_bytes())) + } + + /// return dalek public key + pub fn to_ed25519(&self) -> Result { + let d_skey = match DalekPublicKey::from_bytes(&self.0) { + Ok(k) => k, + Err(e) => { + return Err(OnionV3Error::InvalidPrivateKey(format!( + "Unable to create dalek public key: {}", + e + ))); + } + }; + Ok(d_skey) + } + + /// Return as onion v3 address string + pub fn to_ov3_str(&self) -> String { + // calculate checksum + let mut hasher = Sha3_256::new(); + hasher.input(b".onion checksum"); + hasher.input(self.0); + hasher.input([0x03u8]); + let checksum = hasher.result(); + + let mut address_bytes = self.0.to_vec(); + address_bytes.push(checksum[0]); + address_bytes.push(checksum[1]); + address_bytes.push(0x03u8); + + let ret = BASE32.encode(&address_bytes); + ret.to_lowercase() + } + + /// return as http url + pub fn to_http_str(&self) -> String { + format!("http://{}.onion", self.to_ov3_str()) + } +} + +impl fmt::Display for OnionV3Address { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.to_ov3_str()) + } +} + +impl TryFrom<&str> for OnionV3Address { + type Error = OnionV3Error; + + fn try_from(input: &str) -> Result { + // First attempt to decode a pubkey from hex + if let Ok(b) = from_hex(input) { + if b.len() == 32 { + let mut retval = OnionV3Address([0; 32]); + retval.0.copy_from_slice(&b[0..32]); + return Ok(retval); + } else { + return Err(OnionV3Error::AddressDecoding( + "(Interpreted as Hex String) Public key is wrong length".to_owned(), + )); + } + }; + + // Otherwise try to parse as onion V3 address + let mut input = input.to_uppercase(); + if input.starts_with("HTTP://") || input.starts_with("HTTPS://") { + input = input.replace("HTTP://", ""); + input = input.replace("HTTPS://", ""); + } + if input.ends_with(".ONION") { + input = input.replace(".ONION", ""); + } + let orig_address_raw = input.clone(); + // for now, just check input is the right length and try and decode from base32 + if input.len() != 56 { + return Err(OnionV3Error::AddressDecoding( + "(Interpreted as Base32 String) Input address is wrong length".to_owned(), + )); + } + let address = match BASE32.decode(input.as_bytes()) { + Ok(a) => a, + Err(_) => { + return Err(OnionV3Error::AddressDecoding( + "(Interpreted as Base32 String) Input address is not base 32".to_owned(), + )); + } + }; + + let mut retval = OnionV3Address([0; 32]); + retval.0.copy_from_slice(&address[0..32]); + + let test_v3 = retval.to_ov3_str(); + if test_v3.to_uppercase() != orig_address_raw.to_uppercase() { + return Err(OnionV3Error::AddressDecoding( + "(Interpreted as Base32 String) Provided onion V3 address is invalid (no match)" + .to_owned(), + )); + } + + Ok(retval) + } +} + +#[cfg(test)] +mod test { + use super::*; + use std::convert::TryInto; + + #[test] + fn onion_v3() -> Result<(), OnionV3Error> { + let onion_address_str = "2a6at2obto3uvkpkitqp4wxcg6u36qf534eucbskqciturczzc5suyid"; + let onion_address: OnionV3Address = onion_address_str.try_into()?; + + println!("Onion address: {:?}", onion_address); + let raw_pubkey_str = "d03c09e9c19bb74aa9ea44e0fe5ae237a9bf40bddf0941064a80913a4459c8bb"; + let onion_address_2: OnionV3Address = raw_pubkey_str.try_into()?; + + assert_eq!(onion_address, onion_address_2); + + // invalid hex string, should be interpreted as base32 and fail + let raw_pubkey_str = "d03c09e9c19bb74aa9ea44e0fe5ae237a9bf40bddf0941064a80913a4459c8bx"; + let ret: Result = raw_pubkey_str.try_into(); + assert!(ret.is_err()); + + // wrong length hex string, should be interpreted as base32 and fail + let raw_pubkey_str = "d03c09e9c19bb74aa9ea44e0fe5ae237a9bf40bddf0941064a80913a4459c8bbff"; + let ret: Result = raw_pubkey_str.try_into(); + assert!(ret.is_err()); + + // wrong length ov3 string + let onion_address_str = "2a6at2obto3uvkpkitqp4wxcg6u36qf534eucbskqciturczzc5suyidx"; + let ret: Result = onion_address_str.try_into(); + assert!(ret.is_err()); + + // not base 32 ov3 string + let onion_address_str = "2a6at2obto3uvkpkitqp4wxcg6u36qf534eucbskqciturczzc5suyi-"; + let ret: Result = onion_address_str.try_into(); + assert!(ret.is_err()); + + Ok(()) + } +}