Normalize line endings
This commit is contained in:
@@ -1,31 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -1,20 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -1,73 +0,0 @@
|
||||
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
|
||||
@@ -1,33 +0,0 @@
|
||||
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
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
*.swp
|
||||
.DS_Store
|
||||
.grin*
|
||||
node*
|
||||
!node_clients
|
||||
!node_clients.rs
|
||||
target
|
||||
*/Cargo.lock
|
||||
*.iml
|
||||
grin.log
|
||||
wallet.seed
|
||||
test_output
|
||||
.idea/
|
||||
*.vs
|
||||
@@ -1,50 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,3 +0,0 @@
|
||||
# Code of Conduct
|
||||
|
||||
The Code of Conduct for this repository [can be found online](https://grin.mw/policies/code_of_conduct).
|
||||
Generated
-4626
File diff suppressed because it is too large
Load Diff
-79
@@ -1,79 +0,0 @@
|
||||
[package]
|
||||
name = "grin_wallet"
|
||||
version = "5.4.0-alpha.1"
|
||||
authors = ["Grin Developers <mimblewimble@lists.launchpad.net>"]
|
||||
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"
|
||||
@@ -1,177 +0,0 @@
|
||||
|
||||
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
|
||||
@@ -1,25 +0,0 @@
|
||||
[](https://github.com/mimblewimble/grin-wallet/actions/workflows/ci.yaml)
|
||||
[](https://codecov.io/gh/mimblewimble/grin-wallet)
|
||||
[](https://gitter.im/grin_community/Lobby)
|
||||
[](https://gitter.im/grin_community/support)
|
||||
[](https://docs.rs/releases/search?query=grin_wallet)
|
||||
[](https://github.com/mimblewimble/grin-wallet/releases)
|
||||
[](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
|
||||
@@ -1,57 +0,0 @@
|
||||
[package]
|
||||
name = "grin_wallet_api"
|
||||
version = "5.4.0-alpha.1"
|
||||
authors = ["Grin Developers <mimblewimble@lists.launchpad.net>"]
|
||||
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"
|
||||
@@ -1,515 +0,0 @@
|
||||
// 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<NodeVersionInfo>, 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<Mutex<Box<dyn WalletInst<'a, L, C, K>>>>,
|
||||
/// Flag to normalize some output during testing. Can mostly be ignored.
|
||||
pub doctest_mode: bool,
|
||||
/// foreign check middleware
|
||||
middleware: Option<ForeignCheckMiddleware>,
|
||||
/// Stored keychain mask (in case the stored wallet seed is tokenized)
|
||||
keychain_mask: Option<SecretKey>,
|
||||
/// Optional TOR configuration, holding address of sender and
|
||||
/// data directory
|
||||
tor_config: Mutex<Option<TorConfig>>,
|
||||
}
|
||||
|
||||
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<WalletInst<'static, DefaultLCProvider<HTTPNodeClient, ExtKeychain>, 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<Mutex<Box<dyn WalletInst<'a, L, C, K>>>>,
|
||||
keychain_mask: Option<SecretKey>,
|
||||
middleware: Option<ForeignCheckMiddleware>,
|
||||
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<TorConfig>) {
|
||||
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<VersionInfo, Error> {
|
||||
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<CbData, Error> {
|
||||
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<String>,
|
||||
) -> Result<Slate, Error> {
|
||||
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<Slate, Error> {
|
||||
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>,
|
||||
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));
|
||||
};
|
||||
}
|
||||
@@ -1,569 +0,0 @@
|
||||
// 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<VersionInfo, Error>;
|
||||
|
||||
/**
|
||||
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<VersionedCoinbase, Error>;
|
||||
|
||||
/**
|
||||
;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<String>,
|
||||
dest: Option<String>,
|
||||
) -> Result<VersionedSlate, Error>;
|
||||
|
||||
/**
|
||||
|
||||
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<VersionedSlate, Error>;
|
||||
}
|
||||
|
||||
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<VersionInfo, Error> {
|
||||
Foreign::check_version(self)
|
||||
}
|
||||
|
||||
fn build_coinbase(&self, block_fees: &BlockFees) -> Result<VersionedCoinbase, Error> {
|
||||
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<String>,
|
||||
dest: Option<String>,
|
||||
) -> Result<VersionedSlate, Error> {
|
||||
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<VersionedSlate, Error> {
|
||||
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<NodeVersionInfo>,
|
||||
_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<Option<serde_json::Value>, 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>,
|
||||
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::<LocalWalletClient>::new(client1.clone()).unwrap())
|
||||
as Box<
|
||||
dyn WalletInst<
|
||||
'static,
|
||||
DefaultLCProvider<LocalWalletClient, ExtKeychain>,
|
||||
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::<LocalWalletClient>::new(client2.clone()).unwrap())
|
||||
as Box<
|
||||
dyn WalletInst<
|
||||
'static,
|
||||
DefaultLCProvider<LocalWalletClient, ExtKeychain>,
|
||||
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()
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
// 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,
|
||||
};
|
||||
-2643
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,340 +0,0 @@
|
||||
// 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<SecretKey>,
|
||||
}
|
||||
|
||||
/// 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<Self, Error> {
|
||||
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<Value, Error> {
|
||||
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<String, Error> {
|
||||
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<Value, Error> {
|
||||
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<Self, Error> {
|
||||
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<Value, Error> {
|
||||
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<String, Error> {
|
||||
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<Value, Error> {
|
||||
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<String, EncryptedBody>,
|
||||
}
|
||||
|
||||
impl EncryptedResponse {
|
||||
/// from json
|
||||
pub fn from_json(id: &JsonId, json_in: &Value, enc_key: &SecretKey) -> Result<Self, Error> {
|
||||
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<Value, Error> {
|
||||
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<String, Error> {
|
||||
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<Value, Error> {
|
||||
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(())
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
// 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(_) => (),
|
||||
}
|
||||
}
|
||||
}*/
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,45 +0,0 @@
|
||||
[package]
|
||||
name = "grin_wallet_config"
|
||||
version = "5.4.0-alpha.1"
|
||||
authors = ["Grin Developers <mimblewimble@lists.launchpad.net>"]
|
||||
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"
|
||||
|
||||
@@ -1,396 +0,0 @@
|
||||
// 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<String, String> {
|
||||
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::<Vec<&str>>()[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<u32>,
|
||||
) -> 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::<Vec<String>>();
|
||||
|
||||
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::<Vec<(String, String)>>();
|
||||
|
||||
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
|
||||
}
|
||||
@@ -1,478 +0,0 @@
|
||||
// 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<PathBuf, ConfigError> {
|
||||
// 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<PathBuf>,
|
||||
chain_type: &global::ChainTypes,
|
||||
) -> Result<PathBuf, ConfigError> {
|
||||
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<PathBuf> {
|
||||
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<PathBuf>,
|
||||
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<PathBuf>,
|
||||
create_path: bool,
|
||||
) -> Result<GlobalWalletConfig, ConfigError> {
|
||||
// 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<GlobalWalletConfig, ConfigError> {
|
||||
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<GlobalWalletConfig, ConfigError> {
|
||||
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<GlobalWalletConfigMembers, toml::de::Error> = 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<String, ConfigError> {
|
||||
let encoded: Result<String, toml::ser::Error> =
|
||||
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<String>,
|
||||
old_version: Option<u32>,
|
||||
) -> 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<String, ConfigError> {
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
// 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,
|
||||
};
|
||||
@@ -1,273 +0,0 @@
|
||||
// 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<ChainTypes>,
|
||||
/// 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<u16>,
|
||||
/// Location of the secret for basic auth on the Owner API
|
||||
pub api_secret_path: Option<String>,
|
||||
/// Location of the node api secret for basic auth on the Grin API
|
||||
pub node_api_secret_path: Option<String>,
|
||||
/// 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<bool>,
|
||||
/// 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<bool>,
|
||||
/// TLS certificate file
|
||||
pub tls_certificate_file: Option<String>,
|
||||
/// TLS certificate private key file
|
||||
pub tls_certificate_key: Option<String>,
|
||||
/// 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<bool>,
|
||||
/// Scaling factor from transaction weight to transaction fee
|
||||
/// should match accept_fee_base parameter in grin-server
|
||||
pub accept_fee_base: Option<u64>,
|
||||
}
|
||||
|
||||
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<io::Error> 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<bool>,
|
||||
/// 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<String>,
|
||||
/// Client Option
|
||||
pub client_option: Option<String>,
|
||||
}
|
||||
|
||||
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<String>,
|
||||
/// ip or dns
|
||||
pub address: Option<String>,
|
||||
/// user for auth - socks5|https(s)
|
||||
pub username: Option<String>,
|
||||
/// pass for auth - socks5|https(s)
|
||||
pub password: Option<String>,
|
||||
/// allowed port - proxy
|
||||
pub allowed_port: Option<Vec<u16>>,
|
||||
}
|
||||
|
||||
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<PathBuf>,
|
||||
/// Wallet members
|
||||
pub members: Option<GlobalWalletConfigMembers>,
|
||||
}
|
||||
|
||||
/// 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<u32>,
|
||||
/// Wallet configuration
|
||||
#[serde(default)]
|
||||
pub wallet: WalletConfig,
|
||||
/// Tor config
|
||||
pub tor: Option<TorConfig>,
|
||||
/// Logging config
|
||||
pub logging: Option<LoggingConfig>,
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
[package]
|
||||
name = "grin_wallet_controller"
|
||||
version = "5.4.0-alpha.1"
|
||||
authors = ["Grin Developers <mimblewimble@lists.launchpad.net>"]
|
||||
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"}
|
||||
|
||||
#####
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,875 +0,0 @@
|
||||
// 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<NodeVersionInfo>,
|
||||
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<L, C, K>(
|
||||
wallet: Arc<Mutex<Box<dyn WalletInst<'static, L, C, K> + 'static>>>,
|
||||
keychain_mask: Arc<Mutex<Option<SecretKey>>>,
|
||||
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<String, String> = 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<String, String> = 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<L, F, C, K>(
|
||||
wallet: Option<Arc<Mutex<Box<dyn WalletInst<'static, L, C, K>>>>>,
|
||||
keychain_mask: Option<&SecretKey>,
|
||||
api_context: Option<&mut Owner<L, C, K>>,
|
||||
f: F,
|
||||
) -> Result<(), Error>
|
||||
where
|
||||
L: WalletLCProvider<'static, C, K> + 'static,
|
||||
F: FnOnce(&mut Owner<L, C, K>, 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<Mutex<Box<dyn WalletInst<'a, L, C, K>>>>,
|
||||
keychain_mask: Option<SecretKey>,
|
||||
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<L, C, K>(
|
||||
wallet: Arc<Mutex<Box<dyn WalletInst<'static, L, C, K> + 'static>>>,
|
||||
keychain_mask: Arc<Mutex<Option<SecretKey>>>,
|
||||
addr: &str,
|
||||
api_secret: Option<String>,
|
||||
tls_config: Option<TLSConfig>,
|
||||
owner_api_include_foreign: Option<bool>,
|
||||
tor_config: Option<TorConfig>,
|
||||
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<L, C, K>(
|
||||
wallet: Arc<Mutex<Box<dyn WalletInst<'static, L, C, K> + 'static>>>,
|
||||
keychain_mask: Arc<Mutex<Option<SecretKey>>>,
|
||||
addr: &str,
|
||||
tls_config: Option<TLSConfig>,
|
||||
use_tor: bool,
|
||||
test_mode: bool,
|
||||
tor_config: Option<TorConfig>,
|
||||
) -> 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<L, C, K>
|
||||
where
|
||||
L: WalletLCProvider<'static, C, K> + 'static,
|
||||
C: NodeClient + 'static,
|
||||
K: Keychain + 'static,
|
||||
{
|
||||
/// Wallet instance
|
||||
pub wallet: Arc<Mutex<Box<dyn WalletInst<'static, L, C, K> + 'static>>>,
|
||||
|
||||
/// Handle to Owner API
|
||||
owner_api: Arc<Owner<L, C, K>>,
|
||||
|
||||
/// ECDH shared key
|
||||
pub shared_key: Arc<Mutex<Option<SecretKey>>>,
|
||||
|
||||
/// Keychain mask (to change if also running the foreign API)
|
||||
pub keychain_mask: Arc<Mutex<Option<SecretKey>>>,
|
||||
|
||||
/// 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<Mutex<Option<SecretKey>>>) -> 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<Mutex<Option<SecretKey>>>,
|
||||
) -> 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<Mutex<Option<SecretKey>>>,
|
||||
val: &serde_json::Value,
|
||||
new_key: Option<SecretKey>,
|
||||
) {
|
||||
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<Mutex<Option<SecretKey>>>, 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<Mutex<Option<SecretKey>>>,
|
||||
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<Mutex<Option<SecretKey>>>,
|
||||
id: &JsonId,
|
||||
res: &serde_json::Value,
|
||||
) -> Result<serde_json::Value, serde_json::Value> {
|
||||
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<HashMap<String, String>, serde_json::Error> =
|
||||
serde_json::from_value(val["result"]["Err"].clone());
|
||||
retval = match hashed {
|
||||
Err(e) => {
|
||||
debug!("Can't cast value to Hashmap<String> {}", 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<HashMap<String, serde_json::Value>, serde_json::Error> =
|
||||
serde_json::from_value(val["result"]["Err"].clone());
|
||||
retval = match hashed {
|
||||
Err(e) => {
|
||||
debug!("Can't cast value to Hashmap<Value> {}", 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::<String>(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<L, C, K> OwnerAPIHandlerV3<L, C, K>
|
||||
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<Mutex<Box<dyn WalletInst<'static, L, C, K> + 'static>>>,
|
||||
keychain_mask: Arc<Mutex<Option<SecretKey>>>,
|
||||
tor_config: Option<TorConfig>,
|
||||
running_foreign: bool,
|
||||
) -> OwnerAPIHandlerV3<L, C, K> {
|
||||
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<Body>,
|
||||
key: Arc<Mutex<Option<SecretKey>>>,
|
||||
mask: Arc<Mutex<Option<SecretKey>>>,
|
||||
running_foreign: bool,
|
||||
api: Arc<Owner<L, C, K>>,
|
||||
) -> Result<serde_json::Value, Error> {
|
||||
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 <dyn OwnerRpc>::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<Body>,
|
||||
key: Arc<Mutex<Option<SecretKey>>>,
|
||||
mask: Arc<Mutex<Option<SecretKey>>>,
|
||||
running_foreign: bool,
|
||||
api: Arc<Owner<L, C, K>>,
|
||||
) -> Result<Response<Body>, Error> {
|
||||
let res = Self::call_api(req, key, mask, running_foreign, api).await?;
|
||||
Ok(json_response_pretty(&res))
|
||||
}
|
||||
}
|
||||
|
||||
impl<L, C, K> api::Handler for OwnerAPIHandlerV3<L, C, K>
|
||||
where
|
||||
L: WalletLCProvider<'static, C, K> + 'static,
|
||||
C: NodeClient + 'static,
|
||||
K: Keychain + 'static,
|
||||
{
|
||||
fn post(&self, req: Request<Body>) -> 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<Body>) -> ResponseFuture {
|
||||
Box::pin(async { Ok(create_ok_response("{}")) })
|
||||
}
|
||||
}
|
||||
/// V2 API Handler/Wrapper for foreign functions
|
||||
pub struct ForeignAPIHandlerV2<L, C, K>
|
||||
where
|
||||
L: WalletLCProvider<'static, C, K> + 'static,
|
||||
C: NodeClient + 'static,
|
||||
K: Keychain + 'static,
|
||||
{
|
||||
/// Wallet instance
|
||||
pub wallet: Arc<Mutex<Box<dyn WalletInst<'static, L, C, K> + 'static>>>,
|
||||
/// Keychain mask
|
||||
pub keychain_mask: Arc<Mutex<Option<SecretKey>>>,
|
||||
/// run in doctest mode
|
||||
pub test_mode: bool,
|
||||
/// tor config
|
||||
pub tor_config: Mutex<Option<TorConfig>>,
|
||||
}
|
||||
|
||||
impl<L, C, K> ForeignAPIHandlerV2<L, C, K>
|
||||
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<Mutex<Box<dyn WalletInst<'static, L, C, K> + 'static>>>,
|
||||
keychain_mask: Arc<Mutex<Option<SecretKey>>>,
|
||||
test_mode: bool,
|
||||
tor_config: Mutex<Option<TorConfig>>,
|
||||
) -> ForeignAPIHandlerV2<L, C, K> {
|
||||
ForeignAPIHandlerV2 {
|
||||
wallet,
|
||||
keychain_mask,
|
||||
test_mode,
|
||||
tor_config,
|
||||
}
|
||||
}
|
||||
|
||||
async fn call_api(
|
||||
req: Request<Body>,
|
||||
api: Foreign<'static, L, C, K>,
|
||||
) -> Result<serde_json::Value, Error> {
|
||||
let val: serde_json::Value = parse_body(req).await?;
|
||||
match <dyn ForeignRpc>::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<Body>,
|
||||
mask: Option<SecretKey>,
|
||||
wallet: Arc<Mutex<Box<dyn WalletInst<'static, L, C, K> + 'static>>>,
|
||||
test_mode: bool,
|
||||
tor_config: Option<TorConfig>,
|
||||
) -> Result<Response<Body>, 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<L, C, K> api::Handler for ForeignAPIHandlerV2<L, C, K>
|
||||
where
|
||||
L: WalletLCProvider<'static, C, K> + 'static,
|
||||
C: NodeClient + 'static,
|
||||
K: Keychain + 'static,
|
||||
{
|
||||
fn post(&self, req: Request<Body>) -> 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<Body>) -> 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<T>(s: &T) -> Response<Body>
|
||||
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<T>(s: &T) -> Response<Body>
|
||||
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<Body> {
|
||||
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<Body> {
|
||||
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<T: Into<Body>>(status: StatusCode, text: T) -> Response<Body> {
|
||||
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<T>(req: Request<Body>) -> Result<T, Error>
|
||||
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)))
|
||||
}
|
||||
@@ -1,630 +0,0 @@
|
||||
// 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<OutputCommitMapping>,
|
||||
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<AcctPathMapping>) {
|
||||
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(())
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
// 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),
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
// 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;
|
||||
@@ -1,276 +0,0 @@
|
||||
// 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);
|
||||
}
|
||||
@@ -1,171 +0,0 @@
|
||||
// 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
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
// 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);
|
||||
}
|
||||
@@ -1,878 +0,0 @@
|
||||
// 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<libwallet::OutputData> =
|
||||
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);
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
// 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<DefaultLCProvider<LocalWalletClient, ExtKeychain>, LocalWalletClient, ExtKeychain>
|
||||
{
|
||||
WalletProxy::new(test_dir)
|
||||
}
|
||||
|
||||
pub fn create_local_wallet(
|
||||
test_dir: &str,
|
||||
name: &str,
|
||||
mnemonic: Option<ZeroingString>,
|
||||
client: LocalWalletClient,
|
||||
create_mask: bool,
|
||||
) -> (
|
||||
Arc<
|
||||
Mutex<
|
||||
Box<
|
||||
dyn WalletInst<
|
||||
'static,
|
||||
DefaultLCProvider<'static, LocalWalletClient, ExtKeychain>,
|
||||
LocalWalletClient,
|
||||
ExtKeychain,
|
||||
>,
|
||||
>,
|
||||
>,
|
||||
>,
|
||||
Option<SecretKey>,
|
||||
) {
|
||||
let mut wallet = Box::new(DefaultWalletImpl::<LocalWalletClient>::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<SecretKey>,
|
||||
) {
|
||||
let mut wallet = Box::new(DefaultWalletImpl::<LocalWalletClient>::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)
|
||||
}
|
||||
@@ -1,320 +0,0 @@
|
||||
// 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);
|
||||
}
|
||||
@@ -1,305 +0,0 @@
|
||||
// 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(())
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
// 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);
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
// 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);
|
||||
}
|
||||
@@ -1,212 +0,0 @@
|
||||
// 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);
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
// 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);
|
||||
}
|
||||
@@ -1,268 +0,0 @@
|
||||
// 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);
|
||||
}
|
||||
@@ -1,379 +0,0 @@
|
||||
// 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<chain::Chain>,
|
||||
Arc<AtomicBool>,
|
||||
u64,
|
||||
u64,
|
||||
Transaction,
|
||||
Wallet,
|
||||
Option<SecretKey>,
|
||||
Wallet,
|
||||
Option<SecretKey>,
|
||||
),
|
||||
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);
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
// 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);
|
||||
}
|
||||
@@ -1,587 +0,0 @@
|
||||
// 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<SlatepackAddress>,
|
||||
recipients: Vec<SlatepackAddress>,
|
||||
) -> 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);
|
||||
}
|
||||
@@ -1,601 +0,0 @@
|
||||
// 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);
|
||||
}
|
||||
@@ -1,185 +0,0 @@
|
||||
// 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);
|
||||
}
|
||||
@@ -1,360 +0,0 @@
|
||||
// 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);
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
// 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);
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
# Grin Wallet + Library Design
|
||||
|
||||

|
||||
|
||||
## 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<T: ?Sized, C, K>(
|
||||
!·wallet: &mut T,
|
||||
!·show_spent: bool,
|
||||
!·tx_id: Option<u32>,
|
||||
) -> Result<Vec<OutputData>, Error>
|
||||
where
|
||||
!·T: WalletBackend<C, K>,
|
||||
!·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.
|
||||
@@ -1,82 +0,0 @@
|
||||
|
||||
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.
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 113 KiB |
@@ -1,110 +0,0 @@
|
||||
@startuml grin-wallet-overview
|
||||
skinparam componentStyle uml2
|
||||
|
||||
[Grin Node] as grin_node
|
||||
|
||||
folder "Provided by Grin" as services {
|
||||
component foreign_api [
|
||||
**Foreign API**
|
||||
External-Facing functions
|
||||
- receive_tx, build coinbase
|
||||
]
|
||||
|
||||
component owner_api [
|
||||
**Owner API**
|
||||
Functions used by wallet owner only
|
||||
- retrieve outputs, retrieve txs,
|
||||
get balances, send, etc. . .
|
||||
|
||||
]
|
||||
component libtx [
|
||||
**Transaction Library (libTx)**
|
||||
Lower-Level transaction functions
|
||||
- Build transaction (via Slate), sign,
|
||||
build reward, fees, etc. . .
|
||||
]
|
||||
component libwallet [
|
||||
**Wallet Library (libWallet) **
|
||||
- Higher level wallet functions (select coins,
|
||||
update wallet from node, etc)
|
||||
- Service Controller
|
||||
(instantiate libs, start listeners)
|
||||
]
|
||||
() "Owner HTTP Listener (localhost only)" as owner_http
|
||||
() "Foreign HTTP Listener" as foreign_http
|
||||
() "Owner Single-Use" as owner_single
|
||||
() "Foreign Single-Use" as foreign_single
|
||||
}
|
||||
|
||||
' Trait definitions
|
||||
package "Traits Implemented by Wallets" as traits {
|
||||
database "WalletBackend" as wallet_backend
|
||||
database "KeyChain" as keychain
|
||||
component "NodeClient" as wallet_client
|
||||
}
|
||||
|
||||
note left of wallet_client
|
||||
- Communication layer implementation
|
||||
- Handles underlying communication with grin node
|
||||
or other wallets
|
||||
- HTTP implementation provided currently, (Other,
|
||||
more secure protocols possible.)
|
||||
end note
|
||||
|
||||
note bottom of keychain
|
||||
- Handles all key derivation operations
|
||||
end note
|
||||
|
||||
note bottom of wallet_backend
|
||||
- Implements underlying storage for wallet data
|
||||
- LMDB storage provided in default client, others
|
||||
possible (Flat-file, other DBs, etc)
|
||||
end note
|
||||
|
||||
libtx <--> 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
|
||||
-115
@@ -1,115 +0,0 @@
|
||||
{
|
||||
"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=="
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
# 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!
|
||||
@@ -1,134 +0,0 @@
|
||||
/* 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<iterations*2; i+=2) {
|
||||
let info_response = await new JSONRequestEncrypted(i, 'retrieve_summary_info', {
|
||||
"token": token,
|
||||
"refresh_from_node": true,
|
||||
"minimum_confirmations": 1,
|
||||
}).send(shared_key)
|
||||
|
||||
console.log("Info Response: ", info_response);
|
||||
await sleep(2000)
|
||||
|
||||
let txs_response = await new JSONRequestEncrypted(i+1, 'retrieve_txs', {
|
||||
"token": token,
|
||||
"refresh_from_node": true,
|
||||
"tx_id": null,
|
||||
"tx_slate_id": null,
|
||||
}).send(shared_key)
|
||||
|
||||
console.log("Txs Response: ", txs_response);
|
||||
await sleep(2000)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
main();
|
||||
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
# Wallet TLS setup
|
||||
|
||||
## What you need
|
||||
* A server with a static IP address (eg `3.3.3.3`)
|
||||
* A domain name ownership (`example.com`)
|
||||
* DNS configuration for this IP (`grin1.example.com` -> `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"
|
||||
```
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 154 KiB |
@@ -1,97 +0,0 @@
|
||||
@startuml grin-transaction
|
||||
|
||||
title
|
||||
**Current Grin Tranaction Workflow**
|
||||
Accurate as of Oct 10, 2018 - Master branch only
|
||||
end title
|
||||
|
||||
actor "Sender" as sender
|
||||
actor "Recipient" as recipient
|
||||
entity "Grin Node" as grin_node
|
||||
|
||||
== Round 1 ==
|
||||
|
||||
note left of sender
|
||||
1: Create Transaction **UUID** (for reference and maintaining correct state)
|
||||
2: Set **lock_height** for transaction kernel (current chain height)
|
||||
3: Select **inputs** using desired selection strategy
|
||||
4: Calculate sum **inputs** blinding factors **xI**
|
||||
5: Create **change_output**
|
||||
6: Select blinding factor **xC** for **change_output**
|
||||
7: Create lock function **sF** that locks **inputs** and stores **change_output** in wallet
|
||||
and identifying wallet transaction log entry **TS** linking **inputs + outputs**
|
||||
(Not executed at this point)
|
||||
end note
|
||||
note left of sender
|
||||
8: Calculate **tx_weight**: MAX(-1 * **num_inputs** + 4 * (**num_change_outputs** + 1), 1)
|
||||
(+1 covers a single output on the receiver's side)
|
||||
9: Calculate **fee**: **tx_weight** * 1_000_000 nG
|
||||
10: Calculate total blinding excess sum for all inputs and outputs **xS1** = **xC** - **xI** (private scalar)
|
||||
11: Select a random nonce **kS** (private scalar)
|
||||
12: Subtract random kernel offset **oS** from **xS1**. Calculate **xS** = **xS1** - **oS**
|
||||
13: Multiply **xS** and **kS** by generator G to create public curve points **xSG** and **kSG**
|
||||
14: Add values to **Slate** for passing to other participants: **UUID, inputs, change_outputs,**
|
||||
**fee, amount, lock_height, kSG, xSG, oS**
|
||||
end note
|
||||
sender -> 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
|
||||
@@ -1,81 +0,0 @@
|
||||
[package]
|
||||
name = "grin_wallet_impls"
|
||||
version = "5.4.0-alpha.1"
|
||||
authors = ["Grin Developers <mimblewimble@lists.launchpad.net>"]
|
||||
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"
|
||||
@@ -1,73 +0,0 @@
|
||||
// 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::<VersionedBinSlate>(&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))
|
||||
}
|
||||
}
|
||||
@@ -1,292 +0,0 @@
|
||||
// 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<SocketAddr>,
|
||||
tor_config_dir: String,
|
||||
process: Option<Arc<tor_process::TorProcess>>,
|
||||
bridge: TorBridgeConfig,
|
||||
proxy: TorProxyConfig,
|
||||
}
|
||||
|
||||
impl HttpSlateSender {
|
||||
/// Create, return Err if scheme is not "http"
|
||||
fn new(base_url: &str) -> Result<HttpSlateSender, SchemeNotHttp> {
|
||||
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<HttpSlateSender, SchemeNotHttp> {
|
||||
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<String, String> = 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<String, String> = 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<SlateVersion, Error> {
|
||||
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<String> =
|
||||
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<IN>(
|
||||
&self,
|
||||
url: &str,
|
||||
api_secret: Option<String>,
|
||||
input: IN,
|
||||
) -> Result<String, ClientError>
|
||||
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<Slate, Error> {
|
||||
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<Error> for SchemeNotHttp {
|
||||
fn into(self) -> Error {
|
||||
let err_str = "url scheme must be http".to_string();
|
||||
Error::GenericError(err_str)
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
// 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<Slate, Error>;
|
||||
}
|
||||
|
||||
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<String>,
|
||||
) -> 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>;
|
||||
}
|
||||
@@ -1,179 +0,0 @@
|
||||
// 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<Vec<u8>, 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<Slatepack, Error> {
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -1,767 +0,0 @@
|
||||
// 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<K>(
|
||||
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<K>,
|
||||
/// Check value for XORed keychain seed
|
||||
pub master_checksum: Box<Option<Blake2bResult>>,
|
||||
/// Parent path to use by default for output operations
|
||||
parent_key_id: Identifier,
|
||||
/// wallet to node client
|
||||
w2n_client: C,
|
||||
///phantom
|
||||
_phantom: &'ck PhantomData<C>,
|
||||
}
|
||||
|
||||
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<Self, Error> {
|
||||
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::<C, K>::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::<C, K>::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<K>,
|
||||
mask: bool,
|
||||
use_test_rng: bool,
|
||||
) -> Result<Option<SecretKey>, 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<K, Error> {
|
||||
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<Option<String>, 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<u64>) -> Result<OutputData, Error> {
|
||||
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<dyn Iterator>
|
||||
fn iter<'a>(&'a self) -> Box<dyn Iterator<Item = OutputData> + '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<Option<TxLogEntry>, 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<dyn Iterator>
|
||||
fn tx_log_iter<'a>(&'a self) -> Box<dyn Iterator<Item = TxLogEntry> + '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<Context, 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(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<dyn Iterator>
|
||||
fn acct_path_iter<'a>(&'a self) -> Box<dyn Iterator<Item = AcctPathMapping> + '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<Option<AcctPathMapping>, 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<Option<Transaction>, 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<Box<dyn WalletOutputBatch<K> + '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<Box<dyn WalletOutputBatch<K> + '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<u32, Error> {
|
||||
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<Identifier, Error> {
|
||||
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<u64, Error> {
|
||||
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<ScannedBlockInfo, Error> {
|
||||
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<WalletInitStatus, Error> {
|
||||
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<Option<store::Batch<'a>>>,
|
||||
/// Keychain
|
||||
keychain: Option<K>,
|
||||
}
|
||||
|
||||
#[allow(missing_docs)]
|
||||
impl<'a, C, K> WalletOutputBatch<K> 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<u64>) -> Result<OutputData, Error> {
|
||||
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<dyn Iterator>
|
||||
fn iter(&self) -> Box<dyn Iterator<Item = OutputData>> {
|
||||
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<u64>) -> 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<u32, Error> {
|
||||
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<dyn Iterator>
|
||||
fn tx_log_iter(&self) -> Box<dyn Iterator<Item = TxLogEntry>> {
|
||||
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<dyn Iterator>
|
||||
fn acct_path_iter(&self) -> Box<dyn Iterator<Item = AcctPathMapping>> {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
// 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};
|
||||
@@ -1,289 +0,0 @@
|
||||
// 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<Mutex<Runtime>> = 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, Error> {
|
||||
Self::build(None)
|
||||
}
|
||||
|
||||
pub fn with_socks_proxy(socks_proxy_addr: SocketAddr) -> Result<Self, Error> {
|
||||
Self::build(Some(socks_proxy_addr))
|
||||
}
|
||||
|
||||
fn build(socks_proxy_addr: Option<SocketAddr>) -> Result<Self, Error> {
|
||||
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<String>) -> Result<T, Error>
|
||||
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<String>,
|
||||
) -> Result<T, Error>
|
||||
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<String>) -> 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<IN, OUT>(
|
||||
&self,
|
||||
url: &str,
|
||||
api_secret: Option<String>,
|
||||
input: &IN,
|
||||
) -> Result<OUT, Error>
|
||||
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<IN, OUT>(
|
||||
&self,
|
||||
url: &str,
|
||||
input: &IN,
|
||||
api_secret: Option<String>,
|
||||
) -> Result<OUT, Error>
|
||||
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<IN>(
|
||||
&self,
|
||||
url: &str,
|
||||
api_secret: Option<String>,
|
||||
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<IN>(
|
||||
&self,
|
||||
url: &str,
|
||||
api_secret: Option<String>,
|
||||
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<String>,
|
||||
body: Option<String>,
|
||||
) -> Result<RequestBuilder, Error> {
|
||||
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<IN>(
|
||||
&self,
|
||||
url: &str,
|
||||
api_secret: Option<String>,
|
||||
input: &IN,
|
||||
) -> Result<RequestBuilder, Error>
|
||||
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<T>(&self, req: RequestBuilder) -> Result<T, Error>
|
||||
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<T>(&self, req: RequestBuilder) -> Result<T, Error>
|
||||
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<String, Error> {
|
||||
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<String, Error> {
|
||||
// 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,264 +0,0 @@
|
||||
// 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<serde_json::Value>,
|
||||
/// An error if there is one, or null
|
||||
pub error: Option<RpcError>,
|
||||
/// 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<String>,
|
||||
}
|
||||
|
||||
impl Response {
|
||||
/// Extract the result from a response
|
||||
pub fn result<T: serde::de::DeserializeOwned>(&self) -> Result<T, Error> {
|
||||
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<T: serde::de::DeserializeOwned>(self) -> Result<T, Error> {
|
||||
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<serde_json::Error> for Error {
|
||||
fn from(e: serde_json::Error) -> Error {
|
||||
Error::Json(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RpcError> 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<serde_json::Value>,
|
||||
}
|
||||
|
||||
/// Create a standard error responses
|
||||
pub fn _standard_error(code: StandardError, data: Option<serde_json::Value>) -> 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<serde_json::Value, RpcError>,
|
||||
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")),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
// 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};
|
||||
@@ -1,104 +0,0 @@
|
||||
// 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),
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
// 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<Self, Error> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1,395 +0,0 @@
|
||||
// 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<Box<dyn WalletBackend<'a, C, K> + '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<String, Error> {
|
||||
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<WalletConfig>,
|
||||
logging_config: Option<LoggingConfig>,
|
||||
tor_config: Option<TorConfig>,
|
||||
) -> 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<ZeroingString>,
|
||||
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<Option<SecretKey>, 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<bool, 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();
|
||||
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<ZeroingString, 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.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<dyn WalletBackend<'a, C, K> + '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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
// 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;
|
||||
@@ -1,385 +0,0 @@
|
||||
// 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<u8>);
|
||||
|
||||
impl WalletSeed {
|
||||
pub fn from_bytes(bytes: &[u8]) -> WalletSeed {
|
||||
WalletSeed(bytes.to_vec())
|
||||
}
|
||||
|
||||
pub fn from_mnemonic(word_list: util::ZeroingString) -> Result<WalletSeed, Error> {
|
||||
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<WalletSeed, Error> {
|
||||
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<String, Error> {
|
||||
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<u8> {
|
||||
let seed = blake2::blake2b::blake2b(64, password.as_bytes(), &old_wallet_seed);
|
||||
seed.as_bytes().to_vec()
|
||||
}
|
||||
|
||||
pub fn derive_keychain<K: Keychain>(&self, is_testnet: bool) -> Result<K, Error> {
|
||||
let result = K::from_seed(&self.0, is_testnet)?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn init_new(
|
||||
seed_length: usize,
|
||||
test_mode: bool,
|
||||
password: Option<util::ZeroingString>,
|
||||
) -> WalletSeed {
|
||||
let mut seed: Vec<u8> = 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<bool, Error> {
|
||||
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<String, Error> {
|
||||
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<util::ZeroingString>,
|
||||
password: util::ZeroingString,
|
||||
test_mode: bool,
|
||||
) -> Result<WalletSeed, Error> {
|
||||
// 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<WalletSeed, Error> {
|
||||
// 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<EncryptedWalletSeed, Error> {
|
||||
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<WalletSeed, Error> {
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -1,436 +0,0 @@
|
||||
// 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<String>,
|
||||
node_version_info: Option<NodeVersionInfo>,
|
||||
}
|
||||
|
||||
impl HTTPNodeClient {
|
||||
/// Create a new client that will communicate with the given grin node
|
||||
pub fn new(
|
||||
node_url: &str,
|
||||
node_api_secret: Option<String>,
|
||||
) -> Result<HTTPNodeClient, libwallet::Error> {
|
||||
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<D: serde::de::DeserializeOwned>(
|
||||
&self,
|
||||
method: &str,
|
||||
params: &serde_json::Value,
|
||||
) -> Result<D, libwallet::Error> {
|
||||
let url = format!("{}{}", self.node_url(), ENDPOINT);
|
||||
let req = build_request(method, params);
|
||||
let res = self
|
||||
.client
|
||||
.post::<Request, Response>(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<String> {
|
||||
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<String>) {
|
||||
self.node_api_secret = node_api_secret;
|
||||
}
|
||||
|
||||
fn get_version_info(&mut self) -> Option<NodeVersionInfo> {
|
||||
if let Some(v) = self.node_version_info.as_ref() {
|
||||
return Some(v.clone());
|
||||
}
|
||||
let retval = match self
|
||||
.send_json_request::<GetVersionResp>("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::<serde_json::Value>("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::<GetTipResp>("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<u64>,
|
||||
max_height: Option<u64>,
|
||||
) -> Result<Option<(TxKernel, u64, u64)>, 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::<Request, Response>(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::<LocatedTxKernel>() {
|
||||
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<pedersen::Commitment>,
|
||||
) -> Result<HashMap<pedersen::Commitment, (String, u64, u64)>, libwallet::Error> {
|
||||
// build a map of api outputs by commit so we can look them up efficiently
|
||||
let mut api_outputs: HashMap<pedersen::Commitment, (String, u64, u64)> = HashMap::new();
|
||||
|
||||
if wallet_outputs.is_empty() {
|
||||
return Ok(api_outputs);
|
||||
}
|
||||
|
||||
// build vec of commits for inclusion in query
|
||||
let query_params: Vec<String> = 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::<usize>() {
|
||||
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::<Request, Response>(
|
||||
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<Vec<_>, _> =
|
||||
std::thread::spawn(move || rt.lock().unwrap().block_on(async move { task.await }))
|
||||
.join()
|
||||
.unwrap();
|
||||
|
||||
let results: Vec<OutputPrintable> = match res {
|
||||
Ok(resps) => {
|
||||
let mut results = vec![];
|
||||
for r in resps {
|
||||
match r.into_result::<Vec<OutputPrintable>>() {
|
||||
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<u64>,
|
||||
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::<OutputListing>("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<u64>,
|
||||
) -> Result<(u64, u64), libwallet::Error> {
|
||||
let params = json!([start_height, end_height]);
|
||||
let res = self.send_json_request::<OutputListing>("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);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
// 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;
|
||||
@@ -1,31 +0,0 @@
|
||||
// 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,
|
||||
}
|
||||
@@ -1,276 +0,0 @@
|
||||
// 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<api::Output> {
|
||||
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<chain::Chain>,
|
||||
excess: &pedersen::Commitment,
|
||||
min_height: Option<u64>,
|
||||
max_height: Option<u64>,
|
||||
) -> Option<api::LocatedTxKernel> {
|
||||
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<chain::Chain>,
|
||||
start_index: u64,
|
||||
end_index: Option<u64>,
|
||||
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<chain::Chain>,
|
||||
start_index: u64,
|
||||
end_index: Option<u64>,
|
||||
) -> 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<Mutex<Box<dyn WalletInst<'a, L, C, K> + 'a>>>,
|
||||
keychain_mask: Option<&SecretKey>,
|
||||
) -> Result<core::core::Block, libwallet::Error>
|
||||
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<Mutex<Box<dyn WalletInst<'a, L, C, K> + '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<Mutex<Box<dyn WalletInst<'a, L, C, K> + '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<Mutex<Box<dyn WalletInst<'a, L, C, K>>>>,
|
||||
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<Mutex<Box<dyn WalletInst<'a, L, C, K>>>>,
|
||||
keychain_mask: Option<&SecretKey>,
|
||||
) -> Result<WalletInfo, libwallet::Error>
|
||||
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)
|
||||
}
|
||||
@@ -1,644 +0,0 @@
|
||||
// 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<Chain>,
|
||||
/// list of interested wallets
|
||||
pub wallets: HashMap<
|
||||
String,
|
||||
(
|
||||
Sender<WalletProxyMessage>,
|
||||
Arc<Mutex<Box<dyn WalletInst<'a, L, C, K> + 'a>>>,
|
||||
Option<SecretKey>,
|
||||
),
|
||||
>,
|
||||
/// simulate json send to another client
|
||||
/// address, method, payload (simulate HTTP request)
|
||||
pub tx: Sender<WalletProxyMessage>,
|
||||
/// simulate json receiving
|
||||
pub rx: Receiver<WalletProxyMessage>,
|
||||
/// queue control
|
||||
pub running: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
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<WalletProxyMessage>,
|
||||
wallet: Arc<Mutex<Box<dyn WalletInst<'a, L, C, K> + 'a>>>,
|
||||
keychain_mask: Option<SecretKey>,
|
||||
) {
|
||||
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<WalletProxyMessage, libwallet::Error> {
|
||||
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<WalletProxyMessage, libwallet::Error> {
|
||||
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<WalletProxyMessage, libwallet::Error> {
|
||||
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<WalletProxyMessage, libwallet::Error> {
|
||||
let split = m.body.split(',');
|
||||
//let mut api_outputs: HashMap<pedersen::Commitment, String> = HashMap::new();
|
||||
let mut outputs: Vec<api::Output> = 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<WalletProxyMessage, libwallet::Error> {
|
||||
let split = m.body.split(',').collect::<Vec<&str>>();
|
||||
let start_index = std::cmp::max(split[0].parse::<u64>().unwrap(), 1);
|
||||
let max = split[1].parse::<u64>().unwrap();
|
||||
let end_index = split[2].parse::<u64>().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<WalletProxyMessage, libwallet::Error> {
|
||||
let split = m.body.split(',').collect::<Vec<&str>>();
|
||||
let start_index = split[0].parse::<u64>().unwrap();
|
||||
let end_index = split[1].parse::<u64>().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<WalletProxyMessage, libwallet::Error> {
|
||||
let split = m.body.split(',').collect::<Vec<&str>>();
|
||||
let excess = split[0].parse::<String>().unwrap();
|
||||
let min = split[1].parse::<u64>().unwrap();
|
||||
let max = split[2].parse::<u64>().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<Mutex<Sender<WalletProxyMessage>>>,
|
||||
/// my rx queue
|
||||
pub rx: Arc<Mutex<Receiver<WalletProxyMessage>>>,
|
||||
/// my tx queue
|
||||
pub tx: Arc<Mutex<Sender<WalletProxyMessage>>>,
|
||||
}
|
||||
|
||||
impl LocalWalletClient {
|
||||
/// new
|
||||
pub fn new(id: &str, proxy_rx: Sender<WalletProxyMessage>) -> 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<WalletProxyMessage> {
|
||||
self.tx.lock().clone()
|
||||
}
|
||||
|
||||
/// Send the slate to a listening wallet instance
|
||||
pub fn send_tx_slate_direct(
|
||||
&self,
|
||||
dest: &str,
|
||||
slate: &Slate,
|
||||
) -> Result<Slate, libwallet::Error> {
|
||||
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<String> {
|
||||
None
|
||||
}
|
||||
fn set_node_url(&mut self, _node_url: &str) {}
|
||||
fn set_node_api_secret(&mut self, _node_api_secret: Option<String>) {}
|
||||
fn get_version_info(&mut self) -> Option<NodeVersionInfo> {
|
||||
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::<String>().map_err(|_| {
|
||||
libwallet::Error::ClientCallback("Parsing get_height response".to_owned())
|
||||
})?;
|
||||
let split: Vec<&str> = res.split(',').collect();
|
||||
Ok((split[0].parse::<u64>().unwrap(), split[1].to_owned()))
|
||||
}
|
||||
|
||||
/// Retrieve outputs from node
|
||||
fn get_outputs_from_node(
|
||||
&self,
|
||||
wallet_outputs: Vec<pedersen::Commitment>,
|
||||
) -> Result<HashMap<pedersen::Commitment, (String, u64, u64)>, libwallet::Error> {
|
||||
let query_params: Vec<String> = 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<api::Output> = serde_json::from_str(&m.body).unwrap();
|
||||
let mut api_outputs: HashMap<pedersen::Commitment, (String, u64, u64)> = 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<u64>,
|
||||
max_height: Option<u64>,
|
||||
) -> Result<Option<(TxKernel, u64, u64)>, 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<LocatedTxKernel> = 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<u64>,
|
||||
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<u64>,
|
||||
) -> 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,
|
||||
{
|
||||
}
|
||||
@@ -1,660 +0,0 @@
|
||||
// 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<Self::Item> {
|
||||
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<String>,
|
||||
/// server address
|
||||
pub server: Option<String>,
|
||||
/// fingerprint
|
||||
pub fingerprint: Option<String>,
|
||||
/// certificate (obfs4)
|
||||
pub cert: Option<String>,
|
||||
/// IAT obfuscation: 0 disabled, 1 enabled, 2 paranoid (obfs4)
|
||||
pub iatmode: Option<String>,
|
||||
/// URL of signaling broker (meek)
|
||||
pub url: Option<String>,
|
||||
/// optional - front domain (meek)
|
||||
pub front: Option<String>,
|
||||
/// optional - URL of AMP cache to use as a proxy for signaling (meek)
|
||||
pub utls: Option<String>,
|
||||
/// optional - HPKP disable argument. (meek)
|
||||
pub disablehpkp: Option<String>,
|
||||
}
|
||||
|
||||
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<String, Error> {
|
||||
match arg {
|
||||
Some(addr) => {
|
||||
let address = addr.parse::<SocketAddr>().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<Option<String>, 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<String, Error> {
|
||||
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<String, Error> {
|
||||
let iatmode = arg.parse::<u8>().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<String, Error> {
|
||||
let max = arg.parse::<bool>().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<String>,
|
||||
// Plugin client option
|
||||
pub option: Option<String>,
|
||||
}
|
||||
|
||||
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<String, Error> {
|
||||
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<String, Error> {
|
||||
let url = arg
|
||||
.parse::<Url>()
|
||||
.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<String, Error> {
|
||||
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<String, Error> {
|
||||
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<String, Error> {
|
||||
match arg.parse::<u16>() {
|
||||
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<String, Error> {
|
||||
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<String, Error> {
|
||||
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<HashMap<String, String>, 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<TorBridgeConfig> for TorBridge {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(tbc: TorBridgeConfig) -> Result<Self, Self::Error> {
|
||||
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::<Vec<&str>>();
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,382 +0,0 @@
|
||||
// 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<TorRcConfigItem>,
|
||||
}
|
||||
|
||||
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<OnionV3Address, Error> {
|
||||
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<String, String>,
|
||||
hm_tor_proxy: HashMap<String, String>,
|
||||
) -> 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<String, String>,
|
||||
hm_tor_proxy: HashMap<String, String>,
|
||||
) -> 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<String, String>,
|
||||
hm_tor_proxy: HashMap<String, String>,
|
||||
) -> 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<String, Error> {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
// 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;
|
||||
@@ -1,288 +0,0 @@
|
||||
// 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<String>),
|
||||
InvalidLogLine,
|
||||
InvalidBootstrapLine(String),
|
||||
Regex(regex::Error),
|
||||
ProcessNotStarted,
|
||||
Timeout,
|
||||
}
|
||||
|
||||
pub struct TorProcess {
|
||||
tor_cmd: String,
|
||||
args: Vec<String>,
|
||||
torrc_path: Option<String>,
|
||||
completion_percent: u8,
|
||||
timeout: u32,
|
||||
working_dir: Option<String>,
|
||||
pub stdout: Option<BufReader<ChildStdout>>,
|
||||
pub process: Option<Child>,
|
||||
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<String>) -> &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::<i32>()
|
||||
.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<ChildStdout>,
|
||||
completion_perc: u8,
|
||||
) -> Result<BufReader<ChildStdout>, Error> {
|
||||
let re_bootstrap = Regex::new(r"^\[notice\] Bootstrapped (?P<perc>[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::<u8>().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(());
|
||||
}
|
||||
}
|
||||
@@ -1,191 +0,0 @@
|
||||
// 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<String>,
|
||||
/// Proxy address for the proxy, eg IP:PORT or Hostname
|
||||
pub address: Option<String>,
|
||||
/// Username for the proxy authentification
|
||||
pub username: Option<String>,
|
||||
/// Password for the proxy authentification
|
||||
pub password: Option<String>,
|
||||
/// computer goes through a firewall that only allows connections to certain ports
|
||||
pub allowed_port: Option<Vec<u16>>,
|
||||
}
|
||||
|
||||
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<String>), Error> {
|
||||
let host: String;
|
||||
let str_port: Option<String>;
|
||||
let address = addr
|
||||
.chars()
|
||||
.filter(|c| !c.is_whitespace())
|
||||
.collect::<String>();
|
||||
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<u16>), 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::<u16>()
|
||||
.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<HashMap<String, String>, 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<TorProxyConfig> for TorProxy {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(tb: TorProxyConfig) -> Result<Self, Self::Error> {
|
||||
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 <IP:PORT> or <Hostname>",
|
||||
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()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
[package]
|
||||
name = "grin_integration"
|
||||
version = "1.1.0"
|
||||
authors = ["Grin Developers <mimblewimble@lists.launchpad.net>"]
|
||||
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" }
|
||||
@@ -1,21 +0,0 @@
|
||||
// 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)]
|
||||
@@ -1,485 +0,0 @@
|
||||
// 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<api::Tip, Error> {
|
||||
let url = format!("http://{}:{}/v1/chain", base_addr, api_server_port);
|
||||
api::client::get::<api::Tip>(url.as_str(), None).map_err(|e| Error::API(e))
|
||||
}
|
||||
|
||||
// Status handler function
|
||||
fn get_status(base_addr: &String, api_server_port: u16) -> Result<api::Status, Error> {
|
||||
let url = format!("http://{}:{}/v1/status", base_addr, api_server_port);
|
||||
api::client::get::<api::Status>(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<api::BlockPrintable, Error> {
|
||||
let url = format!(
|
||||
"http://{}:{}/v1/blocks/{}",
|
||||
base_addr, api_server_port, height
|
||||
);
|
||||
api::client::get::<api::BlockPrintable>(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<api::CompactBlockPrintable, Error> {
|
||||
let url = format!(
|
||||
"http://{}:{}/v1/blocks/{}?compact",
|
||||
base_addr, api_server_port, height
|
||||
);
|
||||
api::client::get::<api::CompactBlockPrintable>(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<api::BlockPrintable, Error> {
|
||||
let url = format!(
|
||||
"http://{}:{}/v1/blocks/{}",
|
||||
base_addr, api_server_port, block_hash
|
||||
);
|
||||
api::client::get::<api::BlockPrintable>(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<api::CompactBlockPrintable, Error> {
|
||||
let url = format!(
|
||||
"http://{}:{}/v1/blocks/{}?compact",
|
||||
base_addr, api_server_port, block_hash
|
||||
);
|
||||
api::client::get::<api::CompactBlockPrintable>(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<String>,
|
||||
) -> Result<Vec<api::Output>, Error> {
|
||||
let url = format!(
|
||||
"http://{}:{}/v1/chain/outputs/byids?id={}",
|
||||
base_addr,
|
||||
api_server_port,
|
||||
ids.join(",")
|
||||
);
|
||||
api::client::get::<Vec<api::Output>>(url.as_str(), None).map_err(|e| Error::API(e))
|
||||
}
|
||||
|
||||
fn get_outputs_by_ids2(
|
||||
base_addr: &String,
|
||||
api_server_port: u16,
|
||||
ids: Vec<String>,
|
||||
) -> Result<Vec<api::Output>, 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::<Vec<api::Output>>(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<Vec<api::BlockOutputs>, 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::<Vec<api::BlockOutputs>>(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<api::TxHashSet, Error> {
|
||||
let url = format!(
|
||||
"http://{}:{}/v1/txhashset/roots",
|
||||
base_addr, api_server_port
|
||||
);
|
||||
api::client::get::<api::TxHashSet>(url.as_str(), None).map_err(|e| Error::API(e))
|
||||
}
|
||||
|
||||
fn get_txhashset_lastoutputs(
|
||||
base_addr: &String,
|
||||
api_server_port: u16,
|
||||
n: u64,
|
||||
) -> Result<Vec<api::TxHashSetNode>, 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::<Vec<api::TxHashSetNode>>(url.as_str(), None).map_err(|e| Error::API(e))
|
||||
}
|
||||
|
||||
fn get_txhashset_lastrangeproofs(
|
||||
base_addr: &String,
|
||||
api_server_port: u16,
|
||||
n: u64,
|
||||
) -> Result<Vec<api::TxHashSetNode>, 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::<Vec<api::TxHashSetNode>>(url.as_str(), None).map_err(|e| Error::API(e))
|
||||
}
|
||||
|
||||
fn get_txhashset_lastkernels(
|
||||
base_addr: &String,
|
||||
api_server_port: u16,
|
||||
n: u64,
|
||||
) -> Result<Vec<api::TxHashSetNode>, 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::<Vec<api::TxHashSetNode>>(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<api::BlockOutputs>) -> Vec<String> {
|
||||
let mut ids: Vec<String> = 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<p2p::PeerData, Error> {
|
||||
let url = format!(
|
||||
"http://{}:{}/v1/peers/{}",
|
||||
base_addr, api_server_port, peer_addr
|
||||
);
|
||||
api::client::get::<p2p::PeerData>(url.as_str(), None).map_err(|e| Error::API(e))
|
||||
}
|
||||
|
||||
pub fn get_connected_peers(
|
||||
base_addr: &String,
|
||||
api_server_port: u16,
|
||||
) -> Result<Vec<p2p::types::PeerInfoDisplay>, Error> {
|
||||
let url = format!(
|
||||
"http://{}:{}/v1/peers/connected",
|
||||
base_addr, api_server_port
|
||||
);
|
||||
api::client::get::<Vec<p2p::types::PeerInfoDisplay>>(url.as_str(), None)
|
||||
.map_err(|e| Error::API(e))
|
||||
}
|
||||
|
||||
pub fn get_all_peers(
|
||||
base_addr: &String,
|
||||
api_server_port: u16,
|
||||
) -> Result<Vec<p2p::PeerData>, Error> {
|
||||
let url = format!("http://{}:{}/v1/peers/all", base_addr, api_server_port);
|
||||
api::client::get::<Vec<p2p::PeerData>>(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),
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
// 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);
|
||||
}
|
||||
@@ -1,682 +0,0 @@
|
||||
// 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<servers::ServerStats>,
|
||||
|
||||
// The API server instance
|
||||
api_server: Option<api::ApiServer>,
|
||||
|
||||
// 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<String>,
|
||||
|
||||
// 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<LocalServerContainer, Error> {
|
||||
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<Mutex<LMDBBackend<HTTPNodeClient, keychain::ExtKeychain>>> {
|
||||
// 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<HTTPNodeClient, keychain::ExtKeychain> =
|
||||
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<LocalServerContainer>,
|
||||
|
||||
// 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<Mutex<Vec<servers::Server>>> {
|
||||
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<String> = 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<Mutex<Vec<servers::Server>>>) {
|
||||
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,
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,177 +0,0 @@
|
||||
// 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.");
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
[package]
|
||||
name = "grin_wallet_libwallet"
|
||||
version = "5.4.0-alpha.1"
|
||||
authors = ["Grin Developers <mimblewimble@lists.launchpad.net>"]
|
||||
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"}
|
||||
|
||||
#####
|
||||
@@ -1,49 +0,0 @@
|
||||
// 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<K>(
|
||||
keychain: &K,
|
||||
parent_key_id: &Identifier,
|
||||
index: u32,
|
||||
) -> Result<SecretKey, Error>
|
||||
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()[..],
|
||||
)?)
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
// 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;
|
||||
@@ -1,233 +0,0 @@
|
||||
// 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<CbData, Error>
|
||||
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<Slate, Error>
|
||||
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<Slate, Error>
|
||||
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)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,146 +0,0 @@
|
||||
// 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<StatusMessage>,
|
||||
queue: Arc<Mutex<Vec<StatusMessage>>>,
|
||||
) -> 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<Mutex<Box<dyn WalletInst<'a, L, C, K>>>>,
|
||||
is_running: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
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<Mutex<Box<dyn WalletInst<'a, L, C, K>>>>,
|
||||
is_running: Arc<AtomicBool>,
|
||||
) -> 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<SecretKey>,
|
||||
status_send_channel: &Option<Sender<StatusMessage>>,
|
||||
) -> 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(())
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user