diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 000000000..98b5c3744 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,4 @@ +[target.x86_64-pc-windows-msvc] +rustflags = ["-Ctarget-feature=+crt-static"] +[target.i686-pc-windows-msvc] +rustflags = ["-Ctarget-feature=+crt-static"] diff --git a/.github/workflows/cd.yaml b/.github/workflows/cd.yaml index 956814e3d..611000934 100644 --- a/.github/workflows/cd.yaml +++ b/.github/workflows/cd.yaml @@ -10,7 +10,10 @@ jobs: name: Linux Release runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - name: Checkout + uses: actions/checkout@v6 + with: + submodules: recursive - name: Build run: cargo build --release - name: Archive @@ -32,7 +35,9 @@ jobs: runs-on: macos-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v6 + with: + submodules: recursive - name: Build run: cargo build --release - name: Archive @@ -53,7 +58,9 @@ jobs: runs-on: windows-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v6 + with: + submodules: recursive - name: Build run: cargo build --release - name: Archive diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 783258e94..989bfb6d7 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -9,7 +9,9 @@ jobs: matrix: job_args: [api, config, controller, impls, libwallet, .] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 + with: + submodules: recursive - name: Test ${{ matrix.job_args }} working-directory: ${{ matrix.job_args }} run: cargo test --release @@ -19,7 +21,9 @@ jobs: runs-on: macos-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v6 + with: + submodules: recursive - name: Tests run: cargo test --release --all @@ -28,6 +32,8 @@ jobs: runs-on: windows-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v6 + with: + submodules: recursive - name: Tests run: cargo test --release --all \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..e974e8386 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "grin"] + path = grin + branch = staging + url = https://github.com/mimblewimble/grin diff --git a/.hooks/pre-commit b/.hooks/pre-commit old mode 100644 new mode 100755 diff --git a/Cargo.lock b/Cargo.lock index a342a8a18..1f4930ff8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,18 +4,18 @@ version = 4 [[package]] name = "addr2line" -version = "0.24.2" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" dependencies = [ "gimli", ] [[package]] name = "adler2" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aead" @@ -26,28 +26,42 @@ dependencies = [ "generic-array 0.14.7", ] +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if 1.0.4", + "cipher 0.4.4", + "cpufeatures 0.2.17", + "zeroize", +] + [[package]] name = "age" -version = "0.7.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23100453ca2a1bbda9bfc6deac1bebb828d7e66ba481ebccfedfddf29321b6b9" +checksum = "caf6575916925dce67e5cc5588066ec50db90be641b5de10e2495d1a4045d55c" dependencies = [ "age-core", "base64 0.13.1", "bech32 0.8.1", "chacha20poly1305", + "cipher 0.4.4", "cookie-factory", "hkdf", - "hmac 0.11.0", + "hmac 0.12.1", "i18n-embed", "i18n-embed-fl", "lazy_static", "nom", "pin-project", "rand 0.7.3", - "rand 0.8.5", + "rand 0.8.6", "rust-embed", "scrypt", + "sha2 0.10.9", "sha2 0.9.9", "subtle", "x25519-dalek 1.1.1", @@ -56,51 +70,83 @@ dependencies = [ [[package]] name = "age-core" -version = "0.7.1" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70afa630ef12a4fc666277713efbe6da2bc87bb3f3af0f1149415b701362c615" +checksum = "00a5c8d8a33abc74ad393896a6305351dd159d0e184788f4729e3c80e397fa45" dependencies = [ "base64 0.13.1", "chacha20poly1305", "cookie-factory", "hkdf", + "io_tee", "nom", - "rand 0.8.5", - "secrecy 0.8.0", - "sha2 0.9.9", + "rand 0.8.6", + "secrecy", + "sha2 0.10.9", ] [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] [[package]] -name = "android-tzdata" -version = "0.1.1" +name = "amplify" +version = "4.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" +checksum = "3f7fb4ac7c881e54a8e7015e399b6112a2a5bc958b6c89ac510840ff20273b31" +dependencies = [ + "amplify_derive", + "amplify_num", + "ascii", + "getrandom 0.2.17", + "getrandom 0.3.4", + "wasm-bindgen", +] [[package]] -name = "android_system_properties" -version = "0.1.5" +name = "amplify_derive" +version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +checksum = "2a6309e6b8d89b36b9f959b7a8fa093583b94922a0f6438a24fb08936de4d428" dependencies = [ - "libc", + "amplify_syn", + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 1.0.109", ] [[package]] -name = "ansi_term" -version = "0.11.0" +name = "amplify_num" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afed304556696656d2d71495e1e5f2c4b524a3fb6eb0f2f3778ffc482a40b8a8" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "amplify_syn" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7736fb8d473c0d83098b5bac44df6a561e20470375cd8bcae30516dc889fd62a" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 1.0.109", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" dependencies = [ - "winapi 0.3.9", + "libc", ] [[package]] @@ -109,20 +155,23 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" dependencies = [ - "winapi 0.3.9", + "winapi", ] [[package]] name = "anyhow" -version = "1.0.92" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74f37166d7d48a0284b99dd824694c26119c700b53bf0d1540cdb147dbdaaf13" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "arc-swap" -version = "1.7.1" +version = "1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" +dependencies = [ + "rustversion", +] [[package]] name = "arrayref" @@ -147,32 +196,199 @@ checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" [[package]] name = "arrayvec" -version = "0.7.6" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "arti-client" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d54b6c87059a9b6562bfe585f3a637b72f90cddaa327ac29b5ddf082a54b4" +dependencies = [ + "async-trait", + "cfg-if 1.0.4", + "derive-deftly", + "derive_builder_fork_arti", + "derive_more", + "educe", + "fs-mistrust", + "futures 0.3.32", + "hostname-validator", + "humantime 2.4.0", + "humantime-serde", + "libc", + "once_cell", + "postage", + "rand 0.10.2", + "safelog", + "serde", + "tempfile", + "thiserror 2.0.18", + "time", + "tor-async-utils", + "tor-basic-utils", + "tor-chanmgr", + "tor-circmgr", + "tor-config", + "tor-config-path", + "tor-dircommon", + "tor-dirmgr", + "tor-error", + "tor-guardmgr", + "tor-hsclient", + "tor-hscrypto", + "tor-hsservice", + "tor-keymgr", + "tor-linkspec", + "tor-llcrypto", + "tor-memquota", + "tor-netdir", + "tor-netdoc", + "tor-persist", + "tor-proto", + "tor-protover", + "tor-ptmgr", + "tor-rtcompat", + "tracing", + "void", + "web-time-compat", +] + +[[package]] +name = "ascii" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" + +[[package]] +name = "asn1-rs" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits 0.2.19", + "rusticata-macros", + "thiserror 2.0.18", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "assert_matches" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" + +[[package]] +name = "async-compression" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79b3f8a79cccc2898f31920fc69f304859b3bd567490f75ebf51ae1c792a9ac" +dependencies = [ + "compression-codecs", + "compression-core", + "futures-io", + "pin-project-lite", +] + +[[package]] +name = "async-native-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37dd6b179962fe4048a6f81d4c0d7ed419a21fdf49204b4c6b04971693358e79" +dependencies = [ + "futures-util", + "native-tls", + "thiserror 2.0.18", + "url", +] + +[[package]] +name = "async-trait" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", +] [[package]] -name = "async-stream" -version = "0.3.6" +name = "async_executors" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +checksum = "a982d2f86de6137cc05c9db9a915a19886c97911f9790d04f174cede74be01a5" dependencies = [ - "async-stream-impl", + "blanket", "futures-core", - "pin-project-lite 0.2.15", + "futures-task", + "futures-util", + "pin-project", + "rustc_version", + "tokio", ] [[package]] -name = "async-stream-impl" -version = "0.3.6" +name = "asynchronous-codec" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a860072022177f903e59730004fb5dc13db9275b79bb2aef7ba8ce831956c233" +dependencies = [ + "bytes 1.12.0", + "futures-sink", + "futures-util", + "memchr", + "pin-project-lite", +] + +[[package]] +name = "atomic" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" + +[[package]] +name = "atomic" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" dependencies = [ - "proc-macro2 1.0.89", - "quote 1.0.37", - "syn 2.0.86", + "bytemuck", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "atty" version = "0.2.14" @@ -181,7 +397,7 @@ checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ "hermit-abi 0.1.19", "libc", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -190,30 +406,36 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dde43e75fd43e8a1bf86103336bc699aa8d17ad1be60c76c0bdfd4828e19b78" dependencies = [ - "autocfg 1.4.0", + "autocfg 1.5.1", ] [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "backtrace" -version = "0.3.74" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" dependencies = [ "addr2line", - "cfg-if 1.0.0", + "cfg-if 1.0.4", "libc", "miniz_oxide", "object", "rustc-demangle", - "windows-targets", + "windows-link", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.9.3" @@ -242,17 +464,23 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "base64ct" -version = "1.6.0" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] name = "basic-toml" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "823388e228f614e9558c6804262db37960ec8821856535f5c3f59913140558f8" +checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" dependencies = [ "serde", ] @@ -269,6 +497,25 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf9ff0bbfd639f15c74af777d81383cf53efb7c93613f6cab67c6c11e05bbf8b" +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bincode" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" +dependencies = [ + "serde", + "unty", +] + [[package]] name = "bit-vec" version = "0.6.3" @@ -277,21 +524,30 @@ checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" [[package]] name = "bitflags" -version = "0.9.1" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4efd02e230a02e18f92fc2735f44597385ed02ad8f831e7c1c1156ee5e1ab3a5" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "1.3.2" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +dependencies = [ + "serde_core", +] [[package]] -name = "bitflags" -version = "2.6.0" +name = "bitvec" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] [[package]] name = "blake2-rfc" @@ -314,6 +570,17 @@ dependencies = [ "constant_time_eq", ] +[[package]] +name = "blanket" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0b121a9fe0df916e362fb3271088d071159cdf11db0e4182d02152850756eff" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", +] + [[package]] name = "block-buffer" version = "0.7.3" @@ -359,20 +626,46 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "476e9cd489f9e121e02ffa6014a8ef220ecb15c05ed23fc34cca13925dc283fb" +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bstr" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cee35f73844aa3014bb606320a6c1f010249dbdf43342fe54b5a4f6a8ed4b79" +dependencies = [ + "memchr", + "regex-automata", + "serde_core", +] + [[package]] name = "built" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64" +checksum = "5c0e531d93d39c34eef561e929e8a7f86d77a5af08aac4f6d6e39976c51858e9" dependencies = [ "git2", ] [[package]] name = "bumpalo" -version = "3.16.0" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "by_address" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" [[package]] name = "byte-tools" @@ -380,6 +673,12 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + [[package]] name = "byteorder" version = "1.5.0" @@ -388,31 +687,27 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "0.4.12" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c" -dependencies = [ - "byteorder", - "iovec", -] +checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" [[package]] name = "bytes" -version = "0.5.6" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" [[package]] -name = "bytes" -version = "1.8.0" +name = "caret" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" +checksum = "2bb840a063107846780a14acc022b55bf6c174997a393ffde8b58ff6630c1b56" [[package]] name = "cc" -version = "1.2.56" +version = "1.2.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +checksum = "f5d6cac793997bd970000024b2934968efe83b382de4fdcf4fcb46b6ee4ad996" dependencies = [ "find-msvc-tools", "jobserver", @@ -428,9 +723,9 @@ checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chacha20" @@ -438,12 +733,23 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c80e5460aa66fe3b91d40bcbdab953a597b60053e34d684ac6903f863b680a6" dependencies = [ - "cfg-if 1.0.0", - "cipher", - "cpufeatures", + "cfg-if 1.0.4", + "cipher 0.3.0", + "cpufeatures 0.2.17", "zeroize", ] +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if 1.0.4", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + [[package]] name = "chacha20poly1305" version = "0.9.1" @@ -451,25 +757,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a18446b09be63d457bbec447509e85f662f32952b035ce892290396bc0b0cff5" dependencies = [ "aead", - "chacha20", - "cipher", + "chacha20 0.8.2", + "cipher 0.3.0", "poly1305", "zeroize", ] [[package]] name = "chrono" -version = "0.4.38" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ - "android-tzdata", "iana-time-zone", "js-sys", "num-traits 0.2.19", "serde", "wasm-bindgen", - "windows-targets", + "windows-link", ] [[package]] @@ -481,13 +786,24 @@ dependencies = [ "generic-array 0.14.7", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common 0.1.7", + "inout", + "zeroize", +] + [[package]] name = "clap" version = "2.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" dependencies = [ - "ansi_term 0.12.1", + "ansi_term", "atty", "bitflags 1.3.2", "strsim 0.8.0", @@ -497,6 +813,17 @@ dependencies = [ "yaml-rust", ] +[[package]] +name = "clipboard-win" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7191c27c2357d9b7ef96baac1773290d4ca63b24205b82a3fd8a0637afcf0362" +dependencies = [ + "error-code", + "str-buf", + "winapi", +] + [[package]] name = "cloudabi" version = "0.0.3" @@ -507,52 +834,131 @@ dependencies = [ ] [[package]] -name = "constant_time_eq" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" - -[[package]] -name = "cookie-factory" -version = "0.3.3" +name = "coarsetime" +version = "0.1.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2" +checksum = "e58eb270476aa4fc7843849f8a35063e8743b4dbcdf6dd0f8ea0886980c204c2" dependencies = [ - "futures 0.3.31", + "libc", + "wasix", + "wasm-bindgen", ] [[package]] -name = "core-foundation" -version = "0.9.4" +name = "compression-codecs" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf" dependencies = [ - "core-foundation-sys", - "libc", + "compression-core", + "flate2", + "liblzma", + "zstd", + "zstd-safe", ] [[package]] -name = "core-foundation-sys" -version = "0.8.7" +name = "compression-core" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" [[package]] -name = "cpufeatures" -version = "0.2.14" +name = "concurrent-queue" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" dependencies = [ - "libc", + "crossbeam-utils", ] [[package]] -name = "crc32fast" -version = "1.4.2" +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cookie-factory" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2" +dependencies = [ + "futures 0.3.32", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ - "cfg-if 1.0.0", + "cfg-if 1.0.4", ] [[package]] @@ -576,9 +982,9 @@ dependencies = [ [[package]] name = "crossbeam-deque" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -593,27 +999,57 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" -version = "0.8.20" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array 0.14.7", + "rand_core 0.6.4", + "subtle", + "zeroize", +] [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array 0.14.7", "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + [[package]] name = "crypto-mac" -version = "0.11.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" +checksum = "25fab6889090c8133f3deb8f73ba3c65a7f456f66436fc012a1b1e272b1e103e" dependencies = [ "generic-array 0.14.7", "subtle", @@ -621,43 +1057,42 @@ dependencies = [ [[package]] name = "csv" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe" +checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" dependencies = [ "csv-core", - "itoa 1.0.11", + "itoa", "ryu", - "serde", + "serde_core", ] [[package]] name = "csv-core" -version = "0.1.11" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70" +checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" dependencies = [ "memchr", ] [[package]] -name = "ctor" -version = "0.1.26" +name = "ctr" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" dependencies = [ - "quote 1.0.37", - "syn 1.0.109", + "cipher 0.4.4", ] [[package]] name = "curve25519-dalek" -version = "2.1.3" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a9b85542f99a2dfa2a1b8e192662741c9859a846b296bef1c92ef9b58b5a216" +checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" dependencies = [ "byteorder", - "digest 0.8.1", + "digest 0.9.0", "rand_core 0.5.1", "subtle", "zeroize", @@ -665,3698 +1100,6708 @@ dependencies = [ [[package]] name = "curve25519-dalek" -version = "3.2.0" +version = "4.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ - "byteorder", - "digest 0.9.0", - "rand_core 0.5.1", + "cfg-if 1.0.4", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "digest 0.10.7", + "fiat-crypto 0.2.9", + "rustc_version", "subtle", "zeroize", ] [[package]] -name = "dashmap" -version = "5.5.3" +name = "curve25519-dalek" +version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +checksum = "b5eed333089e2e1c1ac8c6c0398e5e2497b4c9926ca6d0365ed1e099afa5bc23" dependencies = [ - "cfg-if 1.0.0", - "hashbrown 0.14.5", - "lock_api 0.4.12", - "once_cell", - "parking_lot_core 0.9.10", + "cfg-if 1.0.4", + "cpufeatures 0.3.0", + "curve25519-dalek-derive", + "fiat-crypto 0.3.0", + "rustc_version", + "subtle", + "zeroize", ] [[package]] -name = "data-encoding" -version = "2.6.0" +name = "curve25519-dalek-derive" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", +] [[package]] -name = "derivative" -version = "2.2.0" +name = "darling" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" dependencies = [ - "proc-macro2 1.0.89", - "quote 1.0.37", - "syn 1.0.109", + "darling_core 0.13.4", + "darling_macro 0.13.4", ] [[package]] -name = "destructure_traitobject" -version = "0.2.0" +name = "darling" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c877555693c14d2f84191cfd3ad8582790fc52b5e2274b40b59cf5f5cea25c7" +checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" +dependencies = [ + "darling_core 0.14.4", + "darling_macro 0.14.4", +] [[package]] -name = "difference" -version = "2.0.0" +name = "darling" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core 0.21.3", + "darling_macro 0.21.3", +] [[package]] -name = "digest" -version = "0.8.1" +name = "darling" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ - "generic-array 0.12.4", + "darling_core 0.23.0", + "darling_macro 0.23.0", ] [[package]] -name = "digest" -version = "0.9.0" +name = "darling_core" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" dependencies = [ - "generic-array 0.14.7", + "fnv", + "ident_case", + "proc-macro2 1.0.106", + "quote 1.0.46", + "strsim 0.10.0", + "syn 1.0.109", ] [[package]] -name = "digest" -version = "0.10.7" +name = "darling_core" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" dependencies = [ - "block-buffer 0.10.4", - "crypto-common", - "subtle", + "fnv", + "ident_case", + "proc-macro2 1.0.106", + "quote 1.0.46", + "strsim 0.10.0", + "syn 1.0.109", ] [[package]] -name = "dirs" -version = "1.0.5" +name = "darling_core" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fd78930633bd1c6e35c4b42b1df7b0cbc6bc191146e512bb3bedf243fcc3901" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" dependencies = [ - "libc", - "redox_users 0.3.5", - "winapi 0.3.9", + "fnv", + "ident_case", + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", ] [[package]] -name = "dirs" -version = "2.0.2" +name = "darling_core" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13aea89a5c93364a98e9b37b2fa237effbb694d5cfe01c5b70941f7eb087d5e3" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" dependencies = [ - "cfg-if 0.1.10", - "dirs-sys", + "ident_case", + "proc-macro2 1.0.106", + "quote 1.0.46", + "strsim 0.11.1", + "syn 2.0.118", ] [[package]] -name = "dirs" -version = "4.0.0" +name = "darling_macro" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" dependencies = [ - "dirs-sys", + "darling_core 0.13.4", + "quote 1.0.46", + "syn 1.0.109", ] [[package]] -name = "dirs-next" -version = "1.0.2" +name = "darling_macro" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf36e65a80337bea855cd4ef9b8401ffce06a7baedf2e85ec467b1ac3f6e82b6" +checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" dependencies = [ - "cfg-if 1.0.0", - "dirs-sys-next", + "darling_core 0.14.4", + "quote 1.0.46", + "syn 1.0.109", ] [[package]] -name = "dirs-next" -version = "2.0.0" +name = "darling_macro" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ - "cfg-if 1.0.0", - "dirs-sys-next", + "darling_core 0.21.3", + "quote 1.0.46", + "syn 2.0.118", ] [[package]] -name = "dirs-sys" -version = "0.3.7" +name = "darling_macro" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ - "libc", - "redox_users 0.4.6", - "winapi 0.3.9", + "darling_core 0.23.0", + "quote 1.0.46", + "syn 2.0.118", ] [[package]] -name = "dirs-sys-next" -version = "0.1.2" +name = "dashmap" +version = "5.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ - "libc", - "redox_users 0.4.6", - "winapi 0.3.9", + "cfg-if 1.0.4", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", ] [[package]] -name = "displaydoc" -version = "0.2.5" +name = "data-encoding" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2 1.0.89", - "quote 1.0.37", - "syn 2.0.86", -] +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] -name = "easy-jsonrpc-mw" -version = "0.5.4" +name = "der" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3b1a91569d50e3bba3c9febb22ef54d78c6e8a8d8dd91ae859896c8ba05f4e3" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ - "easy-jsonrpc-proc-macro-mw", - "jsonrpc-core", - "rand 0.6.5", - "serde", - "serde_json", + "const-oid 0.9.6", + "pem-rfc7468", + "zeroize", ] [[package]] -name = "easy-jsonrpc-proc-macro-mw" -version = "0.5.1" +name = "der-parser" +version = "10.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6368dbd2c6685fb84fc6e6a4749917ddc98905793fd06341c7e11a2504f2724" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" dependencies = [ - "heck", - "proc-macro2 0.4.30", - "quote 0.6.13", - "syn 0.15.44", + "asn1-rs", + "cookie-factory", + "displaydoc", + "nom", + "num-traits 0.2.19", + "rusticata-macros", ] [[package]] -name = "ed25519" -version = "1.5.3" +name = "deranged" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ - "signature", + "serde_core", ] [[package]] -name = "ed25519-dalek" -version = "1.0.1" +name = "derive-deftly" +version = "1.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" +checksum = "0bc153c91ebf221a2e7feb166aee259acf8a00ecf35c83df8b79fe8f5c3861d7" dependencies = [ - "curve25519-dalek 3.2.0", - "ed25519", - "rand 0.7.3", - "serde", - "sha2 0.9.9", - "zeroize", + "derive-deftly-macros", + "heck 0.5.0", ] [[package]] -name = "either" -version = "1.13.0" +name = "derive-deftly-macros" +version = "1.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +checksum = "1747ed5fb4ab3a9f8253da3401efe7ca8f8e5ec373b2e700ff55c3304d84e31f" +dependencies = [ + "heck 0.5.0", + "indexmap 2.14.0", + "itertools 0.15.0", + "proc-macro-crate", + "proc-macro2 1.0.106", + "quote 1.0.46", + "sha3 0.12.0", + "strum 0.28.0", + "syn 2.0.118", + "unicode-ident", + "void", +] [[package]] -name = "encode_unicode" -version = "1.0.0" +name = "derive_builder_core_fork_arti" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" +checksum = "24c1b715c79be6328caa9a5e1a387a196ea503740f0722ec3dd8f67a9e72314d" +dependencies = [ + "darling 0.14.4", + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 1.0.109", +] [[package]] -name = "encoding_rs" -version = "0.8.35" +name = "derive_builder_fork_arti" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +checksum = "c3eae24d595f4d0ecc90a9a5a6d11c2bd8dafe2375ec4a1ec63250e5ade7d228" dependencies = [ - "cfg-if 1.0.0", + "derive_builder_macro_fork_arti", ] [[package]] -name = "enum_primitive" -version = "0.1.1" +name = "derive_builder_macro_fork_arti" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4551092f4d519593039259a9ed8daedf0da12e5109c5280338073eaeb81180" +checksum = "69887769a2489cd946bf782eb2b1bb2cb7bc88551440c94a765d4f040c08ebf3" dependencies = [ - "num-traits 0.1.43", + "derive_builder_core_fork_arti", + "syn 1.0.109", ] [[package]] -name = "equivalent" -version = "1.0.1" +name = "derive_more" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] [[package]] -name = "errno" -version = "0.3.9" +name = "derive_more-impl" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ - "libc", - "windows-sys 0.52.0", + "convert_case", + "proc-macro2 1.0.106", + "quote 1.0.46", + "rustc_version", + "syn 2.0.118", + "unicode-xid 0.2.6", ] [[package]] -name = "fastrand" -version = "2.1.1" +name = "destructure_traitobject" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" +checksum = "3c877555693c14d2f84191cfd3ad8582790fc52b5e2274b40b59cf5f5cea25c7" [[package]] -name = "find-crate" -version = "0.6.3" +name = "digest" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59a98bbaacea1c0eb6a0876280051b892eb73594fd90cf3b20e9c817029c57d2" +checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" dependencies = [ - "toml", + "generic-array 0.12.4", ] [[package]] -name = "find-msvc-tools" -version = "0.1.9" +name = "digest" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array 0.14.7", +] [[package]] -name = "flate2" -version = "1.0.34" +name = "digest" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "crc32fast", - "miniz_oxide", + "block-buffer 0.10.4", + "const-oid 0.9.6", + "crypto-common 0.1.7", + "subtle", ] [[package]] -name = "fluent" -version = "0.16.1" +name = "digest" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb74634707bebd0ce645a981148e8fb8c7bccd4c33c652aeffd28bf2f96d555a" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ - "fluent-bundle", - "unic-langid", + "const-oid 0.10.2", + "crypto-common 0.2.2", ] [[package]] -name = "fluent-bundle" -version = "0.15.3" +name = "directories" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe0a21ee80050c678013f82edf4b705fe2f26f1f9877593d13198612503f493" +checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" dependencies = [ - "fluent-langneg", - "fluent-syntax", - "intl-memoizer", - "intl_pluralrules", - "rustc-hash", - "self_cell 0.10.3", - "smallvec", - "unic-langid", + "dirs-sys 0.5.0", ] [[package]] -name = "fluent-langneg" -version = "0.13.0" +name = "dirs" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c4ad0989667548f06ccd0e306ed56b61bd4d35458d54df5ec7587c0e8ed5e94" +checksum = "3fd78930633bd1c6e35c4b42b1df7b0cbc6bc191146e512bb3bedf243fcc3901" dependencies = [ - "unic-langid", + "libc", + "redox_users 0.3.5", + "winapi", ] [[package]] -name = "fluent-syntax" -version = "0.11.1" +name = "dirs" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a530c4694a6a8d528794ee9bbd8ba0122e779629ac908d15ad5a7ae7763a33d" +checksum = "13aea89a5c93364a98e9b37b2fa237effbb694d5cfe01c5b70941f7eb087d5e3" dependencies = [ - "thiserror", + "cfg-if 0.1.10", + "dirs-sys 0.3.7", ] [[package]] -name = "fnv" -version = "1.0.7" +name = "dirs" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys 0.3.7", +] [[package]] -name = "foreign-types" -version = "0.3.2" +name = "dirs" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" dependencies = [ - "foreign-types-shared", + "dirs-sys 0.5.0", ] [[package]] -name = "foreign-types-shared" -version = "0.1.1" +name = "dirs-next" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if 1.0.4", + "dirs-sys-next", +] [[package]] -name = "form_urlencoded" -version = "1.2.2" +name = "dirs-sys" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" dependencies = [ - "percent-encoding", + "libc", + "redox_users 0.4.6", + "winapi", ] [[package]] -name = "fuchsia-cprng" -version = "0.1.1" +name = "dirs-sys" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.5.2", + "windows-sys 0.61.2", +] [[package]] -name = "fuchsia-zircon" -version = "0.3.3" +name = "dirs-sys-next" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" dependencies = [ - "bitflags 1.3.2", - "fuchsia-zircon-sys", + "libc", + "redox_users 0.4.6", + "winapi", ] [[package]] -name = "fuchsia-zircon-sys" -version = "0.3.3" +name = "displaydoc" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", +] [[package]] -name = "futures" -version = "0.1.31" +name = "downcast-rs" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a471a38ef8ed83cd6e40aa59c1ffe17db6855c18e3604d9c4ed8c08ebc28678" +checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc" [[package]] -name = "futures" -version = "0.3.31" +name = "doxygen-rs" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "415b6ec780d34dcf624666747194393603d0373b7141eef01d12ee58881507d9" dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", + "phf 0.11.3", ] [[package]] -name = "futures-channel" -version = "0.3.31" +name = "dyn-clone" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "easy-jsonrpc-mw" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3b1a91569d50e3bba3c9febb22ef54d78c6e8a8d8dd91ae859896c8ba05f4e3" dependencies = [ - "futures-core", - "futures-sink", + "easy-jsonrpc-proc-macro-mw", + "jsonrpc-core", + "rand 0.6.5", + "serde", + "serde_json", ] [[package]] -name = "futures-core" -version = "0.3.31" +name = "easy-jsonrpc-proc-macro-mw" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "a6368dbd2c6685fb84fc6e6a4749917ddc98905793fd06341c7e11a2504f2724" +dependencies = [ + "heck 0.3.3", + "proc-macro2 0.4.30", + "quote 0.6.13", + "syn 0.15.44", +] [[package]] -name = "futures-executor" -version = "0.3.31" +name = "ecdsa" +version = "0.16.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ - "futures-core", - "futures-task", - "futures-util", + "der", + "digest 0.10.7", + "elliptic-curve", + "rfc6979", + "signature", + "spki", ] [[package]] -name = "futures-io" -version = "0.3.31" +name = "ed25519" +version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] [[package]] -name = "futures-macro" -version = "0.3.31" +name = "ed25519-dalek" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" dependencies = [ - "proc-macro2 1.0.89", - "quote 1.0.37", - "syn 2.0.86", + "curve25519-dalek 4.1.3", + "ed25519", + "merlin", + "rand_core 0.6.4", + "serde", + "sha2 0.10.9", + "subtle", + "zeroize", ] [[package]] -name = "futures-sink" -version = "0.3.31" +name = "educe" +version = "0.4.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "0f0042ff8246a363dbe77d2ceedb073339e85a804b9a47636c6e016a9a32c05f" +dependencies = [ + "enum-ordinalize", + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 1.0.109", +] [[package]] -name = "futures-task" -version = "0.3.31" +name = "either" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" [[package]] -name = "futures-util" -version = "0.3.31" +name = "elliptic-curve" +version = "0.13.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite 0.2.15", - "pin-utils", - "slab", + "base16ct", + "crypto-bigint", + "digest 0.10.7", + "ff", + "generic-array 0.14.7", + "group", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", ] [[package]] -name = "gcc" -version = "0.3.55" +name = "encode_unicode" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" [[package]] -name = "generic-array" -version = "0.12.4" +name = "encoding_rs" +version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ - "typenum", + "cfg-if 1.0.4", ] [[package]] -name = "generic-array" -version = "0.14.7" +name = "endian-type" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + +[[package]] +name = "enum-ordinalize" +version = "3.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bf1fa3f06bbff1ea5b1a9c7b14aa992a39657db60a2759457328d7e058f49ee" dependencies = [ - "typenum", - "version_check", + "num-bigint 0.4.8", + "num-traits 0.2.19", + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", ] [[package]] -name = "getrandom" -version = "0.1.16" +name = "enum_dispatch" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd" dependencies = [ - "cfg-if 1.0.0", - "libc", - "wasi 0.9.0+wasi-snapshot-preview1", + "once_cell", + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", ] [[package]] -name = "getrandom" -version = "0.2.15" +name = "enum_primitive" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "be4551092f4d519593039259a9ed8daedf0da12e5109c5280338073eaeb81180" dependencies = [ - "cfg-if 1.0.0", - "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "num-traits 0.1.43", ] [[package]] -name = "gimli" -version = "0.31.1" +name = "enumset" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +checksum = "839c4174b41e75c8f7306110b2c51996a293b8d1d850edd529011841d9fede7d" +dependencies = [ + "enumset_derive", +] [[package]] -name = "git2" -version = "0.20.4" +name = "enumset_derive" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b" +checksum = "4bd536557b58c682b217b8fb199afdff47cd3eff260623f19e77074eb073d63a" dependencies = [ - "bitflags 2.6.0", - "libc", - "libgit2-sys", - "log", - "url", + "darling 0.21.3", + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", ] [[package]] -name = "grin_api" -version = "5.4.1" +name = "env_logger" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51cb8a36f1df7f552e7033b07800c5e54e350b06ab42ab77c209b0da2ede9547" +checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" dependencies = [ - "async-stream", - "bytes 1.8.0", - "easy-jsonrpc-mw", - "futures 0.3.31", - "grin_chain", - "grin_core", - "grin_p2p", - "grin_pool", - "grin_store", - "grin_util", - "http", - "hyper 0.14.32", - "hyper-rustls 0.23.2", - "hyper-timeout", - "lazy_static", + "atty", + "humantime 1.3.0", "log", "regex", - "ring 0.16.20", - "rustls 0.20.9", - "rustls-pemfile", - "serde", - "serde_derive", - "serde_json", - "thiserror", - "tokio 1.42.1", - "tokio-rustls 0.23.4", - "url", + "termcolor", ] [[package]] -name = "grin_chain" -version = "5.4.1" +name = "equivalent" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f22a388060be19bb1fe8bcaae54e5b61726db8dae4676ff2f3f082752843bc02" -dependencies = [ +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "error-code" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64f18991e7bf11e7ffee451b5318b5c1a73c52d0d0ada6e5a3017c8c1ced6a21" +dependencies = [ + "libc", + "str-buf", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "fd-lock" +version = "3.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef033ed5e9bad94e55838ca0ca906db0e043f517adda0c8b79c7a8c66c93c1b5" +dependencies = [ + "cfg-if 1.0.4", + "rustix 0.38.44", + "windows-sys 0.48.0", +] + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "fiat-crypto" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" + +[[package]] +name = "figment" +version = "0.10.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3" +dependencies = [ + "atomic 0.6.1", + "serde", + "toml 0.8.23", + "uncased", + "version_check", +] + +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if 1.0.4", + "libc", +] + +[[package]] +name = "find-crate" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59a98bbaacea1c0eb6a0876280051b892eb73594fd90cf3b20e9c817029c57d2" +dependencies = [ + "toml 0.5.11", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fluent" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb74634707bebd0ce645a981148e8fb8c7bccd4c33c652aeffd28bf2f96d555a" +dependencies = [ + "fluent-bundle", + "unic-langid", +] + +[[package]] +name = "fluent-bundle" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe0a21ee80050c678013f82edf4b705fe2f26f1f9877593d13198612503f493" +dependencies = [ + "fluent-langneg", + "fluent-syntax", + "intl-memoizer", + "intl_pluralrules", + "rustc-hash 1.1.0", + "self_cell 0.10.3", + "smallvec", + "unic-langid", +] + +[[package]] +name = "fluent-langneg" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eebbe59450baee8282d71676f3bfed5689aeab00b27545e83e5f14b1195e8b0" +dependencies = [ + "unic-langid", +] + +[[package]] +name = "fluent-syntax" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a530c4694a6a8d528794ee9bbd8ba0122e779629ac908d15ad5a7ae7763a33d" +dependencies = [ + "thiserror 1.0.69", +] + +[[package]] +name = "fluid-let" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "749cff877dc1af878a0b31a41dd221a753634401ea0ef2f87b62d3171522485a" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs-mistrust" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd938e053fbfb6dba36106f4bcf27c53362e6152adc9341700c4bc2264cd4a8f" +dependencies = [ + "derive_builder_fork_arti", + "dirs 6.0.0", + "libc", + "pwd-grp", + "serde", + "thiserror 2.0.18", + "void", + "walkdir", +] + +[[package]] +name = "fslock-guard" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63307a37c3d90d55a90eefa5441ec12cbd291ff456b6bf1126656c5bc43b058" +dependencies = [ + "libc", + "thiserror 2.0.18", + "windows-sys 0.61.2", +] + +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a471a38ef8ed83cd6e40aa59c1ffe17db6855c18e3604d9c4ed8c08ebc28678" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-copy" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcad91f8ba72542347d5cc8ee43891abe4698429fa7b3de56f17f4bb6fa514c4" +dependencies = [ + "futures 0.3.32", + "libc", + "pin-project", + "windows-sys 0.61.2", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" +dependencies = [ + "typenum", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if 1.0.4", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if 1.0.4", + "js-sys", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if 1.0.4", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if 1.0.4", + "js-sys", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasm-bindgen", +] + +[[package]] +name = "getset" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cf442baaabe4213ce7d1239afc26c039180b6456da2cededa316ae2c8a77a77" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[package]] +name = "git2" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddddbf932745a6be37109b6112d3ee09696106f848449069d3a57bba937ab82e" +dependencies = [ + "bitflags 2.13.0", + "libc", + "libgit2-sys", + "log", +] + +[[package]] +name = "glob-match" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9985c9503b412198aa4197559e9a318524ebc4519c229bfa05a535828c950b9d" + +[[package]] +name = "grin_api" +version = "5.5.1-alpha.0" +dependencies = [ + "bytes 1.12.0", + "easy-jsonrpc-mw", + "futures 0.3.32", + "grin_chain", + "grin_core", + "grin_p2p", + "grin_pool", + "grin_util", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-timeout", + "hyper-util", + "lazy_static", + "log", + "regex", + "rustls", + "rustls-pemfile", + "serde", + "serde_derive", + "serde_json", + "subtle", + "thiserror 1.0.69", + "tokio", + "tokio-rustls", + "url", +] + +[[package]] +name = "grin_chain" +version = "5.5.1-alpha.0" +dependencies = [ "bit-vec", "bitflags 1.3.2", - "byteorder", - "chrono", - "croaring", - "enum_primitive", - "grin_core", - "grin_keychain", - "grin_store", - "grin_util", - "lazy_static", + "byteorder", + "chrono", + "croaring", + "enum_primitive", + "env_logger", + "grin_core", + "grin_keychain", + "grin_store", + "grin_util", + "lazy_static", + "log", + "lru-cache", + "rand 0.6.5", + "serde", + "serde_derive", + "thiserror 1.0.69", +] + +[[package]] +name = "grin_core" +version = "5.5.1-alpha.0" +dependencies = [ + "blake2-rfc", + "byteorder", + "bytes 0.5.6", + "chrono", + "croaring", + "enum_primitive", + "grin_keychain", + "grin_util", + "lazy_static", + "log", + "lru-cache", + "num", + "num-bigint 0.2.6", + "rand 0.6.5", + "serde", + "serde_derive", + "serde_json", + "siphasher 0.3.11", + "thiserror 1.0.69", + "zeroize", +] + +[[package]] +name = "grin_keychain" +version = "5.5.1-alpha.0" +dependencies = [ + "blake2-rfc", + "byteorder", + "digest 0.9.0", + "grin_util", + "hmac 0.11.0", + "lazy_static", + "log", + "pbkdf2 0.8.0", + "rand 0.6.5", + "ripemd160", + "serde", + "serde_derive", + "serde_json", + "sha2 0.9.9", + "zeroize", +] + +[[package]] +name = "grin_p2p" +version = "5.5.1-alpha.0" +dependencies = [ + "bitflags 1.3.2", + "built", + "bytes 0.5.6", + "chrono", + "enum_primitive", + "grin_chain", + "grin_core", + "grin_pool", + "grin_store", + "grin_util", + "log", + "lru-cache", + "num", + "rand 0.6.5", + "serde", + "serde_derive", + "tempfile", +] + +[[package]] +name = "grin_pool" +version = "5.5.1-alpha.0" +dependencies = [ + "blake2-rfc", + "chrono", + "grin_chain", + "grin_core", + "grin_keychain", + "grin_util", + "log", + "rand 0.6.5", + "serde", + "serde_derive", + "thiserror 1.0.69", +] + +[[package]] +name = "grin_secp256k1zkp" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf7bb95f155b1eede2648a1b9afbba82bc3d9f2af0518b478767559a572bd973" +dependencies = [ + "arrayvec 0.7.8", + "cc", + "libc", + "rand 0.5.6", + "serde", + "serde_json", + "zeroize", +] + +[[package]] +name = "grin_store" +version = "5.5.1-alpha.0" +dependencies = [ + "byteorder", + "chrono", + "croaring", + "env_logger", + "filetime", + "grin_core", + "grin_util", + "heed", + "libc", + "log", + "memmap", + "rand 0.6.5", + "serde", + "serde_derive", + "tempfile", + "thiserror 1.0.69", +] + +[[package]] +name = "grin_util" +version = "5.5.1-alpha.0" +dependencies = [ + "anyhow", + "backtrace", + "base64 0.12.3", + "grin_secp256k1zkp", + "lazy_static", + "log", + "log4rs", + "parking_lot", + "rand 0.6.5", + "serde", + "serde_derive", + "walkdir", + "zeroize", + "zip", +] + +[[package]] +name = "grin_wallet" +version = "5.4.1" +dependencies = [ + "built", + "clap", + "easy-jsonrpc-mw", + "grin_api", + "grin_core", + "grin_keychain", + "grin_util", + "grin_wallet_api", + "grin_wallet_config", + "grin_wallet_controller", + "grin_wallet_impls", + "grin_wallet_libwallet", + "lazy_static", + "linefeed", + "log", + "rpassword", + "rustyline", + "semver", + "serde", + "serde_json", + "thiserror 1.0.69", + "url", +] + +[[package]] +name = "grin_wallet_api" +version = "5.4.1" +dependencies = [ + "base64 0.12.3", + "chrono", + "easy-jsonrpc-mw", + "ed25519-dalek", + "grin_core", + "grin_keychain", + "grin_util", + "grin_wallet_config", + "grin_wallet_impls", + "grin_wallet_libwallet", + "grin_wallet_util", + "log", + "rand 0.6.5", + "ring", + "serde", + "serde_derive", + "serde_json", + "tempfile", + "uuid", +] + +[[package]] +name = "grin_wallet_config" +version = "5.4.1" +dependencies = [ + "dirs 2.0.2", + "grin_core", + "grin_util", + "lazy_static", + "rand 0.6.5", + "serde", + "serde_derive", + "tempfile", + "toml 0.5.11", + "toml_edit 0.22.27", +] + +[[package]] +name = "grin_wallet_controller" +version = "5.4.1" +dependencies = [ + "easy-jsonrpc-mw", + "ed25519-dalek", + "grin_api", + "grin_chain", + "grin_core", + "grin_keychain", + "grin_util", + "grin_wallet_api", + "grin_wallet_config", + "grin_wallet_impls", + "grin_wallet_libwallet", + "grin_wallet_util", + "http-body-util", + "hyper", + "lazy_static", + "log", + "prettytable-rs", + "qr_code", + "rand 0.7.3", + "serde", + "serde_json", + "term 0.6.1", + "thiserror 1.0.69", + "tokio", + "uuid", +] + +[[package]] +name = "grin_wallet_impls" +version = "5.4.1" +dependencies = [ + "arti-client", + "base64 0.12.3", + "blake2-rfc", + "bytes 1.12.0", + "chrono", + "ed25519-dalek", + "fs-mistrust", + "fslock-guard", + "futures 0.3.32", + "grin_api", + "grin_chain", + "grin_core", + "grin_keychain", + "grin_util", + "grin_wallet_config", + "grin_wallet_libwallet", + "grin_wallet_util", + "http-body-util", + "hyper", + "hyper-util", + "lazy_static", + "log", + "rand 0.6.5", + "regex", + "reqwest", + "ring", + "serde", + "serde_derive", + "serde_json", + "sha2 0.10.9", + "sysinfo 0.29.11", + "thiserror 1.0.69", + "timer", + "tokio", + "tor-hscrypto", + "tor-hsrproxy", + "tor-hsservice", + "tor-keymgr", + "tor-llcrypto", + "tor-rtcompat", + "url", +] + +[[package]] +name = "grin_wallet_libwallet" +version = "5.4.1" +dependencies = [ + "age", + "base64 0.9.3", + "bech32 0.7.3", + "blake2-rfc", + "bs58 0.3.1", + "byteorder", + "chacha20 0.8.2", + "chrono", + "curve25519-dalek 5.0.0", + "ed25519-dalek", + "grin_core", + "grin_keychain", + "grin_store", + "grin_util", + "grin_wallet_config", + "grin_wallet_util", + "hmac 0.12.1", + "lazy_static", + "log", + "num-bigint 0.2.6", + "rand 0.6.5", + "regex", + "serde", + "serde_derive", + "serde_json", + "serde_with 1.14.0", + "sha2 0.10.9", + "strum 0.18.0", + "strum_macros 0.18.0", + "thiserror 1.0.69", + "uuid", + "x25519-dalek 2.0.1", +] + +[[package]] +name = "grin_wallet_util" +version = "5.4.1" +dependencies = [ + "data-encoding", + "ed25519-dalek", + "grin_util", + "serde", + "serde_derive", + "sha3 0.8.2", + "thiserror 1.0.69", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "growable-bloom-filter" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d174ccb4ba660d431329e7f0797870d0a4281e36353ec4b4a3c5eab6c2cfb6f1" +dependencies = [ + "serde", + "serde_bytes", + "serde_derive", + "xxhash-rust", +] + +[[package]] +name = "h2" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" +dependencies = [ + "atomic-waker", + "bytes 1.12.0", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.14.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash 0.2.0", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hashlink" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824e001ac4f3012dd16a264bec811403a67ca9deb6c102fc5049b32c4574b35f" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "heed" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad82d6598ccf1dac15c8b758a1bd282b755b6776be600429176757190a1b0202" +dependencies = [ + "bitflags 2.13.0", + "byteorder", + "heed-traits", + "heed-types", + "libc", + "lmdb-master-sys", + "once_cell", + "page_size", + "serde", + "synchronoise", + "url", +] + +[[package]] +name = "heed-traits" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3130048d404c57ce5a1ac61a903696e8fcde7e8c2991e9fcfc1f27c3ef74ff" + +[[package]] +name = "heed-types" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c255bdf46e07fb840d120a36dcc81f385140d7191c76a7391672675c01a55d" +dependencies = [ + "bincode 1.3.3", + "byteorder", + "heed-traits", + "serde", + "serde_json", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac 0.12.1", +] + +[[package]] +name = "hmac" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" +dependencies = [ + "crypto-mac", + "digest 0.9.0", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "hostname-validator" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f558a64ac9af88b5ba400d99b579451af0d39c6d360980045b91aac966d705e2" + +[[package]] +name = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +dependencies = [ + "bytes 1.12.0", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes 1.12.0", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes 1.12.0", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humantime" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" +dependencies = [ + "quick-error", +] + +[[package]] +name = "humantime" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15cdd26707701c53297e2fa6afb323d55fbc1d0810c3aec078ae3ef0424c3c15" + +[[package]] +name = "humantime-serde" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57a3db5ea5923d99402c94e9feb261dc5ee9b4efa158b0315f788cf549cc200c" +dependencies = [ + "humantime 2.4.0", + "serde", +] + +[[package]] +name = "hybrid-array" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +dependencies = [ + "typenum", +] + +[[package]] +name = "hyper" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes 1.12.0", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "log", + "rustls", + "rustls-native-certs", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes 1.12.0", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes 1.12.0", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "i18n-config" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e06b90c8a0d252e203c94344b21e35a30f3a3a85dc7db5af8f8df9f3e0c63ef" +dependencies = [ + "basic-toml", + "log", + "serde", + "serde_derive", + "thiserror 1.0.69", + "unic-langid", +] + +[[package]] +name = "i18n-embed" +version = "0.13.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92a86226a7a16632de6723449ee5fe70bac5af718bc642ee9ca2f0f6e14fa1fa" +dependencies = [ + "arc-swap", + "fluent", + "fluent-langneg", + "fluent-syntax", + "i18n-embed-impl", + "intl-memoizer", + "lazy_static", + "log", + "parking_lot", + "rust-embed", + "thiserror 1.0.69", + "unic-langid", + "walkdir", +] + +[[package]] +name = "i18n-embed-fl" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26a3d3569737dfaac7fc1c4078e6af07471c3060b8e570bcd83cdd5f4685395" +dependencies = [ + "dashmap", + "find-crate", + "fluent", + "fluent-syntax", + "i18n-config", + "i18n-embed", + "lazy_static", + "proc-macro-error", + "proc-macro2 1.0.106", + "quote 1.0.46", + "strsim 0.10.0", + "syn 2.0.118", + "unic-langid", +] + +[[package]] +name = "i18n-embed-impl" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f2cc0e0523d1fe6fc2c6f66e5038624ea8091b3e7748b5e8e0c84b1698db6c2" +dependencies = [ + "find-crate", + "i18n-config", + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "imara-diff" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f01d462f766df78ab820dd06f5eb700233c51f0f4c2e846520eaf4ba6aa5c5c" +dependencies = [ + "hashbrown 0.15.5", + "memchr", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg 1.5.1", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "inotify" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "533e68a5842e734946fe159fb03fc9bbbb254f590dd0d8ad321ae5ff7beca2c1" +dependencies = [ + "bitflags 2.13.0", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea94e891b3606826e9c998be69ddca42247dad8ad50b1649a5cb7e1c9ae06fd" +dependencies = [ + "libc", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array 0.14.7", +] + +[[package]] +name = "intl-memoizer" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "310da2e345f5eb861e7a07ee182262e94975051db9e4223e909ba90f392f163f" +dependencies = [ + "type-map", + "unic-langid", +] + +[[package]] +name = "intl_pluralrules" +version = "7.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078ea7b7c29a2b4df841a7f6ac8775ff6074020c6776d48491ce2268e068f972" +dependencies = [ + "unic-langid", +] + +[[package]] +name = "inventory" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b" +dependencies = [ + "rustversion", +] + +[[package]] +name = "io_tee" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b3f7cef34251886990511df1c61443aa928499d598a9473929ab5a90a527304" + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi 0.5.2", + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if 1.0.4", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "jsonrpc-core" +version = "10.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc15eef5f8b6bef5ac5f7440a957ff95d036e2f98706947741bfc93d1976db4c" +dependencies = [ + "futures 0.1.31", + "log", + "serde", + "serde_derive", + "serde_json", +] + +[[package]] +name = "k12" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4dc5fdb62af2f520116927304f15d25b3c2667b4817b90efdc045194c912c54" +dependencies = [ + "digest 0.10.7", + "sha3 0.10.9", +] + +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures 0.2.17", +] + +[[package]] +name = "keccak" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e24a010dd405bd7ed803e5253182815b41bf2e6a80cc3bfc066658e03a198aa" +dependencies = [ + "cfg-if 1.0.4", + "cpufeatures 0.3.0", +] + +[[package]] +name = "kqueue" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "273c0752728918e0ac4976f2b275b6fefb9ecd400585dec929419f3844cd87b5" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087" +dependencies = [ + "bitflags 2.13.0", + "libc", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libgit2-sys" +version = "0.18.5+1.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "005d6ae6eac1912906073e069f7db60b1fa98e052a68227824afe3e3a1c59ca2" +dependencies = [ + "cc", + "libc", + "libz-sys", + "pkg-config", +] + +[[package]] +name = "liblzma" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45aec2360b3933207e27908049d8e4df4e476b58180afb1e56b2a4fb72efe4ba" +dependencies = [ + "liblzma-sys", +] + +[[package]] +name = "liblzma-sys" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a046c7f353ba30f810545151e04f63545833803f5b86ee3ddf1517247fe560a5" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" +dependencies = [ + "libc", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f111c8c41e7c61a49cd34e44c7619462967221a6443b0ec299e0ac30cfb9b1" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libz-sys" +version = "1.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85bc9657773828b90eeb625adff10eeac83cc21bbfd8e23a03eaa8a33c9e28d9" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linefeed" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28715d08e35c6c074f9ae6b2e6a2420bac75d050c66ecd669d7d5b98e2caa036" +dependencies = [ + "dirs 1.0.5", + "mortal", + "winapi", +] + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lmdb-master-sys" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaeb9bd22e73bd1babffff614994b341e9b2008de7bb73bf1f7e9154f1978f8b" +dependencies = [ + "cc", + "doxygen-rs", + "libc", +] + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +dependencies = [ + "serde_core", +] + +[[package]] +name = "log-mdc" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a94d21414c1f4a51209ad204c1776a3d0765002c76c6abcb602a6f09f1e881c7" + +[[package]] +name = "log4rs" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e947bb896e702c711fccc2bf02ab2abb6072910693818d1d6b07ee2b9dfd86c" +dependencies = [ + "anyhow", + "arc-swap", + "chrono", + "derive_more", + "flate2", + "fnv", + "humantime 2.4.0", + "libc", + "log", + "log-mdc", + "mock_instant", + "parking_lot", + "rand 0.9.4", + "serde", + "serde-value", + "serde_json", + "serde_yaml", + "thiserror 2.0.18", + "thread-id", + "typemap-ors", + "unicode-segmentation", + "winapi", +] + +[[package]] +name = "lru-cache" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[package]] +name = "memmap" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6585fd95e7bb50d6cc31e20d4cf9afb4e2ba16c5846fc76793f11218da9c475b" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "memmap2" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg 1.5.1", +] + +[[package]] +name = "merlin" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58c38e2799fc0978b65dfff8023ec7843e2330bb462f19198840b34b6582397d" +dependencies = [ + "byteorder", + "keccak 0.1.6", + "rand_core 0.6.4", + "zeroize", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "log", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.61.2", +] + +[[package]] +name = "mock_instant" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb517913cfcfb9eeda59f36020269075a152701a01606c612f547e4890be399" + +[[package]] +name = "mortal" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c624fa1b7aab6bd2aff6e9b18565cc0363b6d45cbcd7465c9ed5e3740ebf097" +dependencies = [ + "bitflags 2.13.0", + "libc", + "nix 0.26.4", + "smallstr", + "terminfo", + "unicode-normalization", + "unicode-width", + "winapi", +] + +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + +[[package]] +name = "nix" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c" +dependencies = [ + "bitflags 1.3.2", + "cc", + "cfg-if 1.0.4", + "libc", + "memoffset", +] + +[[package]] +name = "nix" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +dependencies = [ + "bitflags 1.3.2", + "cfg-if 1.0.4", + "libc", +] + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nonany" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b8866ec53810a9a4b3d434a29801e78c707430a9ae11c2db4b8b62bb9675a0" + +[[package]] +name = "notify" +version = "8.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" +dependencies = [ + "bitflags 2.13.0", + "inotify", + "kqueue", + "libc", "log", - "lru-cache", - "serde", - "serde_derive", - "thiserror", + "mio", + "notify-types", + "walkdir", + "windows-sys 0.60.2", +] + +[[package]] +name = "notify-types" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" +dependencies = [ + "bitflags 2.13.0", +] + +[[package]] +name = "ntapi" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" +dependencies = [ + "winapi", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8536030f9fea7127f841b45bb6243b27255787fb4eb83958aa1ef9d2fdc0c36" +dependencies = [ + "num-bigint 0.2.6", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits 0.2.19", +] + +[[package]] +name = "num-bigint" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304" +dependencies = [ + "autocfg 1.5.1", + "num-integer", + "num-traits 0.2.19", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits 0.2.19", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits 0.2.19", + "rand 0.8.6", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-complex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b19411a9719e753aff12e5187b74d60d3dc449ec3f4dc21e3989c3f554bc95" +dependencies = [ + "autocfg 1.5.1", + "num-traits 0.2.19", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits 0.2.19", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg 1.5.1", + "num-integer", + "num-traits 0.2.19", +] + +[[package]] +name = "num-rational" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c000134b5dbf44adc5cb772486d335293351644b801551abe8f75c84cfa4aef" +dependencies = [ + "autocfg 1.5.1", + "num-bigint 0.2.6", + "num-integer", + "num-traits 0.2.19", +] + +[[package]] +name = "num-traits" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31" +dependencies = [ + "num-traits 0.2.19", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg 1.5.1", + "libm", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate", + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.0", ] [[package]] -name = "grin_core" -version = "5.4.1" +name = "objc2-io-kit" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b286b10a906330569326077779fa6bcde456f5c77e54bc87aefbda308ee9b19e" +checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" dependencies = [ - "blake2-rfc", - "byteorder", - "bytes 0.5.6", - "chrono", - "croaring", - "enum_primitive", - "grin_keychain", - "grin_util", - "lazy_static", - "log", - "lru-cache", - "num", - "num-bigint", - "rand 0.6.5", - "serde", - "serde_derive", - "siphasher", - "thiserror", - "zeroize", + "libc", + "objc2-core-foundation", ] [[package]] -name = "grin_keychain" -version = "5.4.1" +name = "object" +version = "0.37.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afff7e0a383366082aad384e7b4f285519a4940a569cac793bb941354ce0e368" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" dependencies = [ - "blake2-rfc", - "byteorder", - "digest 0.9.0", - "grin_util", - "hmac 0.11.0", - "lazy_static", - "log", - "pbkdf2 0.8.0", - "rand 0.6.5", - "ripemd160", - "serde", - "serde_derive", - "serde_json", - "sha2 0.9.9", - "zeroize", + "memchr", ] [[package]] -name = "grin_p2p" -version = "5.4.1" +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "oneshot-fused-workaround" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69be142b76bd6472d2faab27dd3e0024a2c6818ed0e1ccc5a0e9788590949744" +checksum = "60d6f8f565703c8a15f98b7782e39526490f8c291910d80c9be54a753a822f6f" dependencies = [ - "bitflags 1.3.2", - "bytes 0.5.6", - "chrono", - "enum_primitive", - "grin_chain", - "grin_core", - "grin_store", - "grin_util", - "log", - "lru-cache", - "num", - "rand 0.6.5", - "serde", - "serde_derive", - "tempfile", + "futures 0.3.32", ] [[package]] -name = "grin_pool" -version = "5.4.1" +name = "opaque-debug" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openssl" +version = "0.10.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f770835a6f78de214d650f9fec066d55b854b3689c905d63e931eba05179587a" +checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" dependencies = [ - "blake2-rfc", - "chrono", - "grin_core", - "grin_keychain", - "grin_util", - "log", - "rand 0.6.5", - "serde", - "serde_derive", - "thiserror", + "bitflags 2.13.0", + "cfg-if 1.0.4", + "foreign-types", + "libc", + "openssl-macros", + "openssl-sys", ] [[package]] -name = "grin_secp256k1zkp" -version = "0.7.15" +name = "openssl-macros" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf7bb95f155b1eede2648a1b9afbba82bc3d9f2af0518b478767559a572bd973" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-src" +version = "300.6.1+3.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46eb8fb9fb3b61ce1c0f8a026c4c1a0714d3a9e138e7fbde78753ce2babc3846" dependencies = [ - "arrayvec 0.7.6", "cc", - "libc", - "rand 0.5.6", - "serde", - "serde_json", - "zeroize", ] [[package]] -name = "grin_store" -version = "5.4.1" +name = "openssl-sys" +version = "0.9.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14188a453dbbb505bc068763beb4d8d9e9c18f88d8f0d950b967a13d5500882f" +checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695" dependencies = [ - "byteorder", - "croaring", - "grin_core", - "grin_util", + "cc", "libc", - "lmdb-zero", - "log", - "memmap", - "serde", - "serde_derive", - "tempfile", - "thiserror", + "openssl-src", + "pkg-config", + "vcpkg", ] [[package]] -name = "grin_util" -version = "5.4.1" +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-float" +version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ff9437662d26682f7051a83b4da4544d42cb89966c857011768995dc7dec965" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" dependencies = [ - "anyhow", - "backtrace", - "base64 0.12.3", - "byteorder", - "grin_secp256k1zkp", - "lazy_static", - "log", - "log4rs", - "parking_lot 0.10.2", - "rand 0.6.5", - "serde", - "serde_derive", - "walkdir", - "zeroize", - "zip", + "num-traits 0.2.19", ] [[package]] -name = "grin_wallet" -version = "5.4.1" +name = "os_str_bytes" +version = "6.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" dependencies = [ - "built", - "clap", - "easy-jsonrpc-mw", - "grin_api", - "grin_core", - "grin_keychain", - "grin_util", - "grin_wallet_api", - "grin_wallet_config", - "grin_wallet_controller", - "grin_wallet_impls", - "grin_wallet_libwallet", - "grin_wallet_util", - "lazy_static", - "linefeed", - "log", - "prettytable-rs", - "remove_dir_all", - "rpassword", - "rustyline", - "semver", - "serde", - "serde_derive", - "serde_json", - "thiserror", - "url", + "memchr", ] [[package]] -name = "grin_wallet_api" -version = "5.4.1" +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" dependencies = [ - "base64 0.12.3", - "chrono", - "easy-jsonrpc-mw", - "ed25519-dalek", - "grin_core", - "grin_keychain", - "grin_util", - "grin_wallet_config", - "grin_wallet_impls", - "grin_wallet_libwallet", - "grin_wallet_util", - "log", - "rand 0.6.5", - "ring 0.16.20", - "serde", - "serde_derive", - "serde_json", - "tempfile", - "uuid", + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.9", ] [[package]] -name = "grin_wallet_config" -version = "5.4.1" +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" dependencies = [ - "dirs 2.0.2", - "grin_core", - "grin_util", - "grin_wallet_util", - "pretty_assertions", - "rand 0.6.5", - "serde", - "serde_derive", - "toml", + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.9", ] [[package]] -name = "grin_wallet_controller" -version = "5.4.1" +name = "p521" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc9e2161f1f215afdfce23677034ae137bbd45016a880c2eb3ba8eb95f085b2" dependencies = [ - "chrono", - "easy-jsonrpc-mw", - "ed25519-dalek", - "futures 0.3.31", - "grin_api", - "grin_chain", - "grin_core", - "grin_keychain", - "grin_util", - "grin_wallet_api", - "grin_wallet_config", - "grin_wallet_impls", - "grin_wallet_libwallet", - "grin_wallet_util", - "hyper 0.14.32", - "lazy_static", - "log", - "prettytable-rs", - "qr_code", - "rand 0.7.3", - "remove_dir_all", - "ring 0.16.20", - "serde", - "serde_derive", - "serde_json", - "term 0.6.1", - "thiserror", - "tokio 0.2.25", - "url", - "uuid", + "base16ct", + "ecdsa", + "elliptic-curve", + "primeorder", + "rand_core 0.6.4", + "sha2 0.10.9", ] [[package]] -name = "grin_wallet_impls" -version = "5.4.1" +name = "page_size" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ - "base64 0.12.3", - "blake2-rfc", - "byteorder", - "chrono", - "data-encoding", - "ed25519-dalek", - "futures 0.3.31", - "grin_api", - "grin_chain", - "grin_core", - "grin_keychain", - "grin_store", - "grin_util", - "grin_wallet_config", - "grin_wallet_libwallet", - "grin_wallet_util", - "lazy_static", - "log", - "rand 0.6.5", - "regex", - "remove_dir_all", - "reqwest", - "ring 0.16.20", - "serde", - "serde_derive", - "serde_json", - "sysinfo", - "thiserror", - "timer", - "tokio 0.2.25", - "url", - "uuid", - "x25519-dalek 0.6.0", + "lock_api", + "parking_lot_core", ] [[package]] -name = "grin_wallet_libwallet" -version = "5.4.1" +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ - "age", - "base64 0.9.3", - "bech32 0.7.3", - "blake2-rfc", - "bs58", - "byteorder", - "chacha20", - "chrono", - "curve25519-dalek 2.1.3", - "ed25519-dalek", - "grin_core", - "grin_keychain", - "grin_store", - "grin_util", - "grin_wallet_config", - "grin_wallet_util", - "hmac 0.12.1", - "lazy_static", - "log", - "num-bigint", - "rand 0.6.5", - "regex", - "secrecy 0.6.0", - "serde", - "serde_derive", - "serde_json", - "sha2 0.10.8", - "strum", - "strum_macros", - "thiserror", - "uuid", - "x25519-dalek 0.6.0", + "cfg-if 1.0.4", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", ] [[package]] -name = "grin_wallet_util" -version = "5.4.1" +name = "password-hash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1a5d4e9c205d2c1ae73b84aab6240e98218c0e72e63b50422cfb2d1ca952282" dependencies = [ - "data-encoding", - "ed25519-dalek", - "grin_util", - "pretty_assertions", - "rand 0.6.5", - "serde", - "serde_derive", - "sha3", - "thiserror", + "base64ct", + "rand_core 0.6.4", + "subtle", ] [[package]] -name = "h2" -version = "0.2.7" +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pbkdf2" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e4728fd124914ad25e99e3d15a9361a879f6620f63cb56bbb08f95abb97a535" +checksum = "d95f5254224e617595d2cc3cc73ff0a5eaf2637519e25f03388154e9378b6ffa" dependencies = [ - "bytes 0.5.6", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http", - "indexmap 1.9.3", - "slab", - "tokio 0.2.25", - "tokio-util 0.3.1", - "tracing", - "tracing-futures", + "base64ct", + "crypto-mac", + "hmac 0.11.0", + "password-hash", + "sha2 0.9.9", ] [[package]] -name = "h2" -version = "0.3.27" +name = "pbkdf2" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +checksum = "271779f35b581956db91a3e55737327a03aa051e90b1c47aeb189508533adfd7" dependencies = [ - "bytes 1.8.0", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http", - "indexmap 2.6.0", - "slab", - "tokio 1.42.1", - "tokio-util 0.7.17", - "tracing", + "digest 0.10.7", ] [[package]] -name = "hashbrown" -version = "0.12.3" +name = "pem-rfc7468" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] [[package]] -name = "hashbrown" -version = "0.14.5" +name = "percent-encoding" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] -name = "hashbrown" -version = "0.15.0" +name = "phf" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros 0.11.3", + "phf_shared 0.11.3", +] [[package]] -name = "heck" -version = "0.3.3" +name = "phf" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" dependencies = [ - "unicode-segmentation", + "phf_macros 0.13.1", + "phf_shared 0.13.1", + "serde", ] [[package]] -name = "hermit-abi" -version = "0.1.19" +name = "phf_codegen" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" dependencies = [ - "libc", + "phf_generator 0.11.3", + "phf_shared 0.11.3", ] [[package]] -name = "hermit-abi" -version = "0.3.9" +name = "phf_generator" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared 0.11.3", + "rand 0.8.6", +] [[package]] -name = "hermit-abi" -version = "0.4.0" +name = "phf_generator" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared 0.13.1", +] [[package]] -name = "hkdf" -version = "0.11.0" +name = "phf_macros" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01706d578d5c281058480e673ae4086a9f4710d8df1ad80a5b03e39ece5f886b" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" dependencies = [ - "digest 0.9.0", - "hmac 0.11.0", + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", ] [[package]] -name = "hmac" -version = "0.11.0" +name = "phf_macros" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" dependencies = [ - "crypto-mac", - "digest 0.9.0", + "phf_generator 0.13.1", + "phf_shared 0.13.1", + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", ] [[package]] -name = "hmac" -version = "0.12.1" +name = "phf_shared" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" dependencies = [ - "digest 0.10.7", + "siphasher 1.0.3", ] [[package]] -name = "http" -version = "0.2.12" +name = "phf_shared" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" dependencies = [ - "bytes 1.8.0", - "fnv", - "itoa 1.0.11", + "siphasher 1.0.3", ] [[package]] -name = "http-body" -version = "0.3.1" +name = "pin-project" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13d5ff830006f7646652e057693569bfe0d51760c0085a071769d142a205111b" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" dependencies = [ - "bytes 0.5.6", - "http", + "pin-project-internal", ] [[package]] -name = "http-body" -version = "0.4.6" +name = "pin-project-internal" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" dependencies = [ - "bytes 1.8.0", - "http", - "pin-project-lite 0.2.15", + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", ] [[package]] -name = "httparse" -version = "1.9.5" +name = "pin-project-lite" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] -name = "httpdate" -version = "0.3.2" +name = "pkcs1" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "494b4d60369511e7dea41cf646832512a94e542f68bb9c49e54518e0f468eb47" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] [[package]] -name = "httpdate" -version = "1.0.3" +name = "pkcs8" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] [[package]] -name = "humantime" -version = "2.1.0" +name = "pkg-config" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] -name = "hyper" -version = "0.13.10" +name = "poly1305" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a6f157065790a3ed2f88679250419b5cdd96e714a0d65f7797fd337186e96bb" +checksum = "048aeb476be11a4b6ca432ca569e375810de9294ae78f4774e78ea98a9246ede" dependencies = [ - "bytes 0.5.6", - "futures-channel", - "futures-core", - "futures-util", - "h2 0.2.7", - "http", - "http-body 0.3.1", - "httparse", - "httpdate 0.3.2", - "itoa 0.4.8", - "pin-project", - "socket2 0.3.19", - "tokio 0.2.25", - "tower-service", - "tracing", - "want", + "cpufeatures 0.2.17", + "opaque-debug 0.3.1", + "universal-hash", ] [[package]] -name = "hyper" -version = "0.14.32" +name = "postage" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +checksum = "af3fb618632874fb76937c2361a7f22afd393c982a2165595407edc75b06d3c1" dependencies = [ - "bytes 1.8.0", - "futures-channel", - "futures-core", - "futures-util", - "h2 0.3.27", - "http", - "http-body 0.4.6", - "httparse", - "httpdate 1.0.3", - "itoa 1.0.11", - "pin-project-lite 0.2.15", - "socket2 0.5.8", - "tokio 1.42.1", - "tower-service", - "tracing", - "want", + "atomic 0.5.3", + "crossbeam-queue", + "futures 0.3.32", + "parking_lot", + "pin-project", + "static_assertions", + "thiserror 1.0.69", ] [[package]] -name = "hyper-rustls" -version = "0.21.0" +name = "potential_utf" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37743cc83e8ee85eacfce90f2f4102030d9ff0a95244098d781e9bee4a90abb6" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ - "bytes 0.5.6", - "futures-util", - "hyper 0.13.10", - "log", - "rustls 0.18.1", - "tokio 0.2.25", - "tokio-rustls 0.14.1", - "webpki 0.21.4", + "zerovec", ] [[package]] -name = "hyper-rustls" -version = "0.23.2" +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1788965e61b367cd03a62950836d5cd41560c3577d90e40e0819373194d1661c" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "http", - "hyper 0.14.32", - "log", - "rustls 0.20.9", - "rustls-native-certs", - "tokio 1.42.1", - "tokio-rustls 0.23.4", + "zerocopy", ] [[package]] -name = "hyper-timeout" -version = "0.4.1" +name = "prettytable-rs" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" +checksum = "eea25e07510aa6ab6547308ebe3c036016d162b8da920dbb079e3ba8acf3d95a" dependencies = [ - "hyper 0.14.32", - "pin-project-lite 0.2.15", - "tokio 1.42.1", - "tokio-io-timeout", + "csv", + "encode_unicode", + "is-terminal", + "lazy_static", + "term 0.7.0", + "unicode-width", ] [[package]] -name = "hyper-tls" -version = "0.4.3" +name = "primeorder" +version = "0.13.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d979acc56dcb5b8dddba3917601745e877576475aa046df3226eabdecef78eed" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" dependencies = [ - "bytes 0.5.6", - "hyper 0.13.10", - "native-tls", - "tokio 0.2.25", - "tokio-tls", + "elliptic-curve", ] [[package]] -name = "i18n-config" -version = "0.4.7" +name = "priority-queue" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e88074831c0be5b89181b05e6748c4915f77769ecc9a4c372f88b169a8509c9" +checksum = "93980406f12d9f8140ed5abe7155acb10bb1e69ea55c88960b9c2f117445ef96" dependencies = [ - "basic-toml", - "log", + "equivalent", + "indexmap 2.14.0", "serde", - "serde_derive", - "thiserror", - "unic-langid", ] [[package]] -name = "i18n-embed" -version = "0.13.9" +name = "proc-macro-crate" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92a86226a7a16632de6723449ee5fe70bac5af718bc642ee9ca2f0f6e14fa1fa" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "arc-swap", - "fluent", - "fluent-langneg", - "fluent-syntax", - "i18n-embed-impl", - "intl-memoizer", - "lazy_static", - "log", - "parking_lot 0.12.3", - "rust-embed", - "thiserror", - "unic-langid", - "walkdir", + "toml_edit 0.25.12+spec-1.1.0", ] [[package]] -name = "i18n-embed-fl" -version = "0.6.7" +name = "proc-macro-error" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26a3d3569737dfaac7fc1c4078e6af07471c3060b8e570bcd83cdd5f4685395" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" dependencies = [ - "dashmap", - "find-crate", - "fluent", - "fluent-syntax", - "i18n-config", - "i18n-embed", - "lazy_static", - "proc-macro-error", - "proc-macro2 1.0.89", - "quote 1.0.37", - "strsim 0.10.0", - "syn 2.0.86", - "unic-langid", + "proc-macro-error-attr", + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 1.0.109", + "version_check", ] [[package]] -name = "i18n-embed-impl" -version = "0.8.4" +name = "proc-macro-error-attr" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f2cc0e0523d1fe6fc2c6f66e5038624ea8091b3e7748b5e8e0c84b1698db6c2" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ - "find-crate", - "i18n-config", - "proc-macro2 1.0.89", - "quote 1.0.37", - "syn 2.0.86", + "proc-macro2 1.0.106", + "quote 1.0.46", + "version_check", ] [[package]] -name = "iana-time-zone" -version = "0.1.61" +name = "proc-macro2" +version = "0.4.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "wasm-bindgen", - "windows-core", + "unicode-xid 0.1.0", ] [[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" +name = "proc-macro2" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ - "cc", + "unicode-ident", ] [[package]] -name = "icu_collections" -version = "2.1.1" +name = "pwd-grp" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "0e2023f41b5fcb7c30eb5300a5733edfaa9e0e0d502d51b586f65633fd39e40c" dependencies = [ - "displaydoc", - "potential_utf", - "yoke", - "zerofrom", - "zerovec", + "derive-deftly", + "libc", + "paste", + "thiserror 2.0.18", ] [[package]] -name = "icu_locale_core" -version = "2.1.1" +name = "qr_code" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5520fbcd7da152a449261c5a533a1c7fad044e9e8aa9528cfec3f464786c7926" + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quote" +version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" dependencies = [ - "displaydoc", - "litemap", - "tinystr 0.8.2", - "writeable", - "zerovec", + "proc-macro2 0.4.30", ] [[package]] -name = "icu_normalizer" -version = "2.1.1" +name = "quote" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", + "proc-macro2 1.0.106", ] [[package]] -name = "icu_normalizer_data" -version = "2.1.1" +name = "r-efi" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] -name = "icu_properties" -version = "2.1.2" +name = "r-efi" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] -name = "icu_properties_data" -version = "2.1.2" +name = "radium" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" [[package]] -name = "icu_provider" -version = "2.1.1" +name = "radix_trie" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", + "endian-type", + "nibble_vec", ] [[package]] -name = "idna" -version = "1.1.0" +name = "rand" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +checksum = "c618c47cd3ebd209790115ab837de41425723956ad3ce2e6a7f09890947cacb9" dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", + "cloudabi", + "fuchsia-cprng", + "libc", + "rand_core 0.3.1", + "winapi", ] [[package]] -name = "idna_adapter" -version = "1.2.1" +name = "rand" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" dependencies = [ - "icu_normalizer", - "icu_properties", + "autocfg 0.1.8", + "libc", + "rand_chacha 0.1.1", + "rand_core 0.4.2", + "rand_hc 0.1.0", + "rand_isaac", + "rand_jitter 0.1.4", + "rand_os", + "rand_pcg", + "rand_xorshift", + "winapi", ] [[package]] -name = "indexmap" -version = "1.9.3" +name = "rand" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" dependencies = [ - "autocfg 1.4.0", - "hashbrown 0.12.3", + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc 0.2.0", ] [[package]] -name = "indexmap" -version = "2.6.0" +name = "rand" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ - "equivalent", - "hashbrown 0.15.0", + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", ] [[package]] -name = "intl-memoizer" -version = "0.5.2" +name = "rand" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe22e020fce238ae18a6d5d8c502ee76a52a6e880d99477657e6acc30ec57bda" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ - "type-map", - "unic-langid", + "rand_chacha 0.9.0", + "rand_core 0.9.5", ] [[package]] -name = "intl_pluralrules" -version = "7.0.2" +name = "rand" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "078ea7b7c29a2b4df841a7f6ac8775ff6074020c6776d48491ce2268e068f972" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ - "unic-langid", + "chacha20 0.10.1", + "getrandom 0.4.3", + "rand_core 0.10.1", ] [[package]] -name = "iovec" -version = "0.1.4" +name = "rand_chacha" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" +checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" dependencies = [ - "libc", + "autocfg 0.1.8", + "rand_core 0.3.1", ] [[package]] -name = "ipnet" -version = "2.10.1" +name = "rand_chacha" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] [[package]] -name = "is-terminal" -version = "0.4.13" +name = "rand_chacha" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ - "hermit-abi 0.4.0", - "libc", - "windows-sys 0.52.0", + "ppv-lite86", + "rand_core 0.6.4", ] [[package]] -name = "itoa" -version = "0.4.8" +name = "rand_chacha" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] [[package]] -name = "itoa" -version = "1.0.11" +name = "rand_chacha" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "3e6af7f3e25ded52c41df4e0b1af2d047e45896c2f3281792ed68a1c243daedb" +dependencies = [ + "ppv-lite86", + "rand_core 0.10.1", +] [[package]] -name = "jobserver" -version = "0.1.32" +name = "rand_core" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" dependencies = [ - "libc", + "rand_core 0.4.2", ] [[package]] -name = "js-sys" -version = "0.3.72" +name = "rand_core" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" dependencies = [ - "wasm-bindgen", + "getrandom 0.1.16", ] [[package]] -name = "jsonrpc-core" -version = "10.1.0" +name = "rand_core" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc15eef5f8b6bef5ac5f7440a957ff95d036e2f98706947741bfc93d1976db4c" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "futures 0.1.31", - "log", - "serde", - "serde_derive", - "serde_json", + "getrandom 0.2.17", ] [[package]] -name = "keccak" -version = "0.1.5" +name = "rand_core" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ - "cpufeatures", + "getrandom 0.3.4", ] [[package]] -name = "kernel32-sys" -version = "0.2.2" +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_hc" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" dependencies = [ - "winapi 0.2.8", - "winapi-build", + "rand_core 0.3.1", ] [[package]] -name = "lazy_static" -version = "1.5.0" +name = "rand_hc" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] [[package]] -name = "libc" -version = "0.2.161" +name = "rand_isaac" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" +checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" +dependencies = [ + "rand_core 0.3.1", +] [[package]] -name = "libgit2-sys" -version = "0.18.3+1.9.2" +name = "rand_jitter" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9b3acc4b91781bb0b3386669d325163746af5f6e4f73e6d2d630e09a35f3487" +checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" dependencies = [ - "cc", "libc", - "libz-sys", - "pkg-config", + "rand_core 0.4.2", + "winapi", ] [[package]] -name = "liblmdb-sys" -version = "0.2.2" +name = "rand_jitter" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "feed38a3a580f60bf61aaa067b0ff4123395966839adeaf67258a9e50c4d2e49" +checksum = "3fdcd80e68f0a8f9ca5ec7cfd02fd5fbb8fbe6ef4e9b90ea2f48bb929b74f88e" dependencies = [ - "gcc", "libc", + "rand_core 0.10.1", + "windows-sys 0.59.0", ] [[package]] -name = "libredox" +name = "rand_os" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" dependencies = [ - "bitflags 2.6.0", + "cloudabi", + "fuchsia-cprng", "libc", + "rand_core 0.4.2", + "rdrand 0.4.0", + "winapi", ] [[package]] -name = "libz-sys" -version = "1.1.20" +name = "rand_pcg" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2d16453e800a8cf6dd2fc3eb4bc99b786a9b90c663b8559a5b1a041bf89e472" +checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", + "autocfg 0.1.8", + "rand_core 0.4.2", ] [[package]] -name = "linefeed" -version = "0.6.0" +name = "rand_xorshift" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28715d08e35c6c074f9ae6b2e6a2420bac75d050c66ecd669d7d5b98e2caa036" +checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" dependencies = [ - "dirs 1.0.5", - "mortal", - "winapi 0.3.9", + "rand_core 0.3.1", ] [[package]] -name = "linked-hash-map" -version = "0.5.6" +name = "rangemap" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" +checksum = "973443cf09a9c8656b574a866ab68dfa19f0867d0340648c7d2f6a71b8a8ea68" [[package]] -name = "linux-raw-sys" -version = "0.4.14" +name = "rayon" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] [[package]] -name = "litemap" -version = "0.8.1" +name = "rayon-core" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] [[package]] -name = "lmdb-zero" -version = "0.4.4" +name = "rdrand" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13416eee745b087c22934f35f1f24da22da41ba2a5ce197143d168ce055cc58d" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" dependencies = [ - "bitflags 0.9.1", - "libc", - "liblmdb-sys", - "supercow", + "rand_core 0.3.1", ] [[package]] -name = "lock_api" -version = "0.3.4" +name = "rdrand" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4da24a77a3d8a6d4862d95f72e6fdb9c09a643ecdb402d754004a557f2bec75" +checksum = "d92195228612ac8eed47adbc2ed0f04e513a4ccb98175b6f2bd04d963b533655" dependencies = [ - "scopeguard", + "rand_core 0.6.4", ] [[package]] -name = "lock_api" -version = "0.4.12" +name = "redox_syscall" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" + +[[package]] +name = "redox_syscall" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "autocfg 1.4.0", - "scopeguard", + "bitflags 2.13.0", ] [[package]] -name = "log" -version = "0.4.22" +name = "redox_users" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d" dependencies = [ - "serde", + "getrandom 0.1.16", + "redox_syscall 0.1.57", + "rust-argon2", ] [[package]] -name = "log-mdc" -version = "0.1.0" +name = "redox_users" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a94d21414c1f4a51209ad204c1776a3d0765002c76c6abcb602a6f09f1e881c7" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] [[package]] -name = "log4rs" -version = "1.3.0" +name = "redox_users" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0816135ae15bd0391cf284eab37e6e3ee0a6ee63d2ceeb659862bd8d0a984ca6" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ - "anyhow", - "arc-swap", - "chrono", - "derivative", - "flate2", - "fnv", - "humantime", - "libc", - "log", - "log-mdc", - "once_cell", - "parking_lot 0.12.3", - "rand 0.8.5", - "serde", - "serde-value", - "serde_json", - "serde_yaml", - "thiserror", - "thread-id", - "typemap-ors", - "winapi 0.3.9", + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.18", ] [[package]] -name = "lru-cache" -version = "0.1.2" +name = "ref-cast" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" dependencies = [ - "linked-hash-map", + "ref-cast-impl", ] [[package]] -name = "memchr" -version = "2.7.4" +name = "ref-cast-impl" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", +] [[package]] -name = "memmap" -version = "0.7.0" +name = "regex" +version = "1.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6585fd95e7bb50d6cc31e20d4cf9afb4e2ba16c5846fc76793f11218da9c475b" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" dependencies = [ - "libc", - "winapi 0.3.9", + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", ] [[package]] -name = "mime" -version = "0.3.17" +name = "regex-automata" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] [[package]] -name = "mime_guess" -version = "2.0.5" +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" dependencies = [ + "base64 0.22.1", + "bytes 1.12.0", + "encoding_rs", + "futures-core", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", "mime", - "unicase", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", ] [[package]] -name = "minimal-lexical" -version = "0.2.1" +name = "reseeding_rng" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +checksum = "ecfc8e855ab517235426ec1290bfb0745e73fa478b29619eb66a2a81be9d8716" +dependencies = [ + "rand_core 0.10.1", +] [[package]] -name = "miniz_oxide" -version = "0.8.0" +name = "retry-error" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +checksum = "e7c0e06f9b501df8600ad8bd073f4990f703f36e0f48e574b431f4a15d69b449" dependencies = [ - "adler2", + "humantime 2.4.0", + "web-time", ] [[package]] -name = "mio" -version = "0.6.23" +name = "rfc6979" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" dependencies = [ - "cfg-if 0.1.10", - "fuchsia-zircon", - "fuchsia-zircon-sys", - "iovec", - "kernel32-sys", - "libc", - "log", - "miow 0.2.2", - "net2", - "slab", - "winapi 0.2.8", + "hmac 0.12.1", + "subtle", ] [[package]] -name = "mio" -version = "1.1.0" +name = "ring" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ + "cc", + "cfg-if 1.0.4", + "getrandom 0.2.17", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.61.2", + "untrusted", + "windows-sys 0.52.0", ] [[package]] -name = "mio-named-pipes" -version = "0.1.7" +name = "ripemd160" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0840c1c50fd55e521b247f949c241c9997709f23bd7f023b9762cd561e935656" +checksum = "2eca4ecc81b7f313189bf73ce724400a07da2a6dac19588b03c8bd76a2dcc251" dependencies = [ - "log", - "mio 0.6.23", - "miow 0.3.7", - "winapi 0.3.9", + "block-buffer 0.9.0", + "digest 0.9.0", + "opaque-debug 0.3.1", ] [[package]] -name = "mio-uds" -version = "0.6.8" +name = "rpassword" +version = "7.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afcb699eb26d4332647cc848492bbc15eafb26f08d0304550d5aa1f612e066f0" +checksum = "2da316a15f47e3d053de9cb2c439650bd8fa4aaeb9365f2e5f27f492ff73c196" dependencies = [ - "iovec", "libc", - "mio 0.6.23", + "rtoolbox", + "windows-sys 0.61.2", ] [[package]] -name = "miow" -version = "0.2.2" +name = "rsa" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" dependencies = [ - "kernel32-sys", - "net2", - "winapi 0.2.8", - "ws2_32-sys", + "const-oid 0.9.6", + "digest 0.10.7", + "num-bigint-dig", + "num-integer", + "num-traits 0.2.19", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "sha2 0.10.9", + "signature", + "spki", + "subtle", + "zeroize", ] [[package]] -name = "miow" -version = "0.3.7" +name = "rsqlite-vfs" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" +checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c" dependencies = [ - "winapi 0.3.9", + "hashbrown 0.16.1", + "thiserror 2.0.18", ] [[package]] -name = "mortal" -version = "0.2.4" +name = "rtoolbox" +version = "0.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c624fa1b7aab6bd2aff6e9b18565cc0363b6d45cbcd7465c9ed5e3740ebf097" +checksum = "50a0e551c1e27e1731aba276dbeaeac73f53c7cd34d1bda485d02bd1e0f36844" dependencies = [ - "bitflags 2.6.0", "libc", - "nix 0.26.4", - "smallstr", - "terminfo", - "unicode-normalization", - "unicode-width", - "winapi 0.3.9", + "windows-sys 0.59.0", ] [[package]] -name = "native-tls" -version = "0.2.12" +name = "rusqlite" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +checksum = "a0d2b0146dd9661bf67bb107c0bb2a55064d556eeb3fc314151b957f313bcd4e" dependencies = [ - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", + "bitflags 2.13.0", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", + "sqlite-wasm-rs", + "time", +] + +[[package]] +name = "rust-argon2" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb" +dependencies = [ + "base64 0.13.1", + "blake2b_simd", + "constant_time_eq", + "crossbeam-utils", ] [[package]] -name = "net2" -version = "0.2.39" +name = "rust-embed" +version = "6.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b13b648036a2339d06de780866fbdfda0dde886de7b3af2ddeba8b14f4ee34ac" +checksum = "a36224c3276f8c4ebc8c20f158eca7ca4359c8db89991c4925132aaaf6702661" dependencies = [ - "cfg-if 0.1.10", - "libc", - "winapi 0.3.9", + "rust-embed-impl", + "rust-embed-utils", + "walkdir", ] [[package]] -name = "nix" -version = "0.18.0" +name = "rust-embed-impl" +version = "6.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83450fe6a6142ddd95fb064b746083fc4ef1705fe81f64a64e1d4b39f54a1055" +checksum = "49b94b81e5b2c284684141a2fb9e2a31be90638caf040bf9afbc5a0416afe1ac" dependencies = [ - "bitflags 1.3.2", - "cc", - "cfg-if 0.1.10", - "libc", + "proc-macro2 1.0.106", + "quote 1.0.46", + "rust-embed-utils", + "syn 2.0.118", + "walkdir", ] [[package]] -name = "nix" -version = "0.26.4" +name = "rust-embed-utils" +version = "7.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +checksum = "9d38ff6bf570dc3bb7100fce9f7b60c33fa71d80e88da3f2580df4ff2bdded74" dependencies = [ - "bitflags 1.3.2", - "cfg-if 1.0.0", - "libc", + "sha2 0.10.9", + "walkdir", ] [[package]] -name = "nodrop" -version = "0.1.14" +name = "rustc-demangle" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" [[package]] -name = "nom" -version = "7.1.3" +name = "rustc-hash" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] -name = "ntapi" +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ - "winapi 0.3.9", + "semver", ] [[package]] -name = "num" -version = "0.2.1" +name = "rusticata-macros" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8536030f9fea7127f841b45bb6243b27255787fb4eb83958aa1ef9d2fdc0c36" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" dependencies = [ - "num-bigint", - "num-complex", - "num-integer", - "num-iter", - "num-rational", - "num-traits 0.2.19", + "nom", ] [[package]] -name = "num-bigint" -version = "0.2.6" +name = "rustix" +version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "autocfg 1.4.0", - "num-integer", - "num-traits 0.2.19", + "bitflags 2.13.0", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", ] [[package]] -name = "num-complex" -version = "0.2.4" +name = "rustix" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6b19411a9719e753aff12e5187b74d60d3dc449ec3f4dc21e3989c3f554bc95" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "autocfg 1.4.0", - "num-traits 0.2.19", + "bitflags 2.13.0", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", ] [[package]] -name = "num-integer" -version = "0.1.46" +name = "rustls" +version = "0.23.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" dependencies = [ - "num-traits 0.2.19", + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", ] [[package]] -name = "num-iter" -version = "0.1.45" +name = "rustls-native-certs" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" dependencies = [ - "autocfg 1.4.0", - "num-integer", - "num-traits 0.2.19", + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", ] [[package]] -name = "num-rational" -version = "0.2.4" +name = "rustls-pemfile" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c000134b5dbf44adc5cb772486d335293351644b801551abe8f75c84cfa4aef" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" dependencies = [ - "autocfg 1.4.0", - "num-bigint", - "num-integer", - "num-traits 0.2.19", + "base64 0.21.7", ] [[package]] -name = "num-traits" -version = "0.1.43" +name = "rustls-pki-types" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31" +checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" dependencies = [ - "num-traits 0.2.19", + "zeroize", ] [[package]] -name = "num-traits" -version = "0.2.19" +name = "rustls-webpki" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ - "autocfg 1.4.0", + "ring", + "rustls-pki-types", + "untrusted", ] [[package]] -name = "num_cpus" -version = "1.16.0" +name = "rustversion" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi 0.3.9", - "libc", -] +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] -name = "object" -version = "0.36.5" +name = "rustyline" +version = "9.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +checksum = "db7826789c0e25614b03e5a54a0717a86f9ff6e6e5247f92b369472869320039" dependencies = [ + "bitflags 1.3.2", + "cfg-if 1.0.4", + "clipboard-win", + "dirs-next", + "fd-lock", + "libc", + "log", "memchr", + "nix 0.23.2", + "radix_trie", + "scopeguard", + "smallvec", + "unicode-segmentation", + "unicode-width", + "utf8parse", + "winapi", ] [[package]] -name = "once_cell" -version = "1.20.2" +name = "ryu" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] -name = "opaque-debug" -version = "0.2.3" +name = "safelog" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" +checksum = "4c0812d386a9992c036500b92565c41f84c28fd2e48fe853a395c393261b730b" +dependencies = [ + "derive_more", + "educe", + "either", + "fluid-let", + "thiserror 2.0.18", +] [[package]] -name = "opaque-debug" -version = "0.3.1" +name = "safemem" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" [[package]] -name = "openssl" -version = "0.10.80" +name = "salsa20" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" dependencies = [ - "bitflags 2.6.0", - "cfg-if 1.0.0", - "foreign-types", - "libc", - "openssl-macros", - "openssl-sys", + "cipher 0.4.4", ] [[package]] -name = "openssl-macros" -version = "0.1.1" +name = "same-file" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" dependencies = [ - "proc-macro2 1.0.89", - "quote 1.0.37", - "syn 2.0.86", + "winapi-util", ] [[package]] -name = "openssl-probe" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" - -[[package]] -name = "openssl-sys" -version = "0.9.116" +name = "sanitize-filename" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4" +checksum = "bc984f4f9ceb736a7bb755c3e3bd17dc56370af2600c9780dcc48c66453da34d" dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", + "regex", ] [[package]] -name = "ordered-float" -version = "2.10.1" +name = "saturating-time" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" -dependencies = [ - "num-traits 0.2.19", -] +checksum = "802bdbfcca9a239cb46eeaaedea507e37bb13ba1a673762d2e2ef7a9dac63144" [[package]] -name = "output_vt100" -version = "0.1.3" +name = "schannel" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "628223faebab4e3e40667ee0b2336d34a5b960ff60ea743ddfdbcf7770bcfb66" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ - "winapi 0.3.9", + "windows-sys 0.61.2", ] [[package]] -name = "parking_lot" -version = "0.10.2" +name = "schemars" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3a704eb390aafdc107b0e392f56a82b668e3a71366993b5340f5833fd62505e" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" dependencies = [ - "lock_api 0.3.4", - "parking_lot_core 0.7.3", + "dyn-clone", + "ref-cast", + "serde", + "serde_json", ] [[package]] -name = "parking_lot" -version = "0.12.3" +name = "schemars" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" dependencies = [ - "lock_api 0.4.12", - "parking_lot_core 0.9.10", + "dyn-clone", + "ref-cast", + "serde", + "serde_json", ] [[package]] -name = "parking_lot_core" -version = "0.7.3" +name = "scopeguard" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b93f386bb233083c799e6e642a9d73db98c24a5deeb95ffc85bf281255dffc98" -dependencies = [ - "cfg-if 0.1.10", - "cloudabi", - "libc", - "redox_syscall 0.1.57", - "smallvec", - "winapi 0.3.9", -] +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] -name = "parking_lot_core" -version = "0.9.10" +name = "scrypt" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "ba0aaf3911fff0d942c10a49779de7754699810fc7dbe3df515613b2ecc8195a" dependencies = [ - "cfg-if 1.0.0", - "libc", - "redox_syscall 0.5.7", - "smallvec", - "windows-targets", + "hmac 0.12.1", + "pbkdf2 0.10.1", + "salsa20", + "sha2 0.10.9", ] [[package]] -name = "password-hash" -version = "0.2.3" +name = "sec1" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77e0b28ace46c5a396546bcf443bf422b57049617433d8854227352a4a9b24e7" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ - "base64ct", - "rand_core 0.6.4", + "base16ct", + "der", + "generic-array 0.14.7", + "pkcs8", "subtle", + "zeroize", ] [[package]] -name = "pbkdf2" +name = "secrecy" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d95f5254224e617595d2cc3cc73ff0a5eaf2637519e25f03388154e9378b6ffa" +checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e" dependencies = [ - "base64ct", - "crypto-mac", - "hmac 0.11.0", - "password-hash", - "sha2 0.9.9", + "zeroize", ] [[package]] -name = "pbkdf2" -version = "0.10.1" +name = "security-framework" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271779f35b581956db91a3e55737327a03aa051e90b1c47aeb189508533adfd7" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "digest 0.10.7", + "bitflags 2.13.0", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", ] [[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "phf" -version = "0.11.2" +name = "security-framework-sys" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ - "phf_shared", + "core-foundation-sys", + "libc", ] [[package]] -name = "phf_codegen" -version = "0.11.2" +name = "self_cell" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" +checksum = "e14e4d63b804dc0c7ec4a1e52bcb63f02c7ac94476755aa579edac21e01f915d" dependencies = [ - "phf_generator", - "phf_shared", + "self_cell 1.2.2", ] [[package]] -name = "phf_generator" -version = "0.11.2" +name = "self_cell" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" -dependencies = [ - "phf_shared", - "rand 0.8.5", -] +checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" [[package]] -name = "phf_shared" -version = "0.11.2" +name = "semver" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" -dependencies = [ - "siphasher", -] +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] -name = "pin-project" -version = "1.1.7" +name = "serde" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be57f64e946e500c8ee36ef6331845d40a93055567ec57e8fae13efd33759b95" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ - "pin-project-internal", + "serde_core", + "serde_derive", ] [[package]] -name = "pin-project-internal" -version = "1.1.7" +name = "serde-value" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" dependencies = [ - "proc-macro2 1.0.89", - "quote 1.0.37", - "syn 2.0.86", + "ordered-float", + "serde", ] [[package]] -name = "pin-project-lite" -version = "0.1.12" +name = "serde_bytes" +version = "0.11.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "257b64915a082f7811703966789728173279bdebb956b143dbcd23f6f970a777" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] [[package]] -name = "pin-project-lite" -version = "0.2.15" +name = "serde_core" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] [[package]] -name = "pin-utils" -version = "0.1.0" +name = "serde_derive" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", +] [[package]] -name = "pkg-config" -version = "0.3.31" +name = "serde_ignored" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +checksum = "115dffd5f3853e06e746965a20dcbae6ee747ae30b543d91b0e089668bb07798" +dependencies = [ + "serde", + "serde_core", +] [[package]] -name = "poly1305" -version = "0.7.2" +name = "serde_json" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "048aeb476be11a4b6ca432ca569e375810de9294ae78f4774e78ea98a9246ede" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ - "cpufeatures", - "opaque-debug 0.3.1", - "universal-hash", + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", ] [[package]] -name = "potential_utf" -version = "0.1.4" +name = "serde_spanned" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" dependencies = [ - "zerovec", + "serde", ] [[package]] -name = "ppv-lite86" -version = "0.2.20" +name = "serde_spanned" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ - "zerocopy", + "serde_core", ] [[package]] -name = "pretty_assertions" -version = "0.6.1" +name = "serde_with" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f81e1644e1b54f5a68959a29aa86cde704219254669da328ecfdf6a1f09d427" +checksum = "678b5a069e50bf00ecd22d0cd8ddf7c236f68581b03db652061ed5eb13a312ff" dependencies = [ - "ansi_term 0.11.0", - "ctor", - "difference", - "output_vt100", + "chrono", + "serde", + "serde_with_macros 1.5.2", ] [[package]] -name = "prettytable-rs" -version = "0.10.0" +name = "serde_with" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eea25e07510aa6ab6547308ebe3c036016d162b8da920dbb079e3ba8acf3d95a" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" dependencies = [ - "csv", - "encode_unicode", - "is-terminal", - "lazy_static", - "term 0.7.0", - "unicode-width", + "base64 0.22.1", + "bs58 0.5.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros 3.21.0", + "time", ] [[package]] -name = "proc-macro-error" -version = "1.0.4" +name = "serde_with_macros" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082" dependencies = [ - "proc-macro-error-attr", - "proc-macro2 1.0.89", - "quote 1.0.37", + "darling 0.13.4", + "proc-macro2 1.0.106", + "quote 1.0.46", "syn 1.0.109", - "version_check", ] [[package]] -name = "proc-macro-error-attr" -version = "1.0.4" +name = "serde_with_macros" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" dependencies = [ - "proc-macro2 1.0.89", - "quote 1.0.37", - "version_check", + "darling 0.23.0", + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", ] [[package]] -name = "proc-macro2" -version = "0.4.30" +name = "serde_yaml" +version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "unicode-xid", + "indexmap 2.14.0", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", ] [[package]] -name = "proc-macro2" -version = "1.0.89" +name = "sha1" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ - "unicode-ident", + "cfg-if 1.0.4", + "cpufeatures 0.2.17", + "digest 0.10.7", ] [[package]] -name = "qr_code" -version = "1.1.0" +name = "sha2" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5520fbcd7da152a449261c5a533a1c7fad044e9e8aa9528cfec3f464786c7926" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if 1.0.4", + "cpufeatures 0.2.17", + "digest 0.9.0", + "opaque-debug 0.3.1", +] [[package]] -name = "quote" -version = "0.6.13" +name = "sha2" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ - "proc-macro2 0.4.30", + "cfg-if 1.0.4", + "cpufeatures 0.2.17", + "digest 0.10.7", ] [[package]] -name = "quote" -version = "1.0.37" +name = "sha3" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +checksum = "dd26bc0e7a2e3a7c959bc494caf58b72ee0c71d67704e9520f736ca7e4853ecf" dependencies = [ - "proc-macro2 1.0.89", + "block-buffer 0.7.3", + "byte-tools", + "digest 0.8.1", + "keccak 0.1.6", + "opaque-debug 0.2.3", ] [[package]] -name = "rand" -version = "0.5.6" +name = "sha3" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c618c47cd3ebd209790115ab837de41425723956ad3ce2e6a7f09890947cacb9" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" dependencies = [ - "cloudabi", - "fuchsia-cprng", - "libc", - "rand_core 0.3.1", - "winapi 0.3.9", + "digest 0.10.7", + "keccak 0.1.6", ] [[package]] -name = "rand" -version = "0.6.5" +name = "sha3" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" +checksum = "bc9bad02c26382724b2d2692c6f179285e4b54eeecd7968f52a50059c3c11759" dependencies = [ - "autocfg 0.1.8", - "libc", - "rand_chacha 0.1.1", - "rand_core 0.4.2", - "rand_hc 0.1.0", - "rand_isaac", - "rand_jitter", - "rand_os", - "rand_pcg", - "rand_xorshift", - "winapi 0.3.9", + "digest 0.11.3", + "keccak 0.2.0", + "sponge-cursor", ] [[package]] -name = "rand" -version = "0.7.3" +name = "sharded-slab" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" dependencies = [ - "getrandom 0.1.16", - "libc", - "rand_chacha 0.2.2", - "rand_core 0.5.1", - "rand_hc 0.2.0", + "lazy_static", ] [[package]] -name = "rand" -version = "0.8.5" +name = "shellexpand" +version = "3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "32824fab5e16e6c4d86dc1ba84489390419a39f97699852b66480bb87d297ed8" dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", + "bstr", + "dirs 6.0.0", + "os_str_bytes", ] [[package]] -name = "rand_chacha" -version = "0.1.1" +name = "shlex" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" -dependencies = [ - "autocfg 0.1.8", - "rand_core 0.3.1", -] +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] -name = "rand_chacha" -version = "0.2.2" +name = "signal-hook-registry" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ - "ppv-lite86", - "rand_core 0.5.1", + "errno", + "libc", ] [[package]] -name = "rand_chacha" -version = "0.3.1" +name = "signature" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "ppv-lite86", + "digest 0.10.7", "rand_core 0.6.4", ] [[package]] -name = "rand_core" -version = "0.3.1" +name = "simd-adler32" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" -dependencies = [ - "rand_core 0.4.2", -] +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] -name = "rand_core" -version = "0.4.2" +name = "siphasher" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" [[package]] -name = "rand_core" -version = "0.5.1" +name = "siphasher" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" -dependencies = [ - "getrandom 0.1.16", -] +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" [[package]] -name = "rand_core" -version = "0.6.4" +name = "slab" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" dependencies = [ - "getrandom 0.2.15", + "serde", + "version_check", ] [[package]] -name = "rand_hc" -version = "0.1.0" +name = "slotmap-careful" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" +checksum = "e7cd5208f878dce99cfbefa73c692c67edf946ac53e0cc4455e2dabe43d54af7" dependencies = [ - "rand_core 0.3.1", + "paste", + "serde", + "slotmap", + "thiserror 2.0.18", + "void", ] [[package]] -name = "rand_hc" +name = "smallstr" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +checksum = "1e922794d168678729ffc7e07182721a14219c65814e66e91b839a272fe5ae4f" dependencies = [ - "rand_core 0.5.1", + "smallvec", ] [[package]] -name = "rand_isaac" -version = "0.1.1" +name = "smallvec" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" -dependencies = [ - "rand_core 0.3.1", -] +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] -name = "rand_jitter" -version = "0.1.4" +name = "socket2" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", - "rand_core 0.4.2", - "winapi 0.3.9", + "windows-sys 0.61.2", ] [[package]] -name = "rand_os" -version = "0.1.3" +name = "spin" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" -dependencies = [ - "cloudabi", - "fuchsia-cprng", - "libc", - "rand_core 0.4.2", - "rdrand", - "winapi 0.3.9", -] +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" [[package]] -name = "rand_pcg" -version = "0.1.2" +name = "spki" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ - "autocfg 0.1.8", - "rand_core 0.4.2", + "base64ct", + "der", ] [[package]] -name = "rand_xorshift" -version = "0.1.1" +name = "sponge-cursor" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" +checksum = "3a0219bd7d979d58245a4f41f695e1ac9f8befdffadd7f61f1bae9e39abc6620" + +[[package]] +name = "sqlite-wasm-rs" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc3efc0da82635d7e1ced0053bbbfa8c7ab9645d0bf36ceb4f7127bb85315d75" dependencies = [ - "rand_core 0.3.1", + "cc", + "js-sys", + "rsqlite-vfs", + "wasm-bindgen", ] [[package]] -name = "rayon" -version = "1.10.0" +name = "ssh-cipher-fork-arti" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +checksum = "125c5795103fc93fced42d123c8044180afc55469caa1ab56487c3c5543c898d" dependencies = [ - "either", - "rayon-core", + "cipher 0.4.4", + "ssh-encoding-fork-arti", ] [[package]] -name = "rayon-core" -version = "1.12.1" +name = "ssh-encoding-fork-arti" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +checksum = "e0cf03c3a7ea88451ff83a129a79451fd9891d44fc76c25e916a11848b81814c" dependencies = [ - "crossbeam-deque", - "crossbeam-utils", + "base64ct", + "pem-rfc7468", + "sha2 0.10.9", ] [[package]] -name = "rdrand" -version = "0.4.0" +name = "ssh-key-fork-arti" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +checksum = "433782176b73ea7907763dc314c4a17438a231864d4aa683ba47c07c1cf0a388" dependencies = [ - "rand_core 0.3.1", + "num-bigint-dig", + "p256", + "p384", + "p521", + "rand_core 0.6.4", + "rsa", + "sec1", + "sha2 0.10.9", + "signature", + "ssh-cipher-fork-arti", + "ssh-encoding-fork-arti", + "subtle", + "zeroize", ] [[package]] -name = "redox_syscall" -version = "0.1.57" +name = "stable_deref_trait" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] -name = "redox_syscall" -version = "0.5.7" +name = "static_assertions" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" -dependencies = [ - "bitflags 2.6.0", -] +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] -name = "redox_users" -version = "0.3.5" +name = "str-buf" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d" -dependencies = [ - "getrandom 0.1.16", - "redox_syscall 0.1.57", - "rust-argon2", -] +checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" [[package]] -name = "redox_users" -version = "0.4.6" +name = "strsim" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" -dependencies = [ - "getrandom 0.2.15", - "libredox", - "thiserror", -] +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" [[package]] -name = "regex" -version = "1.11.1" +name = "strsim" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] -name = "regex-automata" -version = "0.4.8" +name = "strsim" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] -name = "regex-syntax" -version = "0.8.5" +name = "strum" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "57bd81eb48f4c437cadc685403cad539345bf703d78e63707418431cecd4522b" [[package]] -name = "remove_dir_all" -version = "0.7.0" +name = "strum" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "882f368737489ea543bc5c340e6f3d34a28c39980bd9a979e47322b26f60ac40" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" dependencies = [ - "libc", - "log", - "num_cpus", - "rayon", - "winapi 0.3.9", + "strum_macros 0.28.0", ] [[package]] -name = "reqwest" -version = "0.10.10" +name = "strum_macros" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0718f81a8e14c4dbb3b34cf23dc6aaf9ab8a0dfec160c534b3dbca1aaa21f47c" +checksum = "87c85aa3f8ea653bfd3ddf25f7ee357ee4d204731f6aa9ad04002306f6e2774c" dependencies = [ - "base64 0.13.1", - "bytes 0.5.6", - "encoding_rs", - "futures-core", - "futures-util", - "http", - "http-body 0.3.1", - "hyper 0.13.10", - "hyper-rustls 0.21.0", - "hyper-tls", - "ipnet", - "js-sys", - "lazy_static", - "log", - "mime", - "mime_guess", - "native-tls", - "percent-encoding", - "pin-project-lite 0.2.15", - "rustls 0.18.1", - "serde", - "serde_urlencoded", - "tokio 0.2.25", - "tokio-rustls 0.14.1", - "tokio-socks", - "tokio-tls", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "webpki-roots", - "winreg", + "heck 0.3.3", + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 1.0.109", ] [[package]] -name = "ring" -version = "0.16.20" +name = "strum_macros" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" dependencies = [ - "cc", - "libc", - "once_cell", - "spin", - "untrusted 0.7.1", - "web-sys", - "winapi 0.3.9", + "heck 0.5.0", + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", ] [[package]] -name = "ring" -version = "0.17.14" +name = "subtle" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" -dependencies = [ - "cc", - "cfg-if 1.0.0", - "getrandom 0.2.15", - "libc", - "untrusted 0.9.0", - "windows-sys 0.52.0", -] +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] -name = "ripemd160" -version = "0.9.1" +name = "syn" +version = "0.15.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eca4ecc81b7f313189bf73ce724400a07da2a6dac19588b03c8bd76a2dcc251" +checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5" dependencies = [ - "block-buffer 0.9.0", - "digest 0.9.0", - "opaque-debug 0.3.1", + "proc-macro2 0.4.30", + "quote 0.6.13", + "unicode-xid 0.1.0", ] [[package]] -name = "rpassword" -version = "4.0.5" +name = "syn" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99371657d3c8e4d816fb6221db98fa408242b0b53bac08f8676a41f8554fe99f" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ - "libc", - "winapi 0.3.9", + "proc-macro2 1.0.106", + "quote 1.0.46", + "unicode-ident", ] [[package]] -name = "rust-argon2" -version = "0.8.3" +name = "syn" +version = "2.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" dependencies = [ - "base64 0.13.1", - "blake2b_simd", - "constant_time_eq", - "crossbeam-utils", + "proc-macro2 1.0.106", + "quote 1.0.46", + "unicode-ident", ] [[package]] -name = "rust-embed" -version = "6.8.1" +name = "sync_wrapper" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a36224c3276f8c4ebc8c20f158eca7ca4359c8db89991c4925132aaaf6702661" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" dependencies = [ - "rust-embed-impl", - "rust-embed-utils", - "walkdir", + "futures-core", ] [[package]] -name = "rust-embed-impl" -version = "6.8.1" +name = "synchronoise" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49b94b81e5b2c284684141a2fb9e2a31be90638caf040bf9afbc5a0416afe1ac" +checksum = "3dbc01390fc626ce8d1cffe3376ded2b72a11bb70e1c75f404a210e4daa4def2" dependencies = [ - "proc-macro2 1.0.89", - "quote 1.0.37", - "rust-embed-utils", - "syn 2.0.86", - "walkdir", + "crossbeam-queue", ] [[package]] -name = "rust-embed-utils" -version = "7.8.1" +name = "synstructure" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d38ff6bf570dc3bb7100fce9f7b60c33fa71d80e88da3f2580df4ff2bdded74" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ - "sha2 0.10.8", - "walkdir", + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", ] [[package]] -name = "rustc-demangle" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" - -[[package]] -name = "rustc-hash" -version = "1.1.0" +name = "sysinfo" +version = "0.29.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +checksum = "cd727fc423c2060f6c92d9534cef765c65a6ed3f428a03d7def74a8c4348e666" +dependencies = [ + "cfg-if 1.0.4", + "core-foundation-sys", + "libc", + "ntapi", + "once_cell", + "rayon", + "winapi", +] [[package]] -name = "rustix" -version = "0.38.38" +name = "sysinfo" +version = "0.38.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa260229e6538e52293eeb577aabd09945a09d6d9cc0fc550ed7529056c2e32a" +checksum = "92ab6a2f8bfe508deb3c6406578252e491d299cbbf3bc0529ecc3313aee4a52f" dependencies = [ - "bitflags 2.6.0", - "errno", "libc", - "linux-raw-sys", - "windows-sys 0.52.0", + "memchr", + "ntapi", + "objc2-core-foundation", + "objc2-io-kit", + "windows", ] [[package]] -name = "rustls" -version = "0.18.1" +name = "system-configuration" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d1126dcf58e93cee7d098dbda643b5f92ed724f1f6a63007c1116eed6700c81" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ - "base64 0.12.3", - "log", - "ring 0.16.20", - "sct 0.6.1", - "webpki 0.21.4", + "bitflags 2.13.0", + "core-foundation 0.9.4", + "system-configuration-sys", ] [[package]] -name = "rustls" -version = "0.20.9" +name = "system-configuration-sys" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b80e3dec595989ea8510028f30c408a4630db12c9cbb8de34203b89d6577e99" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" dependencies = [ - "log", - "ring 0.16.20", - "sct 0.7.1", - "webpki 0.22.4", + "core-foundation-sys", + "libc", ] [[package]] -name = "rustls-native-certs" -version = "0.6.3" +name = "tap" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" -dependencies = [ - "openssl-probe", - "rustls-pemfile", - "schannel", - "security-framework", -] +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] -name = "rustls-pemfile" -version = "1.0.4" +name = "tempfile" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ - "base64 0.21.7", + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix 1.1.4", + "windows-sys 0.61.2", ] [[package]] -name = "rustversion" -version = "1.0.18" +name = "term" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" +checksum = "c0863a3345e70f61d613eab32ee046ccd1bcc5f9105fe402c61fcd0c13eeb8b5" +dependencies = [ + "dirs 2.0.2", + "winapi", +] [[package]] -name = "rustyline" -version = "6.3.0" +name = "term" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f0d5e7b0219a3eadd5439498525d4765c59b7c993ef0c12244865cd2d988413" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" dependencies = [ - "cfg-if 0.1.10", - "dirs-next 1.0.2", - "libc", - "log", - "memchr", - "nix 0.18.0", - "scopeguard", - "unicode-segmentation", - "unicode-width", - "utf8parse", - "winapi 0.3.9", + "dirs-next", + "rustversion", + "winapi", ] [[package]] -name = "ryu" -version = "1.0.18" +name = "termcolor" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] [[package]] -name = "safemem" -version = "0.3.3" +name = "terminfo" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" +checksum = "666cd3a6681775d22b200409aad3b089c5b99fb11ecdd8a204d9d62f8148498f" +dependencies = [ + "dirs 4.0.0", + "fnv", + "nom", + "phf 0.11.3", + "phf_codegen", +] [[package]] -name = "salsa20" -version = "0.9.0" +name = "textwrap" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0fbb5f676da676c260ba276a8f43a8dc67cf02d1438423aeb1c677a7212686" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" dependencies = [ - "cipher", + "unicode-width", ] [[package]] -name = "same-file" -version = "1.0.6" +name = "thiserror" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "winapi-util", + "thiserror-impl 1.0.69", ] [[package]] -name = "schannel" -version = "0.1.26" +name = "thiserror" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01227be5826fa0690321a2ba6c5cd57a19cf3f6a09e76973b58e61de6ab9d1c1" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "windows-sys 0.59.0", + "thiserror-impl 2.0.18", ] [[package]] -name = "scopeguard" -version = "1.2.0" +name = "thiserror-impl" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", +] [[package]] -name = "scrypt" -version = "0.8.1" +name = "thiserror-impl" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e73d6d7c6311ebdbd9184ad6c4447b2f36337e327bda107d3ba9e3c374f9d325" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ - "hmac 0.12.1", - "pbkdf2 0.10.1", - "salsa20", - "sha2 0.10.8", + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", ] [[package]] -name = "sct" -version = "0.6.1" +name = "thread-id" +version = "5.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce" +checksum = "2010d27add3f3240c1fef7959f46c814487b216baee662af53be645ba7831c07" dependencies = [ - "ring 0.16.20", - "untrusted 0.7.1", + "libc", + "windows-sys 0.61.2", ] [[package]] -name = "sct" -version = "0.7.1" +name = "thread_local" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" dependencies = [ - "ring 0.17.14", - "untrusted 0.9.0", + "cfg-if 1.0.4", ] [[package]] -name = "secrecy" -version = "0.6.0" +name = "time" +version = "0.3.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9182278ed645df3477a9c27bfee0621c621aa16f6972635f7f795dae3d81070f" +checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" dependencies = [ - "zeroize", + "deranged", + "js-sys", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", ] [[package]] -name = "secrecy" -version = "0.8.0" +name = "time-core" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" dependencies = [ - "zeroize", + "num-conv", + "time-core", ] [[package]] -name = "security-framework" -version = "2.11.1" +name = "timer" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +checksum = "31d42176308937165701f50638db1c31586f183f1aab416268216577aec7306b" dependencies = [ - "bitflags 2.6.0", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", + "chrono", ] [[package]] -name = "security-framework-sys" -version = "2.12.0" +name = "tinystr" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ - "core-foundation-sys", - "libc", + "displaydoc", + "serde_core", + "zerovec", ] [[package]] -name = "self_cell" -version = "0.10.3" +name = "tinyvec" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14e4d63b804dc0c7ec4a1e52bcb63f02c7ac94476755aa579edac21e01f915d" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ - "self_cell 1.0.4", + "tinyvec_macros", ] [[package]] -name = "self_cell" -version = "1.0.4" +name = "tinyvec_macros" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d369a96f978623eb3dc28807c4852d6cc617fed53da5d3c400feff1ef34a714a" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] -name = "semver" -version = "0.10.0" +name = "tokio" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "394cec28fa623e00903caf7ba4fa6fb9a0e260280bb8cdbbba029611108a0190" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ - "semver-parser", + "bytes 1.12.0", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", ] [[package]] -name = "semver-parser" -version = "0.7.0" +name = "tokio-macros" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", +] [[package]] -name = "serde" -version = "1.0.214" +name = "tokio-native-tls" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" dependencies = [ - "serde_derive", + "native-tls", + "tokio", ] [[package]] -name = "serde-value" -version = "0.7.0" +name = "tokio-rustls" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "ordered-float", - "serde", + "rustls", + "tokio", ] [[package]] -name = "serde_derive" -version = "1.0.214" +name = "tokio-util" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ - "proc-macro2 1.0.89", - "quote 1.0.37", - "syn 2.0.86", + "bytes 1.12.0", + "futures-core", + "futures-io", + "futures-sink", + "pin-project-lite", + "tokio", ] [[package]] -name = "serde_json" -version = "1.0.132" +name = "toml" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" dependencies = [ - "itoa 1.0.11", - "memchr", - "ryu", "serde", ] [[package]] -name = "serde_urlencoded" -version = "0.7.1" +name = "toml" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ - "form_urlencoded", - "itoa 1.0.11", - "ryu", "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", ] [[package]] -name = "serde_yaml" -version = "0.9.34+deprecated" +name = "toml" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" dependencies = [ - "indexmap 2.6.0", - "itoa 1.0.11", - "ryu", - "serde", - "unsafe-libyaml", + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.3", ] [[package]] -name = "sha2" -version = "0.9.9" +name = "toml_datetime" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" dependencies = [ - "block-buffer 0.9.0", - "cfg-if 1.0.0", - "cpufeatures", - "digest 0.9.0", - "opaque-debug 0.3.1", + "serde", ] [[package]] -name = "sha2" -version = "0.10.8" +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ - "cfg-if 1.0.0", - "cpufeatures", - "digest 0.10.7", + "serde_core", ] [[package]] -name = "sha3" -version = "0.8.2" +name = "toml_edit" +version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd26bc0e7a2e3a7c959bc494caf58b72ee0c71d67704e9520f736ca7e4853ecf" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "block-buffer 0.7.3", - "byte-tools", - "digest 0.8.1", - "keccak", - "opaque-debug 0.2.3", + "indexmap 2.14.0", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_write", + "winnow 0.7.15", ] [[package]] -name = "shlex" -version = "1.3.0" +name = "toml_edit" +version = "0.25.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.3", +] [[package]] -name = "signal-hook-registry" -version = "1.4.2" +name = "toml_parser" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "libc", + "winnow 1.0.3", ] [[package]] -name = "signature" -version = "1.6.4" +name = "toml_write" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] -name = "siphasher" -version = "0.3.11" +name = "toml_writer" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" [[package]] -name = "slab" -version = "0.4.9" +name = "tor-async-utils" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +checksum = "58580b41636378f79500bca6a178787e0e434207e0880dd90698ae3f686f24d6" dependencies = [ - "autocfg 1.4.0", + "cfg-if 1.0.4", + "derive-deftly", + "educe", + "futures 0.3.32", + "oneshot-fused-workaround", + "pin-project", + "postage", + "sync_wrapper", + "thiserror 2.0.18", + "tokio", + "tokio-util", + "tor-basic-utils", + "tor-rtcompat", + "tracing", + "void", + "web-time-compat", ] [[package]] -name = "smallstr" -version = "0.2.0" +name = "tor-basic-utils" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e922794d168678729ffc7e07182721a14219c65814e66e91b839a272fe5ae4f" +checksum = "dd3046aa299df67da91b45c02c24d48d40ae1845f9b91e27f9c8adbde78315ba" dependencies = [ + "derive-deftly", + "derive_more", + "getrandom 0.4.3", + "hex", + "itertools 0.14.0", + "libc", + "paste", + "rand 0.10.2", + "rand_chacha 0.10.0", + "serde", + "slab", "smallvec", + "thiserror 2.0.18", + "tracing", + "weak-table", + "web-time-compat", ] [[package]] -name = "smallvec" -version = "1.13.2" +name = "tor-bytes" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +checksum = "6b4833cc8ab3fca79eabc06f3ca97db102a3b7db861d77848d39932a6f0ed598" +dependencies = [ + "bytes 1.12.0", + "derive-deftly", + "digest 0.10.7", + "educe", + "getrandom 0.4.3", + "safelog", + "thiserror 2.0.18", + "tor-error", + "tor-llcrypto", + "zeroize", +] [[package]] -name = "socket2" -version = "0.3.19" +name = "tor-cell" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e" +checksum = "73a01cb2ac6697c12b647729d3d7db404e306d1e08a7a3aacdbee08e35170ebd" dependencies = [ - "cfg-if 1.0.0", - "libc", - "winapi 0.3.9", + "amplify", + "bitflags 2.13.0", + "bytes 1.12.0", + "caret", + "derive-deftly", + "derive_more", + "educe", + "itertools 0.14.0", + "paste", + "rand 0.10.2", + "smallvec", + "thiserror 2.0.18", + "tor-basic-utils", + "tor-bytes", + "tor-cert", + "tor-error", + "tor-hscrypto", + "tor-linkspec", + "tor-llcrypto", + "tor-memquota", + "tor-protover", + "tor-units", + "void", ] [[package]] -name = "socket2" -version = "0.5.8" +name = "tor-cert" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +checksum = "162d44b975e445de099b3a495710689652bbd0a9a3a0e4946341acbb3138b22b" dependencies = [ - "libc", - "windows-sys 0.52.0", + "caret", + "derive_builder_fork_arti", + "derive_more", + "digest 0.10.7", + "saturating-time", + "thiserror 2.0.18", + "tor-bytes", + "tor-checkable", + "tor-error", + "tor-llcrypto", + "web-time-compat", ] [[package]] -name = "spin" -version = "0.5.2" +name = "tor-chanmgr" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +checksum = "bf15c4cbafa5745def4fced9e8e59148533a2bfc8d938e9fad7a0d6d16757712" +dependencies = [ + "async-trait", + "base64ct", + "caret", + "cfg-if 1.0.4", + "derive-deftly", + "derive_more", + "educe", + "futures 0.3.32", + "httparse", + "oneshot-fused-workaround", + "percent-encoding", + "postage", + "rand 0.10.2", + "safelog", + "serde", + "serde_with 3.21.0", + "thiserror 2.0.18", + "tor-async-utils", + "tor-basic-utils", + "tor-cell", + "tor-config", + "tor-error", + "tor-keymgr", + "tor-linkspec", + "tor-llcrypto", + "tor-memquota", + "tor-netdir", + "tor-proto", + "tor-rtcompat", + "tor-socksproto", + "tor-units", + "tracing", + "url", + "void", + "web-time-compat", +] [[package]] -name = "stable_deref_trait" -version = "1.2.1" +name = "tor-checkable" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +checksum = "ef81835cb0b2db460c694678eae8c4cbad5f31847e473aac42605f9b945fb31a" +dependencies = [ + "humantime 2.4.0", + "signature", + "thiserror 2.0.18", + "tor-llcrypto", + "web-time-compat", +] [[package]] -name = "strsim" -version = "0.8.0" +name = "tor-circmgr" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" +checksum = "2b63cca1f18282f82b905b6a8c20c6f247787bc010d1b317d9eb98503e4352bf" +dependencies = [ + "amplify", + "async-trait", + "cfg-if 1.0.4", + "derive-deftly", + "derive_builder_fork_arti", + "derive_more", + "downcast-rs", + "dyn-clone", + "educe", + "futures 0.3.32", + "humantime-serde", + "itertools 0.14.0", + "once_cell", + "oneshot-fused-workaround", + "pin-project", + "rand 0.10.2", + "retry-error", + "safelog", + "serde", + "thiserror 2.0.18", + "tor-async-utils", + "tor-basic-utils", + "tor-cell", + "tor-chanmgr", + "tor-config", + "tor-dircommon", + "tor-error", + "tor-guardmgr", + "tor-linkspec", + "tor-memquota", + "tor-netdir", + "tor-netdoc", + "tor-persist", + "tor-proto", + "tor-protover", + "tor-relay-selection", + "tor-rtcompat", + "tor-units", + "tracing", + "void", + "weak-table", + "web-time-compat", +] [[package]] -name = "strsim" -version = "0.10.0" +name = "tor-config" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +checksum = "a699adfb8bdb34699afcca7cbbd9d26329246940bc9dbc32f380c87578017350" +dependencies = [ + "amplify", + "cfg-if 1.0.4", + "derive-deftly", + "derive_builder_fork_arti", + "educe", + "either", + "figment", + "fs-mistrust", + "futures 0.3.32", + "humantime-serde", + "itertools 0.14.0", + "notify", + "paste", + "postage", + "regex", + "serde", + "serde-value", + "serde_ignored", + "strum 0.28.0", + "thiserror 2.0.18", + "toml 1.1.2+spec-1.1.0", + "tor-basic-utils", + "tor-error", + "tor-rtcompat", + "tracing", + "void", +] [[package]] -name = "strum" -version = "0.18.0" +name = "tor-config-path" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57bd81eb48f4c437cadc685403cad539345bf703d78e63707418431cecd4522b" +checksum = "1d38001695121fc778e67ac7a1c733d3158ee8942a55cfa343981b50d90f5daa" +dependencies = [ + "directories", + "serde", + "shellexpand", + "thiserror 2.0.18", + "tor-error", + "tor-general-addr", +] [[package]] -name = "strum_macros" -version = "0.18.0" +name = "tor-consdiff" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87c85aa3f8ea653bfd3ddf25f7ee357ee4d204731f6aa9ad04002306f6e2774c" +checksum = "80e9e3eb11e91ea4c1affe5e1f73628726c283942715739850546acb94bbe028" dependencies = [ - "heck", - "proc-macro2 1.0.89", - "quote 1.0.37", - "syn 1.0.109", + "derive_more", + "digest 0.10.7", + "hex", + "imara-diff", + "static_assertions", + "thiserror 2.0.18", + "tor-error", + "tor-llcrypto", + "tor-netdoc", ] [[package]] -name = "subtle" -version = "2.4.1" +name = "tor-dirclient" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" +checksum = "0027cd2a58abab9ebaf61c3c1da9b0fa9e6cbc914b88fedb7b3102bd4f4dd656" +dependencies = [ + "async-compression", + "base64ct", + "derive_more", + "futures 0.3.32", + "hex", + "http", + "httparse", + "httpdate", + "itertools 0.14.0", + "memchr", + "thiserror 2.0.18", + "tor-circmgr", + "tor-error", + "tor-hscrypto", + "tor-linkspec", + "tor-llcrypto", + "tor-netdoc", + "tor-proto", + "tor-rtcompat", + "tracing", + "web-time-compat", +] [[package]] -name = "supercow" -version = "0.1.0" +name = "tor-dircommon" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "171758edb47aa306a78dfa4ab9aeb5167405bd4e3dc2b64e88f6a84bbe98bd63" +checksum = "7d2047b2f1ad3453c1060e5e39203bd87ead266e6873707bd7333b1d6bdac768" +dependencies = [ + "base64ct", + "derive-deftly", + "getset", + "humantime 2.4.0", + "humantime-serde", + "serde", + "tor-basic-utils", + "tor-checkable", + "tor-config", + "tor-linkspec", + "tor-llcrypto", + "tor-netdoc", + "tracing", +] [[package]] -name = "syn" -version = "0.15.44" +name = "tor-dirmgr" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5" +checksum = "57ebc162ccd628ef8e96912a9e42e848cd7305eb2a5a455a6002f54ad6826422" dependencies = [ - "proc-macro2 0.4.30", - "quote 0.6.13", - "unicode-xid", + "async-trait", + "base64ct", + "derive_builder_fork_arti", + "derive_more", + "digest 0.10.7", + "educe", + "event-listener", + "fs-mistrust", + "fslock-guard", + "futures 0.3.32", + "hex", + "humantime 2.4.0", + "humantime-serde", + "itertools 0.14.0", + "memmap2", + "oneshot-fused-workaround", + "paste", + "postage", + "rand 0.10.2", + "rusqlite", + "safelog", + "scopeguard", + "serde", + "serde_json", + "signature", + "static_assertions", + "strum 0.28.0", + "thiserror 2.0.18", + "time", + "tor-async-utils", + "tor-basic-utils", + "tor-checkable", + "tor-circmgr", + "tor-config", + "tor-consdiff", + "tor-dirclient", + "tor-dircommon", + "tor-error", + "tor-guardmgr", + "tor-llcrypto", + "tor-netdir", + "tor-netdoc", + "tor-persist", + "tor-proto", + "tor-protover", + "tor-rtcompat", + "tracing", + "void", + "web-time-compat", ] [[package]] -name = "syn" -version = "1.0.109" +name = "tor-error" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +checksum = "c6a7ed4aee3e0613a75d420032116dac82d7702594e28566befdfa6707a8fd0e" dependencies = [ - "proc-macro2 1.0.89", - "quote 1.0.37", - "unicode-ident", + "derive_more", + "futures 0.3.32", + "paste", + "retry-error", + "static_assertions", + "strum 0.28.0", + "thiserror 2.0.18", + "tracing", + "void", + "web-time-compat", ] [[package]] -name = "syn" -version = "2.0.86" +name = "tor-general-addr" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89275301d38033efb81a6e60e3497e734dfcc62571f2854bf4b16690398824c" +checksum = "cfee544a33dd3e67e0311cc46ef8f667cbf9c02c7c00ef4f25098a25b2b6e1cf" dependencies = [ - "proc-macro2 1.0.89", - "quote 1.0.37", - "unicode-ident", + "derive_more", + "thiserror 2.0.18", + "void", ] [[package]] -name = "synstructure" -version = "0.13.2" +name = "tor-guardmgr" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +checksum = "f1e8f52e4b1b08ba58970a9b9879b0b72c35dcba48bad389935f2778ed432d05" +dependencies = [ + "amplify", + "base64ct", + "derive-deftly", + "derive_builder_fork_arti", + "derive_more", + "dyn-clone", + "educe", + "futures 0.3.32", + "humantime 2.4.0", + "humantime-serde", + "itertools 0.14.0", + "num_enum", + "oneshot-fused-workaround", + "pin-project", + "postage", + "rand 0.10.2", + "safelog", + "serde", + "strum 0.28.0", + "thiserror 2.0.18", + "tor-async-utils", + "tor-basic-utils", + "tor-config", + "tor-dircommon", + "tor-error", + "tor-linkspec", + "tor-llcrypto", + "tor-netdir", + "tor-netdoc", + "tor-persist", + "tor-proto", + "tor-protover", + "tor-relay-selection", + "tor-rtcompat", + "tor-units", + "tracing", + "web-time-compat", +] + +[[package]] +name = "tor-hsclient" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dc813f71e63fd324c4b1b1647ddc2fcc4b6e77c45091e6769ae80e591726ec" dependencies = [ - "proc-macro2 1.0.89", - "quote 1.0.37", - "syn 2.0.86", + "async-trait", + "derive-deftly", + "derive_more", + "educe", + "either", + "futures 0.3.32", + "itertools 0.14.0", + "oneshot-fused-workaround", + "postage", + "rand 0.10.2", + "retry-error", + "safelog", + "slotmap-careful", + "strum 0.28.0", + "thiserror 2.0.18", + "tor-async-utils", + "tor-basic-utils", + "tor-bytes", + "tor-cell", + "tor-checkable", + "tor-circmgr", + "tor-config", + "tor-dirclient", + "tor-error", + "tor-hscrypto", + "tor-keymgr", + "tor-linkspec", + "tor-llcrypto", + "tor-memquota", + "tor-netdir", + "tor-netdoc", + "tor-persist", + "tor-proto", + "tor-protover", + "tor-rtcompat", + "tracing", + "web-time-compat", ] [[package]] -name = "sysinfo" -version = "0.29.11" +name = "tor-hscrypto" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd727fc423c2060f6c92d9534cef765c65a6ed3f428a03d7def74a8c4348e666" +checksum = "f82fd6a5a6ede5ece5e7f52708bdf0a007e8f975f9825689b376f038845f1e88" dependencies = [ - "cfg-if 1.0.0", - "core-foundation-sys", - "libc", - "ntapi", - "once_cell", - "rayon", - "winapi 0.3.9", + "cipher 0.4.4", + "data-encoding", + "derive-deftly", + "derive_more", + "digest 0.10.7", + "hex", + "humantime 2.4.0", + "itertools 0.14.0", + "paste", + "rand 0.10.2", + "safelog", + "serde", + "signature", + "subtle", + "thiserror 2.0.18", + "tor-basic-utils", + "tor-bytes", + "tor-error", + "tor-key-forge", + "tor-llcrypto", + "tor-memquota", + "tor-units", + "void", + "web-time-compat", + "zeroize", ] [[package]] -name = "tempfile" -version = "3.13.0" +name = "tor-hsrproxy" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" +checksum = "9b4231a55048af49ae2a8fcb2d41ea0df986b68c26bf6128f97f7f1324d47498" dependencies = [ - "cfg-if 1.0.0", - "fastrand", - "once_cell", - "rustix", - "windows-sys 0.59.0", + "derive-deftly", + "futures 0.3.32", + "futures-copy", + "itertools 0.14.0", + "oneshot-fused-workaround", + "rangemap", + "safelog", + "serde", + "serde_with 3.21.0", + "strum 0.28.0", + "thiserror 2.0.18", + "tor-async-utils", + "tor-cell", + "tor-config", + "tor-error", + "tor-hsservice", + "tor-log-ratelim", + "tor-proto", + "tor-rtcompat", + "tracing", + "void", ] [[package]] -name = "term" -version = "0.6.1" +name = "tor-hsservice" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0863a3345e70f61d613eab32ee046ccd1bcc5f9105fe402c61fcd0c13eeb8b5" +checksum = "170b66c977d3b16a5ff79ec344c06392076f0326d2d65146a58c450c50395dae" dependencies = [ - "dirs 2.0.2", - "winapi 0.3.9", + "amplify", + "async-trait", + "base64ct", + "cfg-if 1.0.4", + "derive-deftly", + "derive_builder_fork_arti", + "derive_more", + "digest 0.10.7", + "educe", + "fs-mistrust", + "futures 0.3.32", + "growable-bloom-filter", + "hex", + "humantime 2.4.0", + "itertools 0.14.0", + "k12", + "once_cell", + "oneshot-fused-workaround", + "postage", + "rand 0.10.2", + "rand_core 0.10.1", + "retry-error", + "safelog", + "serde", + "serde_with 3.21.0", + "strum 0.28.0", + "thiserror 2.0.18", + "tor-async-utils", + "tor-basic-utils", + "tor-bytes", + "tor-cell", + "tor-circmgr", + "tor-config", + "tor-config-path", + "tor-dirclient", + "tor-error", + "tor-hscrypto", + "tor-keymgr", + "tor-linkspec", + "tor-llcrypto", + "tor-log-ratelim", + "tor-netdir", + "tor-netdoc", + "tor-persist", + "tor-proto", + "tor-protover", + "tor-relay-selection", + "tor-rtcompat", + "tracing", + "void", + "web-time-compat", ] [[package]] -name = "term" -version = "0.7.0" +name = "tor-key-forge" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +checksum = "30f83389917e9908c43457c80d0b982ba6dcd3e6ea36a37b29167b7619d9eaa3" dependencies = [ - "dirs-next 2.0.0", - "rustversion", - "winapi 0.3.9", + "derive-deftly", + "derive_more", + "downcast-rs", + "paste", + "rand 0.10.2", + "rsa", + "signature", + "ssh-key-fork-arti", + "thiserror 2.0.18", + "tor-bytes", + "tor-cert", + "tor-checkable", + "tor-error", + "tor-llcrypto", +] + +[[package]] +name = "tor-keymgr" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7c18fb26d112c6022b7d1de558a4045b574fecfa1c504045145d8e1a9dc01a" +dependencies = [ + "amplify", + "arrayvec 0.7.8", + "cfg-if 1.0.4", + "derive-deftly", + "derive_builder_fork_arti", + "derive_more", + "downcast-rs", + "dyn-clone", + "fs-mistrust", + "glob-match", + "humantime 2.4.0", + "inventory", + "itertools 0.14.0", + "rand 0.10.2", + "safelog", + "serde", + "signature", + "ssh-key-fork-arti", + "thiserror 2.0.18", + "tor-basic-utils", + "tor-bytes", + "tor-config", + "tor-config-path", + "tor-error", + "tor-hscrypto", + "tor-key-forge", + "tor-llcrypto", + "tor-persist", + "tracing", + "visibility", + "walkdir", + "web-time-compat", + "zeroize", ] [[package]] -name = "terminfo" -version = "0.8.0" +name = "tor-linkspec" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "666cd3a6681775d22b200409aad3b089c5b99fb11ecdd8a204d9d62f8148498f" +checksum = "638a849a5d65d4e37adb6a2fa597a5a5c0decb749c17e76d3c44c4aefd696270" dependencies = [ - "dirs 4.0.0", - "fnv", - "nom", - "phf", - "phf_codegen", + "base64ct", + "by_address", + "caret", + "derive-deftly", + "derive_builder_fork_arti", + "derive_more", + "hex", + "itertools 0.14.0", + "safelog", + "serde", + "serde_with 3.21.0", + "strum 0.28.0", + "thiserror 2.0.18", + "tor-basic-utils", + "tor-bytes", + "tor-config", + "tor-llcrypto", + "tor-memquota", + "tor-protover", ] [[package]] -name = "textwrap" -version = "0.11.0" +name = "tor-llcrypto" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +checksum = "4ef0e63e805e9c1da61511a93eca502dc29bcb0b8635e9668ec750b63a1e53a1" dependencies = [ - "unicode-width", + "aes", + "base64ct", + "ctr", + "curve25519-dalek 4.1.3", + "der-parser", + "derive-deftly", + "derive_more", + "digest 0.10.7", + "ed25519-dalek", + "educe", + "getrandom 0.2.17", + "getrandom 0.3.4", + "getrandom 0.4.3", + "hex", + "rand 0.10.2", + "rand_chacha 0.10.0", + "rand_core 0.10.1", + "rand_core 0.6.4", + "rand_jitter 0.6.1", + "rdrand 0.8.3", + "reseeding_rng", + "rsa", + "safelog", + "serde", + "sha1", + "sha2 0.10.9", + "sha3 0.10.9", + "signature", + "subtle", + "thiserror 2.0.18", + "tor-error", + "tor-memquota-cost", + "visibility", + "x25519-dalek 2.0.1", + "zeroize", ] [[package]] -name = "thiserror" -version = "1.0.66" +name = "tor-log-ratelim" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d171f59dbaa811dbbb1aee1e73db92ec2b122911a48e1390dfe327a821ddede" +checksum = "37d0f759a875b6f9b6f3657a2626a70e2474e698a4404501033babe63ad888a6" dependencies = [ - "thiserror-impl", + "futures 0.3.32", + "humantime 2.4.0", + "thiserror 2.0.18", + "tor-error", + "tor-rtcompat", + "tracing", + "weak-table", + "web-time-compat", ] [[package]] -name = "thiserror-impl" -version = "1.0.66" +name = "tor-memquota" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b08be0f17bd307950653ce45db00cd31200d82b624b36e181337d9c7d92765b5" +checksum = "3c06f2e981120a4c1da1e361573cee4af618d05fa3c7ec37c139d766f1b8d47f" dependencies = [ - "proc-macro2 1.0.89", - "quote 1.0.37", - "syn 2.0.86", + "cfg-if 1.0.4", + "derive-deftly", + "derive_more", + "dyn-clone", + "educe", + "futures 0.3.32", + "itertools 0.14.0", + "paste", + "pin-project", + "serde", + "slotmap-careful", + "static_assertions", + "sysinfo 0.38.4", + "thiserror 2.0.18", + "tor-async-utils", + "tor-basic-utils", + "tor-config", + "tor-error", + "tor-log-ratelim", + "tor-memquota-cost", + "tor-rtcompat", + "tracing", + "void", ] [[package]] -name = "thread-id" -version = "4.2.2" +name = "tor-memquota-cost" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe8f25bbdd100db7e1d34acf7fd2dc59c4bf8f7483f505eaa7d4f12f76cc0ea" +checksum = "b86eb903fee638e9211daddaddc146f0c5e4f5605d79983548fce6fec596325c" dependencies = [ - "libc", - "winapi 0.3.9", + "derive-deftly", + "itertools 0.14.0", + "paste", + "void", ] [[package]] -name = "timer" -version = "0.2.0" +name = "tor-netdir" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31d42176308937165701f50638db1c31586f183f1aab416268216577aec7306b" +checksum = "b8160580273eec3576158c3e833ff198870665dacebb373585892dde93cf9803" dependencies = [ - "chrono", + "async-trait", + "bitflags 2.13.0", + "derive_more", + "digest 0.10.7", + "futures 0.3.32", + "hex", + "humantime 2.4.0", + "itertools 0.14.0", + "num_enum", + "rand 0.10.2", + "serde", + "strum 0.28.0", + "thiserror 2.0.18", + "time", + "tor-basic-utils", + "tor-error", + "tor-hscrypto", + "tor-linkspec", + "tor-llcrypto", + "tor-netdoc", + "tor-protover", + "tor-units", + "tracing", + "typed-index-collections", + "web-time-compat", ] [[package]] -name = "tinystr" -version = "0.7.6" +name = "tor-netdoc" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +checksum = "51598bfd24866de2c51e0de7a6e2418f86c6278cfeba2f85295ce45367fd8e06" dependencies = [ - "displaydoc", + "amplify", + "base64ct", + "cipher 0.4.4", + "derive-deftly", + "derive_builder_fork_arti", + "derive_more", + "digest 0.10.7", + "educe", + "enumset", + "hex", + "hostname-validator", + "humantime 2.4.0", + "ipnet", + "itertools 0.14.0", + "memchr", + "paste", + "phf 0.13.1", + "rand 0.10.2", + "saturating-time", + "serde", + "serde_with 3.21.0", + "signature", + "smallvec", + "strum 0.28.0", + "subtle", + "thiserror 2.0.18", + "time", + "tinystr", + "tor-basic-utils", + "tor-bytes", + "tor-cell", + "tor-cert", + "tor-checkable", + "tor-error", + "tor-hscrypto", + "tor-linkspec", + "tor-llcrypto", + "tor-protover", + "tor-units", + "void", + "web-time-compat", + "zeroize", ] [[package]] -name = "tinystr" -version = "0.8.2" +name = "tor-persist" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "c2ff75e20adb6a1a7ad07e16b55e56d5376706cc0090ab8831f008f1a89243f2" dependencies = [ - "displaydoc", - "zerovec", + "amplify", + "derive-deftly", + "derive_more", + "filetime", + "fs-mistrust", + "fslock-guard", + "futures 0.3.32", + "itertools 0.14.0", + "oneshot-fused-workaround", + "paste", + "sanitize-filename", + "serde", + "serde_json", + "thiserror 2.0.18", + "time", + "tor-async-utils", + "tor-basic-utils", + "tor-error", + "tracing", + "void", + "web-time-compat", ] [[package]] -name = "tinyvec" -version = "1.8.0" +name = "tor-proto" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +checksum = "666b602da8ba5a4189d6ff551f5dd9db949b503780140d175435658970a1ea54" dependencies = [ - "tinyvec_macros", + "amplify", + "async-trait", + "asynchronous-codec", + "bitvec", + "bytes 1.12.0", + "caret", + "cfg-if 1.0.4", + "cipher 0.4.4", + "coarsetime", + "derive-deftly", + "derive_builder_fork_arti", + "derive_more", + "digest 0.10.7", + "educe", + "enum_dispatch", + "futures 0.3.32", + "futures-util", + "hkdf", + "hmac 0.12.1", + "itertools 0.14.0", + "nonany", + "oneshot-fused-workaround", + "pin-project", + "postage", + "rand 0.10.2", + "rand_core 0.10.1", + "safelog", + "slotmap-careful", + "smallvec", + "static_assertions", + "strum 0.28.0", + "subtle", + "thiserror 2.0.18", + "tokio", + "tokio-util", + "tor-async-utils", + "tor-basic-utils", + "tor-bytes", + "tor-cell", + "tor-cert", + "tor-checkable", + "tor-config", + "tor-error", + "tor-hscrypto", + "tor-linkspec", + "tor-llcrypto", + "tor-log-ratelim", + "tor-memquota", + "tor-protover", + "tor-rtcompat", + "tor-rtmock", + "tor-units", + "tracing", + "typenum", + "visibility", + "void", + "web-time-compat", + "zeroize", ] [[package]] -name = "tinyvec_macros" -version = "0.1.1" +name = "tor-protover" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +checksum = "6ef84569d020c36eb322976f9a5dec1d3fa992db9739237ad1ae923b0ddeb298" +dependencies = [ + "caret", + "paste", + "serde_with 3.21.0", + "thiserror 2.0.18", + "tor-basic-utils", + "tor-bytes", +] [[package]] -name = "tokio" -version = "0.2.25" +name = "tor-ptmgr" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6703a273949a90131b290be1fe7b039d0fc884aa1935860dfcbe056f28cd8092" +checksum = "66db88ee6c5ba60a077b592b7e566d465220de61ccfd473fba6567c577cad475" dependencies = [ - "bytes 0.5.6", - "fnv", - "futures-core", - "iovec", - "lazy_static", - "libc", - "memchr", - "mio 0.6.23", - "mio-named-pipes", - "mio-uds", - "num_cpus", - "pin-project-lite 0.1.12", - "signal-hook-registry", - "slab", - "tokio-macros 0.2.6", - "winapi 0.3.9", + "async-trait", + "cfg-if 1.0.4", + "derive-deftly", + "derive_builder_fork_arti", + "fs-mistrust", + "futures 0.3.32", + "itertools 0.14.0", + "oneshot-fused-workaround", + "serde", + "thiserror 2.0.18", + "tor-async-utils", + "tor-basic-utils", + "tor-chanmgr", + "tor-config", + "tor-config-path", + "tor-error", + "tor-linkspec", + "tor-proto", + "tor-rtcompat", + "tor-socksproto", + "tracing", + "web-time-compat", ] [[package]] -name = "tokio" -version = "1.42.1" +name = "tor-relay-selection" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2209a14885b74764cce87ffa777ffa1b8ce81a3f3166c6f886b83337fe7e077f" +checksum = "09778e3a45fdf49ca335fa16a0032abc5d4e1cacff9f46c3ed3e2a416d088aa6" dependencies = [ - "backtrace", - "bytes 1.8.0", + "rand 0.10.2", + "serde", + "tor-basic-utils", + "tor-linkspec", + "tor-netdir", + "tor-netdoc", +] + +[[package]] +name = "tor-rtcompat" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "981279291476ad7ec309c874fa99b8d31250432a48ff6eea3f7405edbbd254e1" +dependencies = [ + "amplify", + "async-native-tls", + "async-trait", + "async_executors", + "asynchronous-codec", + "cfg-if 1.0.4", + "coarsetime", + "derive_builder_fork_arti", + "derive_more", + "dyn-clone", + "educe", + "futures 0.3.32", + "hex", "libc", - "mio 1.1.0", - "parking_lot 0.12.3", - "pin-project-lite 0.2.15", - "signal-hook-registry", - "socket2 0.5.8", - "tokio-macros 2.4.0", - "windows-sys 0.52.0", + "native-tls", + "paste", + "pin-project", + "socket2", + "thiserror 2.0.18", + "tokio", + "tokio-util", + "tor-error", + "tor-general-addr", + "tracing", + "void", + "web-time-compat", + "zeroize", ] [[package]] -name = "tokio-io-timeout" -version = "1.2.1" +name = "tor-rtmock" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bd86198d9ee903fedd2f9a2e72014287c0d9167e4ae43b5853007205dda1b76" +checksum = "6a3ee9695304cc44cc0de1dc673c9a71419318e214dc04ed7883a058d050654d" dependencies = [ - "pin-project-lite 0.2.15", - "tokio 1.42.1", + "amplify", + "assert_matches", + "async-trait", + "derive-deftly", + "derive_more", + "educe", + "futures 0.3.32", + "humantime 2.4.0", + "itertools 0.14.0", + "oneshot-fused-workaround", + "pin-project", + "priority-queue", + "slotmap-careful", + "strum 0.28.0", + "thiserror 2.0.18", + "tor-error", + "tor-general-addr", + "tor-rtcompat", + "tracing", + "tracing-test", + "void", + "web-time-compat", ] [[package]] -name = "tokio-macros" -version = "0.2.6" +name = "tor-socksproto" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e44da00bfc73a25f814cd8d7e57a68a5c31b74b3152a0a1d1f590c97ed06265a" +checksum = "bbf78ef55dbfccb5a95cd75eb433621947923653cb91c9941d5a0a5116e510ee" dependencies = [ - "proc-macro2 1.0.89", - "quote 1.0.37", - "syn 1.0.109", + "amplify", + "caret", + "derive-deftly", + "educe", + "safelog", + "subtle", + "thiserror 2.0.18", + "tor-bytes", + "tor-error", ] [[package]] -name = "tokio-macros" -version = "2.4.0" +name = "tor-units" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +checksum = "d0325a93f3b35c5a9bda74705ce89b753e1c8e183af6945826188cef16687d80" dependencies = [ - "proc-macro2 1.0.89", - "quote 1.0.37", - "syn 2.0.86", + "derive-deftly", + "derive_more", + "serde", + "thiserror 2.0.18", + "tor-memquota", ] [[package]] -name = "tokio-rustls" -version = "0.14.1" +name = "tower" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e12831b255bcfa39dc0436b01e19fea231a37db570686c06ee72c423479f889a" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", - "rustls 0.18.1", - "tokio 0.2.25", - "webpki 0.21.4", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", ] [[package]] -name = "tokio-rustls" -version = "0.23.4" +name = "tower-http" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "rustls 0.20.9", - "tokio 1.42.1", - "webpki 0.22.4", + "bitflags 2.13.0", + "bytes 1.12.0", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", ] [[package]] -name = "tokio-socks" -version = "0.3.0" +name = "tower-layer" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d611fd5d241872372d52a0a3d309c52d0b95a6a67671a6c8f7ab2c4a37fb2539" -dependencies = [ - "bytes 0.4.12", - "either", - "futures 0.3.31", - "thiserror", - "tokio 0.2.25", -] +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] -name = "tokio-tls" -version = "0.3.1" +name = "tower-service" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a70f4fcd7b3b24fb194f837560168208f669ca8cb70d0c4b862944452396343" -dependencies = [ - "native-tls", - "tokio 0.2.25", -] +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] -name = "tokio-util" -version = "0.3.1" +name = "tracing" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be8242891f2b6cbef26a2d7e8605133c2c554cd35b3e4948ea892d6d68436499" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ - "bytes 0.5.6", - "futures-core", - "futures-sink", - "log", - "pin-project-lite 0.1.12", - "tokio 0.2.25", + "pin-project-lite", + "tracing-attributes", + "tracing-core", ] [[package]] -name = "tokio-util" -version = "0.7.17" +name = "tracing-attributes" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ - "bytes 1.8.0", - "futures-core", - "futures-sink", - "pin-project-lite 0.2.15", - "tokio 1.42.1", + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", ] [[package]] -name = "toml" -version = "0.5.11" +name = "tracing-core" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ - "serde", + "once_cell", + "valuable", ] [[package]] -name = "tower-service" -version = "0.3.3" +name = "tracing-log" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] [[package]] -name = "tracing" -version = "0.1.40" +name = "tracing-subscriber" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ - "log", - "pin-project-lite 0.2.15", + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", "tracing-core", + "tracing-log", ] [[package]] -name = "tracing-core" -version = "0.1.32" +name = "tracing-test" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +checksum = "19a4c448db514d4f24c5ddb9f73f2ee71bfb24c526cf0c570ba142d1119e0051" dependencies = [ - "once_cell", + "tracing-core", + "tracing-subscriber", + "tracing-test-macro", ] [[package]] -name = "tracing-futures" -version = "0.2.5" +name = "tracing-test-macro" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" +checksum = "ad06847b7afb65c7866a36664b75c40b895e318cea4f71299f013fb22965329d" dependencies = [ - "pin-project", - "tracing", + "quote 1.0.46", + "syn 2.0.118", ] [[package]] @@ -4367,11 +7812,21 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "type-map" -version = "0.5.0" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" +dependencies = [ + "rustc-hash 2.1.3", +] + +[[package]] +name = "typed-index-collections" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deb68604048ff8fa93347f02441e4487594adc20bb8a084f9e564d2b827a0a9f" +checksum = "898160f1dfd383b4e92e17f0512a7d62f3c51c44937b23b6ffc3a1614a8eaccd" dependencies = [ - "rustc-hash", + "bincode 2.0.1", + "serde", ] [[package]] @@ -4385,55 +7840,58 @@ dependencies = [ [[package]] name = "typenum" -version = "1.17.0" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "uncased" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697" +dependencies = [ + "version_check", +] [[package]] name = "unic-langid" -version = "0.9.5" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23dd9d1e72a73b25e07123a80776aae3e7b0ec461ef94f9151eed6ec88005a44" +checksum = "a28ba52c9b05311f4f6e62d5d9d46f094bd6e84cb8df7b3ef952748d752a7d05" dependencies = [ "unic-langid-impl", ] [[package]] name = "unic-langid-impl" -version = "0.9.5" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a5422c1f65949306c99240b81de9f3f15929f5a8bfe05bb44b034cc8bf593e5" +checksum = "dce1bf08044d4b7a94028c93786f8566047edc11110595914de93362559bc658" dependencies = [ "serde", - "tinystr 0.7.6", + "tinystr", ] -[[package]] -name = "unicase" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" - [[package]] name = "unicode-ident" -version = "1.0.13" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-normalization" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" dependencies = [ "tinyvec", ] [[package]] name = "unicode-segmentation" -version = "1.12.0" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" [[package]] name = "unicode-width" @@ -4447,11 +7905,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "universal-hash" -version = "0.4.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05" +checksum = "8326b2c654932e3e4f9196e69d08fdf7cfd718e1dc6f66b347e6024a0c961402" dependencies = [ "generic-array 0.14.7", "subtle", @@ -4474,15 +7938,15 @@ checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" [[package]] name = "untrusted" -version = "0.7.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] -name = "untrusted" -version = "0.9.0" +name = "unty" +version = "0.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" [[package]] name = "url" @@ -4510,14 +7974,22 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "0.8.2" +version = "1.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" dependencies = [ - "getrandom 0.2.15", - "serde", + "getrandom 0.4.3", + "js-sys", + "serde_core", + "wasm-bindgen", ] +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "vcpkg" version = "0.2.15" @@ -4536,6 +8008,23 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "visibility" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d674d135b4a8c1d7e813e2f8d1c9a58308aee4a680323066025e53132218bd91" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" + [[package]] name = "walkdir" version = "2.5.0" @@ -4563,123 +8052,117 @@ checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasm-bindgen" -version = "0.2.95" +name = "wasip2" +version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ - "cfg-if 1.0.0", - "once_cell", - "serde", - "serde_json", - "wasm-bindgen-macro", + "wit-bindgen", +] + +[[package]] +name = "wasix" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae86f02046da16a333a9129d31451423e1657737ecdafed4193838a5f54c5cfe" +dependencies = [ + "wasi 0.11.1+wasi-snapshot-preview1", ] [[package]] -name = "wasm-bindgen-backend" -version = "0.2.95" +name = "wasm-bindgen" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ - "bumpalo", - "log", + "cfg-if 1.0.4", "once_cell", - "proc-macro2 1.0.89", - "quote 1.0.37", - "syn 2.0.86", + "rustversion", + "wasm-bindgen-macro", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.45" +version = "0.4.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" dependencies = [ - "cfg-if 1.0.0", "js-sys", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.95" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" dependencies = [ - "quote 1.0.37", + "quote 1.0.46", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.95" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" dependencies = [ - "proc-macro2 1.0.89", - "quote 1.0.37", - "syn 2.0.86", - "wasm-bindgen-backend", + "bumpalo", + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" - -[[package]] -name = "web-sys" -version = "0.3.72" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ - "js-sys", - "wasm-bindgen", + "unicode-ident", ] [[package]] -name = "webpki" -version = "0.21.4" +name = "weak-table" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" -dependencies = [ - "ring 0.16.20", - "untrusted 0.7.1", -] +checksum = "323f4da9523e9a669e1eaf9c6e763892769b1d38c623913647bfdc1532fe4549" [[package]] -name = "webpki" -version = "0.22.4" +name = "web-sys" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" dependencies = [ - "ring 0.17.14", - "untrusted 0.9.0", + "js-sys", + "wasm-bindgen", ] [[package]] -name = "webpki-roots" -version = "0.20.0" +name = "web-time" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f20dea7535251981a9670857150d571846545088359b28e4951d350bdaf179f" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" dependencies = [ - "webpki 0.21.4", + "js-sys", + "wasm-bindgen", ] [[package]] -name = "winapi" -version = "0.2.8" +name = "web-time-compat" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" +checksum = "af46dad42aa37be3b4a116fe613365f991b8bdae6cd6ce82d84fa9f9e8f70ae4" +dependencies = [ + "web-time", +] [[package]] name = "winapi" @@ -4691,12 +8174,6 @@ dependencies = [ "winapi-x86_64-pc-windows-gnu", ] -[[package]] -name = "winapi-build" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" - [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" @@ -4705,11 +8182,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4718,13 +8195,71 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core", +] + [[package]] name = "windows-core" -version = "0.52.0" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ - "windows-targets", + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", ] [[package]] @@ -4733,13 +8268,61 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core", + "windows-link", +] + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -4748,7 +8331,16 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", ] [[package]] @@ -4760,64 +8352,189 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -4825,52 +8542,79 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "winreg" -version = "0.7.0" +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" dependencies = [ - "winapi 0.3.9", + "memchr", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "writeable" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] -name = "ws2_32-sys" -version = "0.2.1" +name = "wyz" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" dependencies = [ - "winapi 0.2.8", - "winapi-build", + "tap", ] [[package]] name = "x25519-dalek" -version = "0.6.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "637ff90c9540fa3073bb577e65033069e4bae7c79d49d74aa3ffdf5342a53217" +checksum = "5a0c105152107e3b96f6a00a65e86ce82d9b125230e1c4302940eca58ff71f4f" dependencies = [ - "curve25519-dalek 2.1.3", + "curve25519-dalek 3.2.0", "rand_core 0.5.1", "zeroize", ] [[package]] name = "x25519-dalek" -version = "1.1.1" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a0c105152107e3b96f6a00a65e86ce82d9b125230e1c4302940eca58ff71f4f" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" dependencies = [ - "curve25519-dalek 3.2.0", - "rand_core 0.5.1", + "curve25519-dalek 4.1.3", + "rand_core 0.6.4", + "serde", "zeroize", ] +[[package]] +name = "xxhash-rust" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d93c89cdc2d3a63c3ec48ffe926931bdc069eafa8e4402fe6d8f790c9d1e576" + [[package]] name = "yaml-rust" version = "0.3.5" @@ -4879,9 +8623,9 @@ checksum = "e66366e18dc58b46801afbf2ca7661a9f59cc8c5962c29892b6039b4f86fa992" [[package]] name = "yoke" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -4890,83 +8634,82 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ - "proc-macro2 1.0.89", - "quote 1.0.37", - "syn 2.0.86", + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", "synstructure", ] [[package]] name = "zerocopy" -version = "0.7.35" +version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" dependencies = [ - "byteorder", "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.35" +version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" dependencies = [ - "proc-macro2 1.0.89", - "quote 1.0.37", - "syn 2.0.86", + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", ] [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ - "proc-macro2 1.0.89", - "quote 1.0.37", - "syn 2.0.86", + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", "synstructure", ] [[package]] name = "zeroize" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" dependencies = [ "zeroize_derive", ] [[package]] name = "zeroize_derive" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" dependencies = [ - "proc-macro2 1.0.89", - "quote 1.0.37", - "syn 2.0.86", + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", ] [[package]] name = "zerotrie" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", @@ -4975,10 +8718,11 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ + "serde", "yoke", "zerofrom", "zerovec-derive", @@ -4986,13 +8730,13 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ - "proc-macro2 1.0.89", - "quote 1.0.37", - "syn 2.0.86", + "proc-macro2 1.0.106", + "quote 1.0.46", + "syn 2.0.118", ] [[package]] @@ -5003,5 +8747,39 @@ checksum = "93ab48844d61251bb3835145c521d88aa4031d7139e8485990f60ca911fa0815" dependencies = [ "byteorder", "crc32fast", - "thiserror", + "thiserror 1.0.69", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", ] diff --git a/Cargo.toml b/Cargo.toml index d47f3bc53..18dfbf045 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,13 +21,12 @@ exclude = ["integration"] [dependencies] clap = { version = "2.33", features = ["yaml"] } -rpassword = "4.0" +rpassword = "7.5.4" thiserror = "1" -prettytable-rs = "0.10" log = "0.4" linefeed = "0.6" -semver = "0.10" -rustyline = "6" +semver = "1.0.28" +rustyline = "9" lazy_static = "1" grin_wallet_api = { path = "./api", version = "5.4.1" } @@ -35,35 +34,13 @@ grin_wallet_impls = { path = "./impls", version = "5.4.1" } grin_wallet_libwallet = { path = "./libwallet", version = "5.4.1" } grin_wallet_controller = { path = "./controller", version = "5.4.1" } grin_wallet_config = { path = "./config", version = "5.4.1" } -grin_wallet_util = { path = "./util", version = "5.4.1" } - ##### Grin Imports -# For Release -grin_core = "5.4.1" -grin_keychain = "5.4.1" -grin_util = "5.4.1" -grin_api = "5.4.1" - -# 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"} +grin_core = { path = "grin/core"} +grin_keychain = { path = "grin/keychain"} +grin_util = { path = "grin/util"} +grin_api = { path = "grin/api"} ###### @@ -73,7 +50,5 @@ built = { version = "0.8.0", features = ["git2"]} [dev-dependencies] url = "2.1" serde = "1" -serde_derive = "1" serde_json = "1" -remove_dir_all = "0.7" easy-jsonrpc-mw = "0.5.4" \ No newline at end of file diff --git a/README.md b/README.md index 0554c4a91..d8c584137 100644 --- a/README.md +++ b/README.md @@ -14,12 +14,28 @@ This is the reference implementation of [Grin's](https://github.com/mimblewimble * A reference command-line wallet, which provides a baseline wallet for Grin and demonstrates how the wallet APIs should be called. -# Usage +## Contributing + +To get involved, read our [contributing docs](https://github.com/mimblewimble/grin/blob/master/CONTRIBUTING.md). + +Find us: + +* Telegram: [Grin Development](https://t.me/grindevelopment) +* Chat: [Keybase](https://keybase.io/team/grincoin), more instructions on how to join [here](https://grin.mw/community). + +## 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 +To build locally install Rust and execute in the project directory: +``` +git submodule update --init --recursive +cargo build --release +./target/release/grin-wallet +``` + +## License Apache License v2.0 diff --git a/api/Cargo.toml b/api/Cargo.toml index c52a3810a..ee4a43c5f 100644 --- a/api/Cargo.toml +++ b/api/Cargo.toml @@ -11,16 +11,16 @@ edition = "2021" [dependencies] log = "0.4" -uuid = { version = "0.8", features = ["serde", "v4"] } +uuid = { version = "1.23.4", 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" +ring = "0.17.14" base64 = "0.12" -ed25519-dalek = "1.0.0-pre.4" +ed25519-dalek = "2" grin_wallet_libwallet = { path = "../libwallet", version = "5.4.1" } grin_wallet_config = { path = "../config", version = "5.4.1" } @@ -29,26 +29,9 @@ grin_wallet_util = { path = "../util", version = "5.4.1" } ##### Grin Imports -# For Release -grin_core = "5.4.1" -grin_keychain = "5.4.1" -grin_util = "5.4.1" - -# 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"} +grin_core = { path = "../grin/core"} +grin_keychain = { path = "../grin/keychain"} +grin_util = { path = "../grin/util"} ##### diff --git a/api/src/foreign.rs b/api/src/foreign.rs index 1d96256ba..32a0e8f6c 100644 --- a/api/src/foreign.rs +++ b/api/src/foreign.rs @@ -14,9 +14,11 @@ //! Foreign API External Definition -use crate::config::TorConfig; use crate::keychain::Keychain; use crate::libwallet::api_impl::foreign; +use crate::libwallet::api_impl::types::update_tx_slate_state; +use crate::libwallet::contract::proofs::InvoiceProof; +use crate::libwallet::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI}; use crate::libwallet::{ BlockFees, CbData, Error, NodeClient, NodeVersionInfo, Slate, VersionInfo, WalletInst, WalletLCProvider, @@ -24,6 +26,8 @@ use crate::libwallet::{ use crate::try_slatepack_sync_workflow; use crate::util::secp::key::SecretKey; use crate::util::Mutex; +use ed25519_dalek::VerifyingKey as DalekPublicKey; +use libwallet::SlatepackAddress; use std::sync::Arc; /// ForeignAPI Middleware Check callback @@ -66,15 +70,14 @@ where { /// Wallet instance pub wallet_inst: Arc>>>, + /// Wallet configuration path + config_path: crate::ConfigPath, /// Flag to normalize some output during testing. Can mostly be ignored. pub doctest_mode: bool, /// foreign check middleware middleware: Option, /// Stored keychain mask (in case the stored wallet seed is tokenized) keychain_mask: Option, - /// Optional TOR configuration, holding address of sender and - /// data directory - tor_config: Mutex>, } impl<'a, L, C, K> Foreign<'a, L, C, K> @@ -94,6 +97,7 @@ where /// # Arguments /// * `wallet_in` - A reference-counted mutex containing an implementation of the /// [`WalletBackend`](../grin_wallet_libwallet/types/trait.WalletBackend.html) trait. + /// * `config_path` - Path to the wallet configuration file /// * `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) @@ -105,6 +109,7 @@ where /// /// # Example /// ``` + /// use std::path::PathBuf; /// use grin_keychain as keychain; /// use grin_util as util; /// use grin_core; @@ -139,14 +144,14 @@ where /// /// // 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(); + /// let node_client = HTTPNodeClient::new(&wallet_config.check_node_api_http_addr, None, wallet_config.api_request_timeout()).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()) + /// let mut wallet = Box::new(DefaultWalletImpl::::new(node_client.clone()).unwrap()) /// as Box, HTTPNodeClient, ExtKeychain>>; /// /// // Wallet LifeCycle Provider provides all functions init wallet and work with seeds, etc... @@ -163,39 +168,30 @@ where /// // 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); + /// let api_foreign = Foreign::new(wallet.clone(), PathBuf::from(dir).join("grin-wallet.toml"), None, None, false); /// // .. perform wallet operations /// /// ``` - pub fn new( + pub fn new

( wallet_inst: Arc>>>, + config_path: P, keychain_mask: Option, middleware: Option, doctest_mode: bool, - ) -> Self { + ) -> Self + where + P: Into, + { Foreign { wallet_inst, + config_path: config_path.into(), doctest_mode, middleware, keychain_mask, - tor_config: Mutex::new(None), } } - /// Set the TOR configuration for this instance of the ForeignAPI, used during - /// `recieve_tx` when a return address is specified - /// - /// # Arguments - /// * `tor_config` - The optional [TorConfig](#) to use - /// # Returns - /// * Nothing - - pub fn set_tor_config(&self, tor_config: Option) { - let mut lock = self.tor_config.lock(); - *lock = tor_config; - } - /// Return the version capabilities of the running ForeignApi Node /// # Arguments /// None @@ -206,7 +202,7 @@ where /// ``` /// # grin_wallet_api::doctest_helper_setup_doc_env_foreign!(wallet, wallet_config); /// - /// let mut api_foreign = Foreign::new(wallet.clone(), None, None, false); + /// let mut api_foreign = Foreign::new(wallet.clone(), std::path::PathBuf::from("grin-wallet.toml"), None, None, false); /// /// let version_info = api_foreign.check_version(); /// // check and proceed accordingly @@ -258,7 +254,7 @@ where /// ``` /// # grin_wallet_api::doctest_helper_setup_doc_env_foreign!(wallet, wallet_config); /// - /// let mut api_foreign = Foreign::new(wallet.clone(), None, None, false); + /// let mut api_foreign = Foreign::new(wallet.clone(), std::path::PathBuf::from("grin-wallet.toml"), None, None, false); /// /// let block_fees = BlockFees { /// fees: 800000, @@ -286,7 +282,7 @@ where )?; } foreign::build_coinbase( - &mut **w, + w, (&self.keychain_mask).as_ref(), block_fees, self.doctest_mode, @@ -333,7 +329,7 @@ where /// ``` /// # grin_wallet_api::doctest_helper_setup_doc_env_foreign!(wallet, wallet_config); /// - /// let mut api_foreign = Foreign::new(wallet.clone(), None, None, false); + /// let mut api_foreign = Foreign::new(wallet.clone(), std::path::PathBuf::from("grin-wallet.toml"), None, None, false); /// # let slate = Slate::blank(2, false); /// /// // . . . @@ -350,9 +346,13 @@ where &self, slate: &Slate, dest_acct_name: Option<&str>, - r_addr: Option, + r_addr: Option, ) -> Result { let mut w_lock = self.wallet_inst.lock(); + let tor_config = r_addr + .as_ref() + .map(|_| crate::tor_config::load(&self.config_path.get())) + .transpose()?; let w = w_lock.lc_provider()?.wallet_inst()?; if let Some(m) = self.middleware.as_ref() { m( @@ -362,7 +362,7 @@ where )?; } let ret_slate = foreign::receive_tx( - &mut **w, + w, (&self.keychain_mask).as_ref(), slate, dest_acct_name, @@ -370,18 +370,32 @@ where )?; 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, - ); + let tc = tor_config.ok_or_else(|| { + Error::TorConfig("Tor config was not loaded with a return address".into()) + })?; + let can_send = tc.send_tor(None); + if self.doctest_mode || !can_send { + return Ok(ret_slate); + } + let res = try_slatepack_sync_workflow(&ret_slate, &a, Some(tc), None, true); match res { - Ok(s) => return Ok(s.unwrap()), - Err(_) => return Ok(ret_slate), + Ok(s) => { + let parent_key_id = w.parent_key_id(); + match update_tx_slate_state( + w, + (&self.keychain_mask).as_ref(), + &parent_key_id, + &s, + ) { + Ok(_) => {} + Err(e) => error!("Error on updating slate state: {}", e), + } + Ok(s) + } + Err(e) => { + error!("Error on sending over Tor: {}", e); + Ok(ret_slate) + } } } None => Ok(ret_slate), @@ -416,8 +430,8 @@ where /// ``` /// # 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); + /// let mut api_owner = Owner::new(wallet.clone(), None, std::path::PathBuf::from("grin-wallet.toml")); + /// let mut api_foreign = Foreign::new(wallet.clone(), std::path::PathBuf::from("grin-wallet.toml"), None, None, false); /// /// // . . . /// // Issue the invoice tx via the owner API @@ -443,12 +457,43 @@ where true => false, false => post_automatically, }; - foreign::finalize_tx( - &mut **w, - (&self.keychain_mask).as_ref(), - slate, - post_automatically, - ) + foreign::finalize_tx(w, (&self.keychain_mask).as_ref(), slate, post_automatically) + } + + // Below is a foreign wrapper around owner calls to 'new' and 'sign' which are only executed + // if this is a receiving contract. This preserves the ability to receive on a foreign interface. + /// TODO + pub fn contract_new( + &self, + keychain_mask: Option<&SecretKey>, + args: &ContractNewArgsAPI, + ) -> Result { + let mut w_lock = self.wallet_inst.lock(); + let w = w_lock.lc_provider()?.wallet_inst()?; + foreign::contract_new(w, keychain_mask, args) + } + + /// TODO + pub fn contract_sign( + &self, + keychain_mask: Option<&SecretKey>, + slate: &Slate, + args: &ContractSetupArgsAPI, + ) -> Result { + let mut w_lock = self.wallet_inst.lock(); + let w = w_lock.lc_provider()?.wallet_inst()?; + foreign::contract_sign(w, keychain_mask, args, slate) + } + + /// TODO + pub fn verify_payment_proof_invoice( + &self, + recipient_address: &DalekPublicKey, + proof: &InvoiceProof, + ) -> Result<(), Error> { + let mut w_lock = self.wallet_inst.lock(); + let w = w_lock.lc_provider()?.wallet_inst()?; + foreign::verify_payment_proof_invoice(w, recipient_address, proof) } } @@ -494,19 +539,22 @@ macro_rules! doctest_helper_setup_doc_env_foreign { 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(), + let node_client = HTTPNodeClient::new( + &wallet_config.check_node_api_http_addr, + None, + wallet_config.api_request_timeout(), ) - as Box< - dyn WalletInst< - 'static, - DefaultLCProvider, - HTTPNodeClient, - ExtKeychain, - >, - >; + .unwrap(); + let mut wallet = + Box::new(DefaultWalletImpl::::new(node_client.clone()).unwrap()) + as Box< + dyn WalletInst< + 'static, + DefaultLCProvider, + HTTPNodeClient, + ExtKeychain, + >, + >; let lc = wallet.lc_provider().unwrap(); let _ = lc.set_top_level_directory(&wallet_config.data_file_dir); lc.open_wallet(None, pw, false, false); diff --git a/api/src/foreign_rpc.rs b/api/src/foreign_rpc.rs index 1d2b526a0..78a44ea8b 100644 --- a/api/src/foreign_rpc.rs +++ b/api/src/foreign_rpc.rs @@ -21,6 +21,9 @@ use crate::libwallet::{ }; use crate::{Foreign, ForeignCheckMiddlewareFn}; use easy_jsonrpc_mw; +use grin_wallet_config::initial_setup_wallet; +use libwallet::SlatepackAddress; +use std::path::PathBuf; /// Public definition used to generate Foreign jsonrpc api. /// * When running `grin-wallet listen` with defaults, the V2 api is available at @@ -52,6 +55,7 @@ pub trait ForeignRpc { "Ok": { "foreign_api_version": 2, "supported_slate_versions": [ + "V5", "V4" ] } @@ -205,7 +209,7 @@ pub trait ForeignRpc { "method": "finalize_tx", "id": 1, "params": [{ - "ver": "4:2", + "ver": "5:2", "id": "0436430c-2b02-624c-2032-570501212b00", "sta": "I2", "off": "383bc9df0dd332629520a0a72f8dd7f0e97d579dccb4dbdc8592aa3d424c846c", @@ -275,7 +279,7 @@ pub trait ForeignRpc { } ], "sta": "I3", - "ver": "4:2" + "ver": "5:2" } } } @@ -309,6 +313,16 @@ where ) -> Result { let version = in_slate.version(); let slate_from = Slate::from(in_slate); + let dest = match dest { + None => None, + Some(a) => match SlatepackAddress::try_from(a.as_str()) { + Ok(d) => Some(d), + Err(_) => { + error!("Error parsing Slatepack address: {}", a); + None + } + }, + }; let out_slate = Foreign::receive_tx( self, &slate_from, @@ -364,6 +378,14 @@ pub fn run_doctest_foreign( let _ = fs::remove_dir_all(test_dir); global::set_local_chain_type(ChainTypes::AutomatedTesting); + let _ = fs::create_dir_all(test_dir); + let config = initial_setup_wallet( + &ChainTypes::AutomatedTesting, + Some(PathBuf::from(test_dir)), + false, + ) + .unwrap(); + let mut wallet_proxy: WalletProxy< DefaultLCProvider, LocalWalletClient, @@ -381,7 +403,6 @@ pub fn run_doctest_foreign( Box::new(DefaultWalletImpl::::new(client1.clone()).unwrap()) as Box< dyn WalletInst< - 'static, DefaultLCProvider, LocalWalletClient, ExtKeychain, @@ -416,7 +437,6 @@ pub fn run_doctest_foreign( Box::new(DefaultWalletImpl::::new(client2.clone()).unwrap()) as Box< dyn WalletInst< - 'static, DefaultLCProvider, LocalWalletClient, ExtKeychain, @@ -473,7 +493,7 @@ pub fn run_doctest_foreign( amount, ..Default::default() }; - api_impl::owner::issue_invoice_tx(&mut **w, (&mask2).as_ref(), args, true).unwrap() + api_impl::owner::issue_invoice_tx(w, (&mask2).as_ref(), args, true).unwrap() }; slate = { let mut w_lock = wallet1.lock(); @@ -487,8 +507,7 @@ pub fn run_doctest_foreign( selection_strategy_is_use_all: true, ..Default::default() }; - api_impl::owner::process_invoice_tx(&mut **w, (&mask1).as_ref(), &slate, args, true) - .unwrap() + api_impl::owner::process_invoice_tx(w, (&mask1).as_ref(), &slate, args, true).unwrap() }; println!("INIT INVOICE SLATE"); // Spit out slate for input to finalize_tx @@ -508,15 +527,27 @@ pub fn run_doctest_foreign( selection_strategy_is_use_all: true, ..Default::default() }; - let slate = api_impl::owner::init_send_tx(&mut **w, (&mask1).as_ref(), args, true).unwrap(); + let slate = api_impl::owner::init_send_tx(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), + false => Foreign::new( + wallet1, + config.config_file_path, + mask1, + Some(test_check_middleware), + true, + ), + true => Foreign::new( + wallet2, + config.config_file_path, + mask2, + Some(test_check_middleware), + true, + ), }; api_foreign.doctest_mode = true; let foreign_api = &api_foreign as &dyn ForeignRpc; diff --git a/api/src/lib.rs b/api/src/lib.rs index 1b7949163..8827d5fa1 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -42,12 +42,15 @@ mod foreign_rpc; mod owner; mod owner_rpc; +mod tor_config; 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; +#[doc(hidden)] +pub use crate::tor_config::ConfigPath; pub use crate::foreign_rpc::foreign_rpc as foreign_rpc_client; pub use crate::foreign_rpc::run_doctest_foreign; diff --git a/api/src/owner.rs b/api/src/owner.rs index d291b2be4..6eee54be7 100644 --- a/api/src/owner.rs +++ b/api/src/owner.rs @@ -14,20 +14,19 @@ //! Owner API External Definition -use chrono::prelude::*; -use ed25519_dalek::SecretKey as DalekSecretKey; -use grin_wallet_libwallet::mwixnet::{MixnetReqCreationParams, SwapReq}; -use grin_wallet_libwallet::RetrieveTxQueryArgs; -use uuid::Uuid; - use crate::config::{TorConfig, WalletConfig}; use crate::core::core::OutputFeatures; use crate::core::global; -use crate::impls::HttpSlateSender; use crate::impls::SlateSender as _; +use crate::impls::TorSlateSender; use crate::keychain::{Identifier, Keychain}; use crate::libwallet::api_impl::owner_updater::{start_updater_log_thread, StatusMessage}; +use crate::libwallet::api_impl::types::update_tx_slate_state; use crate::libwallet::api_impl::{owner, owner_updater}; +use crate::libwallet::contract::proofs::InvoiceProof; +use crate::libwallet::contract::types::{ + ContractNewArgsAPI, ContractRevokeArgsAPI, ContractSetupArgsAPI, ContractView, +}; use crate::libwallet::{ AcctPathMapping, BuiltOutput, Error, InitTxArgs, IssueInvoiceTxArgs, NodeClient, NodeHeightResult, OutputCommitMapping, PaymentProof, Slate, Slatepack, SlatepackAddress, @@ -36,13 +35,25 @@ use crate::libwallet::{ use crate::util::logger::LoggingConfig; use crate::util::secp::{key::SecretKey, pedersen::Commitment}; use crate::util::{from_hex, static_secp_instance, Mutex, ZeroingString}; +use grin_wallet_config::config::{ + reload_global_config, update_global_config, WALLET_CONFIG_FILE_NAME, +}; +use grin_wallet_libwallet::mwixnet::{MixnetReqCreationParams, SwapReq}; +use grin_wallet_libwallet::RetrieveTxQueryArgs; use grin_wallet_util::OnionV3Address; + +use chrono::prelude::*; +use ed25519_dalek::SigningKey as DalekSecretKey; use std::convert::TryFrom; +use std::fs::File; +use std::io::Write; +use std::path::PathBuf; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::mpsc::{channel, Sender}; use std::sync::Arc; use std::thread; use std::time::Duration; +use uuid::Uuid; /// Main interface into all wallet API functions. /// Wallet APIs are split into two seperate blocks of functionality @@ -64,11 +75,13 @@ where C: NodeClient + 'static, K: Keychain + 'static, { - /// contain all methods to manage the wallet + /// Contains all methods to manage the wallet pub wallet_inst: Arc>>>, + /// Wallet configuration path + config_path: crate::ConfigPath, /// Flag to normalize some output during testing. Can mostly be ignored. pub doctest_mode: bool, - /// retail TLD during doctest + /// Retail TLD during doctest pub doctest_retain_tld: bool, /// Share ECDH key pub shared_key: Arc>>, @@ -81,9 +94,6 @@ where /// Holds all update and status messages returned by the /// updater process updater_messages: Arc>>, - /// Optional TOR configuration, holding address of sender and - /// data directory - tor_config: Mutex>, } impl Owner @@ -97,20 +107,22 @@ where /// /// 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 + /// initializing 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. /// * `custom_channel` - A custom MPSC Tx/Rx pair to capture status /// updates - /// [`WalletBackend`](../grin_wallet_libwallet/types/trait.WalletBackend.html) trait. + /// * `config_path` - Path to the wallet configuration file /// /// # Returns /// * An instance of the OwnerApi holding a reference to the provided wallet /// /// # Example /// ``` + /// use std::path::PathBuf; /// use grin_keychain as keychain; /// use grin_util as util; /// use grin_core; @@ -144,14 +156,14 @@ where /// /// // 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(); + /// let node_client = HTTPNodeClient::new(&wallet_config.check_node_api_http_addr, None, wallet_config.api_request_timeout()).unwrap(); /// /// // impls::DefaultWalletImpl is provided for convenience in instantiating the wallet - /// // It contains the LMDBBackend, DefaultLCProvider (lifecycle) and ExtKeychain used + /// // It contains the WalletBackend, 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()) + /// let mut wallet = Box::new(DefaultWalletImpl::::new(node_client.clone()).unwrap()) /// as Box, HTTPNodeClient, ExtKeychain>>; /// /// // Wallet LifeCycle Provider provides all functions init wallet and work with seeds, etc... @@ -168,15 +180,19 @@ where /// // All wallet functions operate on an Arc::Mutex to allow multithreading where needed /// let mut wallet = Arc::new(Mutex::new(wallet)); /// - /// let api_owner = Owner::new(wallet.clone(), None); + /// let api_owner = Owner::new(wallet.clone(), None, PathBuf::from(dir).join("grin-wallet.toml")); /// // .. perform wallet operations /// /// ``` - pub fn new( + pub fn new

( wallet_inst: Arc>>>, custom_channel: Option>, - ) -> Self { + config_path: P, + ) -> Self + where + P: Into, + { let updater_running = Arc::new(AtomicBool::new(false)); let updater = Arc::new(Mutex::new(owner_updater::Updater::new( wallet_inst.clone(), @@ -195,6 +211,7 @@ where Owner { wallet_inst, + config_path: config_path.into(), doctest_mode: false, doctest_retain_tld: false, shared_key: Arc::new(Mutex::new(None)), @@ -202,21 +219,49 @@ where updater_running, status_tx: Mutex::new(Some(tx)), updater_messages, - tor_config: Mutex::new(None), } } + /// Return the active wallet configuration path. + pub fn config_path(&self) -> PathBuf { + self.config_path.get() + } + + /// Return the shared wallet configuration path. + #[doc(hidden)] + pub fn shared_config_path(&self) -> crate::ConfigPath { + self.config_path.clone() + } + /// Set the TOR configuration for this instance of the OwnerAPI, used during - /// `init_send_tx` when send args are present and a TOR address is specified + /// `init_send_tx` when send args are present and a TOR address is specified, + /// this will also update persistent config and will restart foreign listener /// /// # Arguments /// * `tor_config` - The optional [TorConfig](#) to use /// # Returns - /// * Nothing + /// * Result Containing: + /// * `Ok(())` if the config was correctly saved + /// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered. + /// - pub fn set_tor_config(&self, tor_config: Option) { - let mut lock = self.tor_config.lock(); - *lock = tor_config; + pub fn set_tor_config(&self, tor_config: Option) -> Result<(), Error> { + let _wallet_lock = self.wallet_inst.lock(); + update_global_config(&self.config_path(), |config| { + if let Some(tor_config) = tor_config { + config.members.tor = Some(tor_config); + } else if let Some(tor) = config.members.tor.as_mut() { + tor.use_tor_listener = false; + tor.skip_send_attempt = Some(true); + } else { + let mut tor_config = config.tor_config(); + tor_config.use_tor_listener = false; + tor_config.skip_send_attempt = Some(true); + config.members.tor = Some(tor_config); + } + Ok(()) + }) + .map_err(|e| Error::TorConfig(format!("{}", e))) } /// Returns a list of accounts stored in the wallet (i.e. mappings between @@ -241,7 +286,7 @@ where /// ``` /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); /// - /// let api_owner = Owner::new(wallet.clone(), None); + /// let api_owner = Owner::new(wallet.clone(), None, std::path::PathBuf::from("grin-wallet.toml")); /// /// let result = api_owner.accounts(None); /// @@ -258,7 +303,7 @@ where let w = w_lock.lc_provider()?.wallet_inst()?; // Test keychain mask, to keep API consistent let _ = w.keychain(keychain_mask)?; - owner::accounts(&mut **w) + owner::accounts(w) } /// Creates a new 'account', which is a mapping of a user-specified @@ -292,7 +337,7 @@ where /// ``` /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); /// - /// let api_owner = Owner::new(wallet.clone(), None); + /// let api_owner = Owner::new(wallet.clone(), None, std::path::PathBuf::from("grin-wallet.toml")); /// /// let result = api_owner.create_account_path(None, "account1"); /// @@ -308,7 +353,7 @@ where ) -> Result { let mut w_lock = self.wallet_inst.lock(); let w = w_lock.lc_provider()?.wallet_inst()?; - owner::create_account_path(&mut **w, keychain_mask, label) + owner::create_account_path(w, keychain_mask, label) } /// Sets the wallet's currently active account. This sets the @@ -339,7 +384,7 @@ where /// ``` /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); /// - /// let api_owner = Owner::new(wallet.clone(), None); + /// let api_owner = Owner::new(wallet.clone(), None, std::path::PathBuf::from("grin-wallet.toml")); /// /// let result = api_owner.create_account_path(None, "account1"); /// @@ -358,7 +403,7 @@ where let w = w_lock.lc_provider()?.wallet_inst()?; // Test keychain mask, to keep API consistent let _ = w.keychain(keychain_mask)?; - owner::set_active_account(&mut **w, label) + owner::set_active_account(w, label) } /// Returns a list of outputs from the active account in the wallet. @@ -395,7 +440,7 @@ where /// ``` /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); /// - /// let api_owner = Owner::new(wallet.clone(), None); + /// let api_owner = Owner::new(wallet.clone(), None, std::path::PathBuf::from("grin-wallet.toml")); /// let show_spent = false; /// let update_from_node = true; /// let tx_id = None; @@ -466,7 +511,7 @@ where /// ``` /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); /// - /// let api_owner = Owner::new(wallet.clone(), None); + /// let api_owner = Owner::new(wallet.clone(), None, std::path::PathBuf::from("grin-wallet.toml")); /// let update_from_node = true; /// let tx_id = None; /// let tx_slate_id = None; @@ -545,7 +590,7 @@ where /// ``` /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); /// - /// let mut api_owner = Owner::new(wallet.clone(), None); + /// let mut api_owner = Owner::new(wallet.clone(), None, std::path::PathBuf::from("grin-wallet.toml")); /// let update_from_node = true; /// let minimum_confirmations=10; /// @@ -632,7 +677,7 @@ where /// ``` /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); /// - /// let mut api_owner = Owner::new(wallet.clone(), None); + /// let mut api_owner = Owner::new(wallet.clone(), None, std::path::PathBuf::from("grin-wallet.toml")); /// // Attempt to create a transaction using the 'default' account /// let args = InitTxArgs { /// src_acct_name: None, @@ -662,60 +707,59 @@ where args: InitTxArgs, ) -> Result { let send_args = args.send_args.clone(); - let slate = { + let dest = match send_args.as_ref() { + Some(args) if !args.dest.is_empty() => { + Some(SlatepackAddress::try_from(args.dest.as_str())?) + } + _ => None, + }; + let (slate, tor_config) = { let mut w_lock = self.wallet_inst.lock(); + let tor_config = send_args + .as_ref() + .map(|_| crate::tor_config::load(&self.config_path())) + .transpose()?; let w = w_lock.lc_provider()?.wallet_inst()?; - owner::init_send_tx(&mut **w, keychain_mask, args, self.doctest_mode)? + let slate = owner::init_send_tx(w, keychain_mask, args, self.doctest_mode)?; + (slate, tor_config) }; // Helper functionality. If send arguments exist, attempt to send sync and // finalize - let skip_tor = match send_args.as_ref() { - None => false, - Some(sa) => sa.skip_tor, - }; match send_args { Some(sa) => { - let tor_config_lock = self.tor_config.lock(); - let tc = tor_config_lock.clone(); - let tc = match tc { - Some(mut c) => { - c.skip_send_attempt = Some(skip_tor); - Some(c) - } - None => None, - }; - let res = try_slatepack_sync_workflow( - &slate, - &sa.dest, - tc, - None, - false, - self.doctest_mode, - ); + let tc = tor_config.ok_or_else(|| { + Error::TorConfig("Tor config was not loaded with send arguments".into()) + })?; + let can_send = tc.send_tor(sa.skip_tor); + if self.doctest_mode || !can_send || dest.is_none() { + return Ok(slate); + } + let res = + try_slatepack_sync_workflow(&slate, &dest.unwrap(), Some(tc), None, false); match res { - Ok(Some(s)) => { + Ok(s) => { + self.tx_lock_outputs(keychain_mask, &s)?; + let ret_slate = self.finalize_tx(keychain_mask, &s)?; if sa.post_tx { - self.tx_lock_outputs(keychain_mask, &s)?; - let ret_slate = self.finalize_tx(keychain_mask, &s)?; let result = self.post_tx(keychain_mask, &ret_slate, sa.fluff); match result { Ok(_) => { info!("Tx sent ok",); - return Ok(ret_slate); + Ok(ret_slate) } Err(e) => { error!("Tx sent fail: {}", e); - return Err(e); + Err(e) } } } else { - self.tx_lock_outputs(keychain_mask, &s)?; - let ret_slate = self.finalize_tx(keychain_mask, &s)?; - return Ok(ret_slate); + Ok(ret_slate) } } - Ok(None) => Ok(slate), - Err(_) => Ok(slate), + Err(e) => { + error!("Error on sending over Tor: {}", e); + Ok(slate) + } } } None => Ok(slate), @@ -724,7 +768,7 @@ where /// Issues a new invoice transaction slate, essentially a `request for payment`. /// The slate created by this function will contain the amount, an output for the amount, - /// as well as round 1 of singature creation complete. The slate should then be send + /// as well as round 1 of signature creation complete. The slate should then be sent /// to the payer, who should add their inputs and signature data and return the slate /// via the [Foreign API's `finalize_tx`](struct.Foreign.html#method.finalize_tx) method. /// @@ -744,7 +788,7 @@ where /// ``` /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); /// - /// let mut api_owner = Owner::new(wallet.clone(), None); + /// let mut api_owner = Owner::new(wallet.clone(), None, std::path::PathBuf::from("grin-wallet.toml")); /// /// let args = IssueInvoiceTxArgs { /// amount: 60_000_000_000, @@ -764,14 +808,70 @@ where ) -> Result { let mut w_lock = self.wallet_inst.lock(); let w = w_lock.lc_provider()?.wallet_inst()?; - owner::issue_invoice_tx(&mut **w, keychain_mask, args, self.doctest_mode) + owner::issue_invoice_tx(w, keychain_mask, args, self.doctest_mode) } - /// Processes an invoice tranaction created by another party, essentially + /// Initiate a new contract. Also performs the initial setup on the slate. + pub fn contract_new( + &self, + keychain_mask: Option<&SecretKey>, + args: &ContractNewArgsAPI, + ) -> Result { + let mut w_lock = self.wallet_inst.lock(); + let w = w_lock.lc_provider()?.wallet_inst()?; + owner::contract_new(w, keychain_mask, args) + } + + /// Summarise a contract slate, including its participants, signatures and net change. + pub fn contract_view( + &self, + keychain_mask: Option<&SecretKey>, + slate: &Slate, + ) -> Result { + let mut w_lock = self.wallet_inst.lock(); + let w = w_lock.lc_provider()?.wallet_inst()?; + owner::contract_view(w, keychain_mask, slate) + } + + /// Sign a contract, running setup first if it has not been done yet. + pub fn contract_sign( + &self, + keychain_mask: Option<&SecretKey>, + slate: &Slate, + args: &ContractSetupArgsAPI, + ) -> Result { + let mut w_lock = self.wallet_inst.lock(); + let w = w_lock.lc_provider()?.wallet_inst()?; + owner::contract_sign(w, keychain_mask, args, slate) + } + + /// Return the participant index in the slate that matches this wallet's context. + pub fn get_slate_index_matching_my_context( + &self, + keychain_mask: Option<&SecretKey>, + slate: &Slate, + ) -> Result { + let mut w_lock = self.wallet_inst.lock(); + let w = w_lock.lc_provider()?.wallet_inst()?; + owner::get_slate_index_matching_my_context(w, keychain_mask, slate) + } + + /// Revoke a contract by double-spending one of its locked inputs. + pub fn contract_revoke( + &self, + keychain_mask: Option<&SecretKey>, + args: &ContractRevokeArgsAPI, + ) -> Result, Error> { + let mut w_lock = self.wallet_inst.lock(); + let w = w_lock.lc_provider()?.wallet_inst()?; + owner::contract_revoke(w, keychain_mask, args) + } + + /// Processes an invoice transaction created by another party, essentially /// a `request for payment`. The incoming slate should contain a requested - /// amount, an output created by the invoicer convering the amount, and + /// amount, an output created by the invoicer converting the amount, and /// part 1 of signature creation completed. This function will add inputs - /// equalling the amount + fees, as well as perform round 1 and 2 of signature + /// equaling the amount + fees, as well as perform round 1 and 2 of signature /// creation. /// /// Callers should note that no prompting of the user will be done by this function @@ -805,7 +905,7 @@ where /// ``` /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); /// - /// let mut api_owner = Owner::new(wallet.clone(), None); + /// let mut api_owner = Owner::new(wallet.clone(), None, std::path::PathBuf::from("grin-wallet.toml")); /// /// // . . . /// // The slate has been recieved from the invoicer, somehow @@ -834,34 +934,59 @@ where slate: &Slate, args: InitTxArgs, ) -> Result { - let mut w_lock = self.wallet_inst.lock(); - let w = w_lock.lc_provider()?.wallet_inst()?; let send_args = args.send_args.clone(); - let slate = - owner::process_invoice_tx(&mut **w, keychain_mask, slate, args, self.doctest_mode)?; + let dest = match send_args.as_ref() { + Some(args) if !args.dest.is_empty() => { + Some(SlatepackAddress::try_from(args.dest.as_str())?) + } + _ => None, + }; + let (slate, tor_config) = { + let mut w_lock = self.wallet_inst.lock(); + let tor_config = send_args + .as_ref() + .map(|_| crate::tor_config::load(&self.config_path())) + .transpose()?; + let w = w_lock.lc_provider()?.wallet_inst()?; + let slate = + owner::process_invoice_tx(w, keychain_mask, slate, args, self.doctest_mode)?; + (slate, tor_config) + }; // Helper functionality. If send arguments exist, attempt to send match send_args { Some(sa) => { - let tor_config_lock = self.tor_config.lock(); - let tc = tor_config_lock.clone(); - let tc = match tc { - Some(mut c) => { - c.skip_send_attempt = Some(sa.skip_tor); - Some(c) - } - None => None, - }; - let res = try_slatepack_sync_workflow( - &slate, - &sa.dest, - tc, - None, - true, - self.doctest_mode, - ); + let tc = tor_config.ok_or_else(|| { + Error::TorConfig("Tor config was not loaded with send arguments".into()) + })?; + let can_send = tc.send_tor(sa.skip_tor); + if self.doctest_mode || !can_send || dest.is_none() { + return Ok(slate); + } + let dest = dest.unwrap(); + let res = try_slatepack_sync_workflow(&slate, &dest, Some(tc), None, true); match res { - Ok(s) => Ok(s.unwrap()), - Err(_) => Ok(slate), + Ok(s) => { + // Update slate state. + { + let mut w_lock = self.wallet_inst.lock(); + let w = w_lock.lc_provider()?.wallet_inst()?; + let parent_key_id = w.parent_key_id(); + match update_tx_slate_state(w, keychain_mask, &parent_key_id, &s) { + Ok(_) => {} + Err(e) => error!("Error on updating slate state: {}", e), + } + } + // Output slatepack message to file. + match output_slatepack_file(&self, keychain_mask, &s, Some(dest)) { + Ok(_) => {} + Err(e) => error!("Error on saving output slatepack message: {}", e), + } + Ok(s) + } + Err(e) => { + error!("Error on sending over Tor: {}", e); + Ok(slate) + } } } None => Ok(slate), @@ -871,7 +996,7 @@ where /// Locks the outputs associated with the inputs to the transaction in the given /// [`Slate`](../grin_wallet_libwallet/slate/struct.Slate.html), /// making them unavailable for use in further transactions. This function is called - /// by the sender, (or more generally, all parties who have put inputs into the transaction,) + /// by the sender, (or more generally, all parties who have put inputs into the transaction) /// and must be called before the corresponding call to [`finalize_tx`](struct.Owner.html#method.finalize_tx) /// that completes the transaction. /// @@ -899,7 +1024,7 @@ where /// ``` /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); /// - /// let mut api_owner = Owner::new(wallet.clone(), None); + /// let mut api_owner = Owner::new(wallet.clone(), None, std::path::PathBuf::from("grin-wallet.toml")); /// let args = InitTxArgs { /// src_acct_name: None, /// amount: 2_000_000_000, @@ -929,7 +1054,7 @@ where ) -> Result<(), Error> { let mut w_lock = self.wallet_inst.lock(); let w = w_lock.lc_provider()?.wallet_inst()?; - owner::tx_lock_outputs(&mut **w, keychain_mask, slate) + owner::tx_lock_outputs(w, keychain_mask, slate) } /// Finalizes a transaction, after all parties @@ -961,7 +1086,7 @@ where /// ``` /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); /// - /// let mut api_owner = Owner::new(wallet.clone(), None); + /// let mut api_owner = Owner::new(wallet.clone(), None, std::path::PathBuf::from("grin-wallet.toml")); /// let args = InitTxArgs { /// src_acct_name: None, /// amount: 2_000_000_000, @@ -995,7 +1120,7 @@ where ) -> Result { let mut w_lock = self.wallet_inst.lock(); let w = w_lock.lc_provider()?.wallet_inst()?; - owner::finalize_tx(&mut **w, keychain_mask, slate) + owner::finalize_tx(w, keychain_mask, slate) } /// Posts a completed transaction to the listening node for validation and inclusion in a block @@ -1020,7 +1145,7 @@ where /// ``` /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); /// - /// let mut api_owner = Owner::new(wallet.clone(), None); + /// let mut api_owner = Owner::new(wallet.clone(), None, std::path::PathBuf::from("grin-wallet.toml")); /// let args = InitTxArgs { /// src_acct_name: None, /// amount: 2_000_000_000, @@ -1091,7 +1216,7 @@ where /// ``` /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); /// - /// let mut api_owner = Owner::new(wallet.clone(), None); + /// let mut api_owner = Owner::new(wallet.clone(), None, std::path::PathBuf::from("grin-wallet.toml")); /// let args = InitTxArgs { /// src_acct_name: None, /// amount: 2_000_000_000, @@ -1162,7 +1287,7 @@ where /// ``` /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); /// - /// let api_owner = Owner::new(wallet.clone(), None); + /// let api_owner = Owner::new(wallet.clone(), None, std::path::PathBuf::from("grin-wallet.toml")); /// let update_from_node = true; /// let tx_id = None; /// let tx_slate_id = None; @@ -1186,7 +1311,7 @@ where let w = w_lock.lc_provider()?.wallet_inst()?; // Test keychain mask, to keep API consistent let _ = w.keychain(keychain_mask)?; - owner::get_stored_tx(&**w, tx_id, slate_id) + owner::get_stored_tx(w, tx_id, slate_id) } /// Return the rewind hash of the wallet. @@ -1206,7 +1331,7 @@ where /// ``` /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); /// - /// let mut api_owner = Owner::new(wallet.clone(), None); + /// let mut api_owner = Owner::new(wallet.clone(), None, std::path::PathBuf::from("grin-wallet.toml")); /// let result = api_owner.scan( /// None, /// Some(20000), @@ -1246,7 +1371,7 @@ where /// ``` /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); /// - /// let mut api_owner = Owner::new(wallet.clone(), None); + /// let mut api_owner = Owner::new(wallet.clone(), None, std::path::PathBuf::from("grin-wallet.toml")); /// let result = api_owner.scan( /// None, /// Some(20000), @@ -1310,7 +1435,7 @@ where /// ``` /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); /// - /// let mut api_owner = Owner::new(wallet.clone(), None); + /// let mut api_owner = Owner::new(wallet.clone(), None, std::path::PathBuf::from("grin-wallet.toml")); /// let result = api_owner.scan( /// None, /// Some(20000), @@ -1368,7 +1493,7 @@ where /// ``` /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); /// - /// let api_owner = Owner::new(wallet.clone(), None); + /// let api_owner = Owner::new(wallet.clone(), None, std::path::PathBuf::from("grin-wallet.toml")); /// let result = api_owner.node_height(None); /// /// if let Ok(node_height_result) = result { @@ -1425,7 +1550,7 @@ where /// ``` /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); /// - /// let api_owner = Owner::new(wallet.clone(), None); + /// let api_owner = Owner::new(wallet.clone(), None, std::path::PathBuf::from("grin-wallet.toml")); /// let result = api_owner.get_top_level_directory(); /// /// if let Ok(dir) = result { @@ -1450,6 +1575,9 @@ where /// Set [`get_top_level_directory`](struct.Owner.html#method.get_top_level_directory) for a /// description of the top level directory and default paths. /// + /// The wallet must be closed and the updater stopped before changing this directory. + /// Open the wallet again afterwards to load its database from the new location. + /// /// # Arguments /// /// * `dir`: The new top-level directory path (either relative to current directory or @@ -1473,7 +1601,7 @@ where /// # .ok_or("Failed to convert tmpdir path to string.".to_owned()) /// # .unwrap(); /// - /// let api_owner = Owner::new(wallet.clone(), None); + /// let api_owner = Owner::new(wallet.clone(), None, std::path::PathBuf::from(dir)); /// let result = api_owner.set_top_level_directory(dir); /// /// if let Ok(dir) = result { @@ -1482,9 +1610,25 @@ where /// ``` pub fn set_top_level_directory(&self, dir: &str) -> Result<(), Error> { + if self.updater_running.load(Ordering::Relaxed) { + return Err(Error::Lifecycle( + "Stop the updater before changing the top-level directory".into(), + )); + } let mut w_lock = self.wallet_inst.lock(); let lc = w_lock.lc_provider()?; - lc.set_top_level_directory(dir) + if lc.wallet_inst().is_ok() { + return Err(Error::Lifecycle( + "Close the wallet before changing the top-level directory".into(), + )); + } + let config_path = PathBuf::from(dir).join(WALLET_CONFIG_FILE_NAME); + if config_path.exists() { + reload_global_config(&config_path)?; + } + lc.set_top_level_directory(dir)?; + self.config_path.set(config_path); + Ok(()) } /// Create a `grin-wallet.toml` configuration file in the top-level directory for the @@ -1523,7 +1667,7 @@ where /// # .ok_or("Failed to convert tmpdir path to string.".to_owned()) /// # .unwrap(); /// - /// let api_owner = Owner::new(wallet.clone(), None); + /// let api_owner = Owner::new(wallet.clone(), None, std::path::PathBuf::from(dir)); /// let _ = api_owner.set_top_level_directory(dir); /// /// let result = api_owner.create_config(&ChainTypes::Mainnet, None, None, None); @@ -1592,7 +1736,7 @@ where /// # .to_str() /// # .ok_or("Failed to convert tmpdir path to string.".to_owned()) /// # .unwrap(); - /// let api_owner = Owner::new(wallet.clone(), None); + /// let api_owner = Owner::new(wallet.clone(), None, std::path::PathBuf::from(dir)); /// let _ = api_owner.set_top_level_directory(dir); /// /// // Create configuration @@ -1659,7 +1803,7 @@ where /// # .to_str() /// # .ok_or("Failed to convert tmpdir path to string.".to_owned()) /// # .unwrap(); - /// let api_owner = Owner::new(wallet.clone(), None); + /// let api_owner = Owner::new(wallet.clone(), None, std::path::PathBuf::from(dir)); /// let _ = api_owner.set_top_level_directory(dir); /// /// // Create configuration @@ -1716,7 +1860,7 @@ where /// use grin_core::global::ChainTypes; /// /// // Set up as above - /// # let api_owner = Owner::new(wallet.clone(), None); + /// # let api_owner = Owner::new(wallet.clone(), None, std::path::PathBuf::from("grin-wallet.toml")); /// /// let res = api_owner.close_wallet(None); /// @@ -1752,7 +1896,7 @@ where /// use grin_core::global::ChainTypes; /// /// // Set up as above - /// # let api_owner = Owner::new(wallet.clone(), None); + /// # let api_owner = Owner::new(wallet.clone(), None, std::path::PathBuf::from("grin-wallet.toml")); /// /// let pw = ZeroingString::from("my_password"); /// let res = api_owner.get_mnemonic(None, pw); @@ -1797,7 +1941,7 @@ where /// use grin_core::global::ChainTypes; /// /// // Set up as above - /// # let api_owner = Owner::new(wallet.clone(), None); + /// # let api_owner = Owner::new(wallet.clone(), None, std::path::PathBuf::from("grin-wallet.toml")); /// /// let old = ZeroingString::from("my_password"); /// let new = ZeroingString::from("new_password"); @@ -1840,7 +1984,7 @@ where /// use grin_core::global::ChainTypes; /// /// // Set up as above - /// # let api_owner = Owner::new(wallet.clone(), None); + /// # let api_owner = Owner::new(wallet.clone(), None, std::path::PathBuf::from("grin-wallet.toml")); /// /// let res = api_owner.delete_wallet(None); /// @@ -1896,7 +2040,7 @@ where /// use std::time::Duration; /// /// // Set up as above - /// # let api_owner = Owner::new(wallet.clone(), None); + /// # let api_owner = Owner::new(wallet.clone(), None, std::path::PathBuf::from("grin-wallet.toml")); /// /// let res = api_owner.start_updater(None, Duration::from_secs(60)); /// @@ -1951,7 +2095,7 @@ where /// use std::time::Duration; /// /// // Set up as above - /// # let api_owner = Owner::new(wallet.clone(), None); + /// # let api_owner = Owner::new(wallet.clone(), None, std::path::PathBuf::from("grin-wallet.toml")); /// /// let res = api_owner.start_updater(None, Duration::from_secs(60)); /// @@ -1993,7 +2137,7 @@ where /// use std::time::Duration; /// /// // Set up as above - /// # let api_owner = Owner::new(wallet.clone(), None); + /// # let api_owner = Owner::new(wallet.clone(), None, std::path::PathBuf::from("grin-wallet.toml")); /// /// let res = api_owner.start_updater(None, Duration::from_secs(60)); /// @@ -2059,7 +2203,7 @@ where /// use std::time::Duration; /// /// // Set up as above - /// # let api_owner = Owner::new(wallet.clone(), None); + /// # let api_owner = Owner::new(wallet.clone(), None, std::path::PathBuf::from("grin-wallet.toml")); /// /// let res = api_owner.get_slatepack_address(None, 0); /// @@ -2099,7 +2243,7 @@ where /// use std::time::Duration; /// /// // Set up as above - /// # let api_owner = Owner::new(wallet.clone(), None); + /// # let api_owner = Owner::new(wallet.clone(), None, std::path::PathBuf::from("grin-wallet.toml")); /// /// let res = api_owner.get_slatepack_secret_key(None, 0); /// @@ -2140,9 +2284,8 @@ where /// use std::time::Duration; /// /// // Set up as above - /// # let api_owner = Owner::new(wallet.clone(), None); + /// # let api_owner = Owner::new(wallet.clone(), None, std::path::PathBuf::from("grin-wallet.toml")); /// - /// let mut api_owner = Owner::new(wallet.clone(), None); /// let args = InitTxArgs { /// src_acct_name: None, /// amount: 2_000_000_000, @@ -2210,7 +2353,7 @@ where /// use std::time::Duration; /// /// // Set up as above - /// # let api_owner = Owner::new(wallet.clone(), None); + /// # let api_owner = Owner::new(wallet.clone(), None, std::path::PathBuf::from("grin-wallet.toml")); /// // ... receive a slatepack from somewhere /// # let slatepack_string = String::from(""); /// let res = api_owner.slate_from_slatepack_message( @@ -2260,7 +2403,7 @@ where /// use std::time::Duration; /// /// // Set up as above - /// # let api_owner = Owner::new(wallet.clone(), None); + /// # let api_owner = Owner::new(wallet.clone(), None, std::path::PathBuf::from("grin-wallet.toml")); /// # let slatepack_string = String::from(""); /// // .. receive a slatepack from somewhere /// let res = api_owner.decode_slatepack_message( @@ -2318,7 +2461,7 @@ where /// ``` /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); /// - /// let api_owner = Owner::new(wallet.clone(), None); + /// let api_owner = Owner::new(wallet.clone(), None, std::path::PathBuf::from("grin-wallet.toml")); /// let update_from_node = true; /// let tx_id = None; /// let tx_slate_id = Some(Uuid::parse_str("0436430c-2b02-624c-2032-570501212b00").unwrap()); @@ -2356,6 +2499,33 @@ where ) } + /// Retrieve the invoice payment proof for a stored transaction. + /// FUTURE: likely merge with retrieve_payment_proof above. + pub fn retrieve_payment_proof_invoice( + &self, + keychain_mask: Option<&SecretKey>, + refresh_from_node: bool, + tx_id: Option, + tx_slate_id: Option, + ) -> Result { + let tx = { + let t = self.status_tx.lock(); + t.clone() + }; + let refresh_from_node = match self.updater_running.load(Ordering::Relaxed) { + true => false, + false => refresh_from_node, + }; + owner::retrieve_payment_proof_invoice( + self.wallet_inst.clone(), + keychain_mask, + &tx, + refresh_from_node, + tx_id, + tx_slate_id, + ) + } + /// Verifies a [PaymentProof](../grin_wallet_libwallet/api_impl/types/struct.PaymentProof.html) /// This process entails: /// @@ -2387,7 +2557,7 @@ where /// ``` /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); /// - /// let api_owner = Owner::new(wallet.clone(), None); + /// let api_owner = Owner::new(wallet.clone(), None, std::path::PathBuf::from("grin-wallet.toml")); /// let update_from_node = true; /// let tx_id = None; /// let tx_slate_id = Some(Uuid::parse_str("0436430c-2b02-624c-2032-570501212b00").unwrap()); @@ -2422,7 +2592,7 @@ where ) -> Result { let mut w_lock = self.wallet_inst.lock(); let w = w_lock.lc_provider()?.wallet_inst()?; - owner::build_output(&mut **w, keychain_mask, features, amount) + owner::build_output(w, keychain_mask, features, amount) } // MWIXNET @@ -2449,7 +2619,7 @@ where /// ``` /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); /// - /// let api_owner = Owner::new(wallet.clone(), None); + /// let api_owner = Owner::new(wallet.clone(), None, std::path::PathBuf::from("grin-wallet.toml")); /// let keychain_mask = None; /// let params = MixnetReqCreationParams { /// server_keys: vec![], // Public keys here in secret key representation @@ -2481,7 +2651,7 @@ where let mut w_lock = self.wallet_inst.lock(); let w = w_lock.lc_provider()?.wallet_inst()?; owner::create_mwixnet_req( - &mut **w, + w, keychain_mask, params, commitment, @@ -2494,85 +2664,55 @@ where /// attempt to send slate synchronously with TOR pub fn try_slatepack_sync_workflow( slate: &Slate, - dest: &str, + dest: &SlatepackAddress, tor_config: Option, - tor_sender: Option, + tor_sender: Option, send_to_finalize: bool, - test_mode: bool, -) -> Result, libwallet::Error> { - if let Some(tc) = &tor_config { - if tc.skip_send_attempt == Some(true) { - return Ok(None); - } - } +) -> Result { let mut ret_slate = Slate::blank(2, false); - let mut send_sync = |mut sender: HttpSlateSender, method_str: &str| match sender - .send_tx(&slate, send_to_finalize) - { - Ok(s) => { - ret_slate = s; - return Ok(()); - } - Err(e) => { - debug!( - "Send ({}): Could not send Slate via {}: {}", - method_str, method_str, e - ); - return Err(e); - } + let mut send_sync = |mut sender: TorSlateSender, method_str: &str| { + return match sender.send_tx(&slate, send_to_finalize) { + Ok(s) => { + ret_slate = s; + Ok(()) + } + Err(e) => { + debug!( + "Send ({}): Could not send Slate via {}: {}", + method_str, method_str, e + ); + Err(e) + } + }; }; - // Try parsing Slatepack address - match SlatepackAddress::try_from(dest) { - Ok(address) => { - let tor_addr = OnionV3Address::try_from(&address).unwrap(); - // Try sending to the destination via TOR - let sender = match tor_sender { - None => { - if test_mode { - None - } else { - match HttpSlateSender::with_socks_proxy( - &tor_addr.to_http_str(), - &tor_config.as_ref().unwrap().socks_proxy_addr, - &tor_config.as_ref().unwrap().send_config_dir, - tor_config.as_ref().unwrap().bridge.clone(), - tor_config.as_ref().unwrap().proxy.clone(), - ) { - Ok(s) => Some(s), - Err(e) => { - debug!("Send (TOR): Cannot create TOR Slate sender {:?}", e); - None - } - } - } - } - Some(s) => { - if test_mode { - None - } else { - Some(s) - } - } - }; - if let Some(s) = sender { - warn!("Attempting to send transaction via TOR"); - match send_sync(s, "TOR") { - Ok(_) => return Ok(Some(ret_slate)), + let tor_addr = OnionV3Address::try_from(dest).map_err(|_| { + Error::SlatepackAddress(format!( + "Destination {} is not a valid Onion address.", + dest + )) + })?; + let sender = match tor_sender { + None => { + if let Some(tc) = tor_config { + match TorSlateSender::new(&tor_addr.to_http_str(), tc) { + Ok(s) => s, Err(e) => { - debug!("Unable to send via TOR: {}", e); - warn!("Unable to send transaction via TOR"); + debug!("Send (Tor): Cannot create TOR Slate sender {:?}", e); + return Err(e); } } + } else { + return Err(Error::TorConfig("Tor config is not set".to_string())); } } - Err(e) => { - debug!("Send (TOR): Destination is not SlatepackAddress {:?}", e); - warn!("Destination is not a valid Slatepack address. Will output Slatepack.") - } + Some(s) => s, + }; + warn!("Attempting to send transaction via Tor"); + match send_sync(sender, "Tor") { + Ok(_) => Ok(ret_slate), + Err(e) => Err(e), } - - Ok(None) } #[doc(hidden)] @@ -2604,7 +2744,7 @@ macro_rules! doctest_helper_setup_doc_env { use uuid::Uuid; - // don't run on windows CI, which gives very inconsistent results + // don't run on Windows CI, which gives very inconsistent results if cfg!(windows) { return; } @@ -2622,22 +2762,56 @@ macro_rules! doctest_helper_setup_doc_env { 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(), + let node_client = HTTPNodeClient::new( + &wallet_config.check_node_api_http_addr, + None, + wallet_config.api_request_timeout(), ) - as Box< - dyn WalletInst< - 'static, - DefaultLCProvider, - HTTPNodeClient, - ExtKeychain, - >, - >; + .unwrap(); + let mut wallet = + Box::new(DefaultWalletImpl::::new(node_client.clone()).unwrap()) + as Box< + dyn WalletInst< + 'static, + DefaultLCProvider, + HTTPNodeClient, + ExtKeychain, + >, + >; let lc = wallet.lc_provider().unwrap(); let _ = lc.set_top_level_directory(&wallet_config.data_file_dir); lc.open_wallet(None, pw, false, false); let mut $wallet = Arc::new(Mutex::new(wallet)); }; } + +/// Output slatepack message to file. +fn output_slatepack_file( + api: &Owner, + keychain_mask: Option<&SecretKey>, + slate: &Slate, + dest: Option, +) -> Result<(), Error> +where + L: WalletLCProvider<'static, C, K> + 'static, + C: NodeClient + 'static, + K: Keychain + 'static, +{ + // encrypt for recipient by default + let recipients = match dest { + Some(a) => vec![a], + None => vec![], + }; + let message = api.create_slatepack_message(keychain_mask, &slate, Some(0), recipients)?; + let tld = api.get_top_level_directory()?; + + // Create a directory to which files will be output. + let slate_dir = format!("{}/{}", tld, "slatepack"); + let _ = std::fs::create_dir_all(slate_dir.clone()); + let out_file_name = format!("{}/{}.{}.slatepack", slate_dir, slate.id, slate.state); + + let mut output = File::create(out_file_name.clone())?; + output.write_all(&message.as_bytes())?; + output.sync_all()?; + Ok(()) +} diff --git a/api/src/owner_rpc.rs b/api/src/owner_rpc.rs index c983c20c8..6bf9d8dba 100644 --- a/api/src/owner_rpc.rs +++ b/api/src/owner_rpc.rs @@ -21,6 +21,9 @@ use crate::config::{TorConfig, WalletConfig}; use crate::core::core::OutputFeatures; use crate::core::global; use crate::keychain::{Identifier, Keychain}; +use crate::libwallet::contract::types::{ + ContractNewArgsAPI, ContractRevokeArgsAPI, ContractSetupArgsAPI, ContractView, +}; use crate::libwallet::{ mwixnet::MixnetReqCreationParams, AcctPathMapping, Amount, BuiltOutput, Error, InitTxArgs, IssueInvoiceTxArgs, NodeClient, NodeHeightResult, OutputCommitMapping, PaymentProof, Slate, @@ -36,6 +39,7 @@ use easy_jsonrpc_mw; use grin_wallet_util::OnionV3Address; use rand::thread_rng; use std::convert::TryFrom; +use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; @@ -272,6 +276,7 @@ pub trait OwnerRpc { "stored_tx": null, "ttl_cutoff_height": null, "tx_slate_id": null, + "tx_slate_state": null, "payment_proof": null, "reverted_after": null, "tx_type": "ConfirmedCoinbase" @@ -294,6 +299,7 @@ pub trait OwnerRpc { "payment_proof": null, "reverted_after": null, "tx_slate_id": null, + "tx_slate_state": null, "tx_type": "ConfirmedCoinbase" } ] @@ -363,6 +369,7 @@ pub trait OwnerRpc { "stored_tx": null, "ttl_cutoff_height": null, "tx_slate_id": null, + "tx_slate_state": null, "payment_proof": null, "reverted_after": null, "tx_type": "ConfirmedCoinbase" @@ -385,6 +392,7 @@ pub trait OwnerRpc { "payment_proof": null, "reverted_after": null, "tx_slate_id": null, + "tx_slate_state": null, "tx_type": "ConfirmedCoinbase" } ] @@ -512,8 +520,45 @@ pub trait OwnerRpc { ``` */ + /** + TODO: Full docs once API has stabilised + */ fn init_send_tx(&self, token: Token, args: InitTxArgs) -> Result; + /** + TODO: Full docs once API has stabilised + */ + + fn contract_new(&self, token: Token, args: ContractNewArgsAPI) + -> Result; + + /** + TODO: Full docs once API has stabilised + */ + + fn contract_sign( + &self, + token: Token, + slate: VersionedSlate, + args: ContractSetupArgsAPI, + ) -> Result; + + /** + TODO: Full docs once API has stabilised + */ + + fn contract_view(&self, token: Token, slate: VersionedSlate) -> Result; + + /** + TODO: Full docs once API has stabilised + */ + + fn contract_revoke( + &self, + token: Token, + args: ContractRevokeArgsAPI, + ) -> Result, Error>; + /** ;Networked version of [Owner::issue_invoice_tx](struct.Owner.html#method.issue_invoice_tx). @@ -1203,6 +1248,9 @@ pub trait OwnerRpc { /** Networked version of [Owner::set_top_level_directory](struct.Owner.html#method.set_top_level_directory). + + The wallet must be closed and the updater stopped before calling this method. Running listeners + should be restarted after opening the wallet from the new directory. ``` # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( # r#" @@ -1713,7 +1761,7 @@ pub trait OwnerRpc { "params": { "token": "d202964900000000d302964900000000d402964900000000d502964900000000", "secret_indices": [0], - "message": "BEGINSLATEPACK. 8GQrdcwdLKJD28F 3a9siP7ZhZgAh7w BR2EiZHza5WMWmZ Cc8zBUemrrYRjhq j3VBwA8vYnvXXKU BDmQBN2yKgmR8mX UzvXHezfznA61d7 qFZYChhz94vd8Ew NEPLz7jmcVN2C3w wrfHbeiLubYozP2 uhLouFiYRrbe3fQ 4uhWGfT3sQYXScT dAeo29EaZJpfauh j8VL5jsxST2SPHq nzXFC2w9yYVjt7D ju7GSgHEp5aHz9R xstGbHjbsb4JQod kYLuELta1ohUwDD pvjhyJmsbLcsPei k5AQhZsJ8RJGBtY bou6cU7tZeFJvor 4LB9CBfFB3pmVWD vSLd5RPS75dcnHP nbXD8mSDZ8hJS2Q A9wgvppWzuWztJ2 dLUU8f9tLJgsRBw YZAs71HiVeg7. ENDSLATEPACK." + "message": "BEGINSLATEPACK. t9EcGgrKr1GFCQB SK2jPCxME6Hgpqx bntpQm3zKFycoPY nW4UeoL4KQ7ExNK At6EQsvpz6MjUs8 6WG8KHEbMfqufJQ ZJTw2gkcdJmJjiJ f29oGgYqqXDZox4 ujPSjrtoxCN4h3e i1sZ8dYsm3dPeXL 7VQLsYNjAefciqj ZJXPm4Pqd7VDdd4 okGBGBu3YJvYzT6 arAxeCEx66us31h AJLcDweFwyWBkW5 J1DLiYAjt5ftFTo CjpfW9KjiLq2LM5 jepXWEHJPSDAYVK 4macDZUhRbJiG6E hrQcPrJBVC716mb Hw5E1PFrE6on5wq oEmrS4j9vaB5nw8 Z9ZyXvPc2LN7tER yt6pSHZeY9EpYdY zv4bthzfRfF8ePT TMeMpV2gpgyRXQa CPD2TR. ENDSLATEPACK." }, "id": 1 } @@ -1857,9 +1905,9 @@ pub trait OwnerRpc { "proof": { "amount": "60000000000", "excess": "09eac5f5872fa5e08e0c29fd900f1b8f77ff3ad1d0d1c46aeb202cbf92363fe0af", - "recipient_address": "slatepack10qlk22rxjap2ny8qltc2tl996kenxr3hhwuu6hrzs6tdq08yaqgqnlumr7", + "recipient_address": "tgrin10qlk22rxjap2ny8qltc2tl996kenxr3hhwuu6hrzs6tdq08yaqgqq6t83r", "recipient_sig": "02868f2d2b983981f8f98043701687a8531ed2de564ea3df48e9e7e0229ccbe8359efe506896df2efbe3528e977252c50e4a41ca3cc9896e7c5a30bbb1d33604", - "sender_address": "slatepack1xtxavwfgs48ckf3gk8wwgcndmn0nt4tvkl8a7ltyejjcy2mc6nfskdvkdu", + "sender_address": "tgrin1xtxavwfgs48ckf3gk8wwgcndmn0nt4tvkl8a7ltyejjcy2mc6nfs9gm2lp", "sender_sig": "c511764f3f61ed3d1cbca9514df8bc6811fad5662b1cb0e0587b9c9e49db9f33183cce71af6cb24b507fabf525a2bc405c6e84e63a60334edff0b451ae5e6102" } }, @@ -2111,6 +2159,58 @@ where VersionedSlate::into_version(slate, version) } + fn contract_new( + &self, + token: Token, + args: ContractNewArgsAPI, + ) -> Result { + let slate = Owner::contract_new(self, (&token.keychain_mask).as_ref(), &args)?; + // An early payment proof needs V5: its promise signature binds the timestamp and + // memo, which a V4 slate cannot carry. + let version = SlateVersion::lowest_for(&slate); + VersionedSlate::into_version(slate, version) + } + + fn contract_sign( + &self, + token: Token, + in_slate: VersionedSlate, + args: ContractSetupArgsAPI, + ) -> Result { + let in_version = in_slate.version(); + let slate = Owner::contract_sign( + self, + (&token.keychain_mask).as_ref(), + &Slate::from(in_slate), + &args, + )?; + // Keep the version the counterparty sent us, since they can clearly read it, but + // never below what the slate needs to represent its payment proof. + let version = match SlateVersion::lowest_for(&slate) { + SlateVersion::V5 => SlateVersion::V5, + SlateVersion::V4 => in_version, + }; + VersionedSlate::into_version(slate, version) + } + + fn contract_view(&self, token: Token, slate: VersionedSlate) -> Result { + Owner::contract_view(self, (&token.keychain_mask).as_ref(), &Slate::from(slate)) + } + + fn contract_revoke( + &self, + token: Token, + args: ContractRevokeArgsAPI, + ) -> Result, Error> { + let slate_opt = Owner::contract_revoke(self, (&token.keychain_mask).as_ref(), &args)?; + // We return a slate only when we had to perform a self-spend safe cancel + if let Some(slate) = slate_opt { + let version = SlateVersion::lowest_for(&slate); + return Ok(Some(VersionedSlate::into_version(slate, version)?)); + } + Ok(None) + } + fn issue_invoice_tx( &self, token: Token, @@ -2137,6 +2237,14 @@ where VersionedSlate::into_version(out_slate, version) } + fn tx_lock_outputs(&self, token: Token, in_slate: VersionedSlate) -> Result<(), Error> { + Owner::tx_lock_outputs( + self, + (&token.keychain_mask).as_ref(), + &Slate::from(in_slate), + ) + } + fn finalize_tx(&self, token: Token, in_slate: VersionedSlate) -> Result { let out_slate = Owner::finalize_tx( self, @@ -2147,11 +2255,12 @@ where VersionedSlate::into_version(out_slate, version) } - fn tx_lock_outputs(&self, token: Token, in_slate: VersionedSlate) -> Result<(), Error> { - Owner::tx_lock_outputs( + fn post_tx(&self, token: Token, slate: VersionedSlate, fluff: bool) -> Result<(), Error> { + Owner::post_tx( self, (&token.keychain_mask).as_ref(), - &Slate::from(in_slate), + &Slate::from(slate), + fluff, ) } @@ -2185,15 +2294,6 @@ where } } - fn post_tx(&self, token: Token, slate: VersionedSlate, fluff: bool) -> Result<(), Error> { - Owner::post_tx( - self, - (&token.keychain_mask).as_ref(), - &Slate::from(slate), - fluff, - ) - } - fn get_rewind_hash(&self, token: Token) -> Result { Owner::get_rewind_hash(self, (&token.keychain_mask).as_ref()) } @@ -2419,7 +2519,7 @@ where } fn set_tor_config(&self, tor_config: Option) -> Result<(), Error> { - Owner::set_tor_config(self, tor_config); + Owner::set_tor_config(self, tor_config)?; Ok(()) } @@ -2483,6 +2583,7 @@ pub fn run_doctest_owner( ) -> Result, String> { use easy_jsonrpc_mw::Handler; use grin_keychain::ExtKeychain; + use grin_wallet_config::initial_setup_wallet; use grin_wallet_impls::test_framework::{self, LocalWalletClient, WalletProxy}; use grin_wallet_impls::{DefaultLCProvider, DefaultWalletImpl}; use grin_wallet_libwallet::{api_impl, WalletInst}; @@ -2496,6 +2597,14 @@ pub fn run_doctest_owner( let _ = fs::remove_dir_all(test_dir); global::set_local_chain_type(ChainTypes::AutomatedTesting); + let _ = fs::create_dir_all(test_dir); + let config = initial_setup_wallet( + &ChainTypes::AutomatedTesting, + Some(PathBuf::from(test_dir)), + false, + ) + .unwrap(); + let mut wallet_proxy: WalletProxy< DefaultLCProvider, LocalWalletClient, @@ -2503,11 +2612,11 @@ pub fn run_doctest_owner( > = WalletProxy::new(test_dir); let chain = wallet_proxy.chain.clone(); - let rec_phrase_1 = util::ZeroingString::from( + let rec_phrase_1 = 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 empty_string = ZeroingString::from(""); let client1 = LocalWalletClient::new("wallet1", wallet_proxy.tx.clone()); let mut wallet1 = @@ -2542,7 +2651,7 @@ pub fn run_doctest_owner( let mut slate_outer = Slate::blank(2, false); - let rec_phrase_2 = util::ZeroingString::from( + let rec_phrase_2 = 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", ); @@ -2611,7 +2720,8 @@ pub fn run_doctest_owner( true => { let address = "783f6528669742a990e0faf0a5fca5d5b3330e37bbb9cd5c628696d03ce4e810"; let address = OnionV3Address::try_from(address).unwrap(); - Some(SlatepackAddress::try_from(address).unwrap()) + let sp = SlatepackAddress::try_from(address).unwrap(); + Some(sp) } false => None, }; @@ -2625,26 +2735,25 @@ pub fn run_doctest_owner( payment_proof_recipient_address: proof_address, ..Default::default() }; - let mut slate = - api_impl::owner::init_send_tx(&mut **w, (&mask1).as_ref(), args, true).unwrap(); + let mut slate = api_impl::owner::init_send_tx(w, (&mask1).as_ref(), args, true).unwrap(); println!("INITIAL SLATE"); println!("{}", serde_json::to_string_pretty(&slate).unwrap()); { let mut w_lock = wallet2.lock(); let w2 = w_lock.lc_provider().unwrap().wallet_inst().unwrap(); - slate = api_impl::foreign::receive_tx(&mut **w2, (&mask2).as_ref(), &slate, None, true) - .unwrap(); + slate = + api_impl::foreign::receive_tx(w2, (&mask2).as_ref(), &slate, None, true).unwrap(); w2.close().unwrap(); } // Spit out slate for input to finalize_tx if lock_tx { println!("LOCKING TX"); - api_impl::owner::tx_lock_outputs(&mut **w, (&mask1).as_ref(), &slate).unwrap(); + api_impl::owner::tx_lock_outputs(w, (&mask1).as_ref(), &slate).unwrap(); } println!("RECEIPIENT SLATE"); println!("{}", serde_json::to_string_pretty(&slate).unwrap()); if finalize_tx { - slate = api_impl::owner::finalize_tx(&mut **w, (&mask1).as_ref(), &slate).unwrap(); + slate = api_impl::owner::finalize_tx(w, (&mask1).as_ref(), &slate).unwrap(); error!("FINALIZED TX SLATE"); println!("{}", serde_json::to_string_pretty(&slate).unwrap()); } @@ -2666,8 +2775,11 @@ pub fn run_doctest_owner( ); } - let mut api_owner = Owner::new(wallet1, None); + let mut api_owner = Owner::new(wallet1, None, config.config_file_path); api_owner.doctest_mode = true; + if request["method"] == "set_top_level_directory" { + api_owner.close_wallet(None).unwrap(); + } let owner_api = &api_owner as &dyn OwnerRpc; let res = owner_api.handle_request(request).as_option(); let _ = fs::remove_dir_all(test_dir); diff --git a/api/src/tor_config.rs b/api/src/tor_config.rs new file mode 100644 index 000000000..3ef7a2ff4 --- /dev/null +++ b/api/src/tor_config.rs @@ -0,0 +1,45 @@ +// Copyright 2026 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::config::TorConfig; +use crate::libwallet::Error; +use crate::util::Mutex; +use grin_wallet_config::config::get_global_config; +use std::path::{Path, PathBuf}; +use std::sync::Arc; + +/// Shared path to the active wallet configuration. +#[doc(hidden)] +#[derive(Clone)] +pub struct ConfigPath(Arc>); + +impl ConfigPath { + pub(crate) fn get(&self) -> PathBuf { + self.0.lock().clone() + } + + pub(crate) fn set(&self, path: PathBuf) { + *self.0.lock() = path; + } +} + +impl From for ConfigPath { + fn from(path: PathBuf) -> Self { + Self(Arc::new(Mutex::new(path))) + } +} + +pub(crate) fn load(config_path: &Path) -> Result { + Ok(get_global_config(config_path)?.tor_config()) +} diff --git a/api/src/types.rs b/api/src/types.rs index 4ac88a36d..797901a79 100644 --- a/api/src/types.rs +++ b/api/src/types.rs @@ -16,7 +16,7 @@ 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 ed25519_dalek::SigningKey as DalekSecretKey; use base64; use rand::{thread_rng, Rng}; diff --git a/config/Cargo.toml b/config/Cargo.toml index 18bd9cb49..a50f1e4e9 100644 --- a/config/Cargo.toml +++ b/config/Cargo.toml @@ -14,31 +14,17 @@ rand = "0.6" serde = "1" serde_derive = "1" toml = "0.5" +toml_edit = "0.22" dirs = "2.0" - -grin_wallet_util = { path = "../util", version = "5.4.1" } +lazy_static = "1" ##### Grin Imports -# For Release -grin_core = "5.4.1" -grin_util = "5.4.1" - -# 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"} +grin_core = { path = "../grin/core"} +grin_util = { path = "../grin/util"} ##### [dev-dependencies] -pretty_assertions = "0.6" +tempfile = "3.1" diff --git a/config/src/comments.rs b/config/src/comments.rs index c027042a9..a303c62c5 100644 --- a/config/src/comments.rs +++ b/config/src/comments.rs @@ -70,6 +70,13 @@ fn comments() -> HashMap { "check_node_api_http_addr".to_string(), " #where the wallet should find a running node +" + .to_string(), + ); + retval.insert( + "node_api_request_timeout_secs".to_string(), + " +#total timeout for requests to node API " .to_string(), ); @@ -206,16 +213,24 @@ fn comments() -> HashMap { "[tor]".to_string(), " ######################################### -### TOR CONFIGURATION (Experimental) ### +### TOR CONFIGURATION ### ######################################### " .to_string(), ); + retval.insert( + "use_integrated".to_string(), + " +#Whether to use integrated Tor library +" + .to_string(), + ); + retval.insert( "skip_send_attempt".to_string(), " -#Whether to skip send attempts (used for debugging) +#Whether to skip send attempts (default false) " .to_string(), ); @@ -244,6 +259,22 @@ fn comments() -> HashMap { .to_string(), ); + retval.insert( + "request_timeout_secs".to_string(), + " +#Tor request timeout in seconds +" + .to_string(), + ); + + retval.insert( + "bootstrap_timeout_secs".to_string(), + " +#Tor bootstrap timeout in seconds +" + .to_string(), + ); + retval.insert( "[tor.bridge]".to_string(), " @@ -259,8 +290,11 @@ fn comments() -> HashMap { " #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. +#Support of the transport: webtunnel, obfs4, and snowflake. +#webtunnel, obfs4proxy or snowflake client binary must be installed and on your path. +#Custom path for client binary +#bridge_bin_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 = \"\" @@ -279,10 +313,10 @@ fn comments() -> HashMap { } fn get_key(line: &str) -> String { - if line.contains('[') && line.contains(']') { + if line.starts_with('[') && line.ends_with(']') { line.to_owned() - } else if line.contains('=') { - line.split('=').collect::>()[0].trim().to_owned() + } else if let Some((key, _)) = line.split_once('=') { + key.trim().to_owned() } else { "NOT_FOUND".to_owned() } @@ -306,91 +340,3 @@ pub fn insert_comments(orig: String) -> String { } ret_val } - -pub fn migrate_comments( - old_config: String, - new_config: String, - old_version: Option, -) -> String { - let comments = comments(); - // Prohibe the key we are basing on to introduce new comments for [tor.proxy] - let prohibited_key = match old_version { - None => vec!["[logging]"], - Some(_) => vec![], - }; - let mut vec_old_conf = vec![]; - let mut hm_key_cmt_old = HashMap::new(); - let old_conf: Vec<&str> = old_config.split_inclusive('\n').collect(); - // collect old key in a vec and insert old key/comments from the old conf in a hashmap - let vec_key_old = old_conf - .iter() - .filter_map(|line| { - let line_nospace = line.trim(); - let is_ascii_control = line_nospace.chars().all(|x| x.is_ascii_control()); - match line.contains("#") || is_ascii_control { - true => { - vec_old_conf.push(line.to_owned()); - None - } - false => { - let comments: String = vec_old_conf.iter().flat_map(|s| s.chars()).collect(); - let key = get_key(line_nospace); - match key != "NOT_FOUND" { - true => { - vec_old_conf.clear(); - hm_key_cmt_old.insert(key.clone(), comments); - Some(key) - } - false => None, - } - } - } - }) - .collect::>(); - - let new_conf: Vec<&str> = new_config.split_inclusive('\n').collect(); - // collect new key and the whole key line from the new config - let vec_key_cmt_new = new_conf - .iter() - .filter_map(|line| { - let line_nospace = line.trim(); - let is_ascii_control = line_nospace.chars().all(|x| x.is_ascii_control()); - match !line.contains("#") && !is_ascii_control { - true => { - let key = get_key(line_nospace); - match key != "NOT_FOUND" { - true => Some((key, line_nospace.to_string())), - false => None, - } - } - false => None, - } - }) - .collect::>(); - - let mut new_config_str = String::from(""); - // Merging old comments in the new config (except if the key is contained in the prohibited vec) with all new introduced key comments - for (key, key_line) in vec_key_cmt_new { - let old_key_exist = vec_key_old.iter().any(|old_key| *old_key == key); - let key_fmt = format!("{}\n", key_line); - if old_key_exist { - if prohibited_key.contains(&key.as_str()) { - // push new config key/comments - let value = comments.get(&key).unwrap(); - new_config_str.push_str(value); - new_config_str.push_str(&key_fmt); - } else { - // push old config key/comment - let value = hm_key_cmt_old.get(&key).unwrap(); - new_config_str.push_str(value); - new_config_str.push_str(&key_fmt); - } - } else { - // old key does not exist, we push new key/comments - let value = comments.get(&key).unwrap(); - new_config_str.push_str(value); - new_config_str.push_str(&key_fmt); - } - } - new_config_str -} diff --git a/config/src/config.rs b/config/src/config.rs index 2c37ae729..c32a5bbdd 100644 --- a/config/src/config.rs +++ b/config/src/config.rs @@ -14,24 +14,57 @@ //! Configuration file management -use crate::comments::{insert_comments, migrate_comments}; +use crate::comments::insert_comments; use crate::core::global; use crate::types::{ ConfigError, GlobalWalletConfig, GlobalWalletConfigMembers, TorBridgeConfig, TorProxyConfig, }; use crate::types::{TorConfig, WalletConfig}; use crate::util::logger::LoggingConfig; +use crate::util::{Mutex, RwLock}; + +use lazy_static::lazy_static; use rand::distributions::{Alphanumeric, Distribution}; -use rand::thread_rng; +use rand::{thread_rng, Rng}; +use std::collections::hash_map::Entry; +use std::collections::HashMap; use std::env; -use std::fs::{self, File}; +use std::fs::{self, File, OpenOptions}; use std::io::prelude::*; use std::io::BufReader; use std::path::{Path, PathBuf}; +use std::sync::mpsc::Sender; +use std::sync::Arc; use toml; +use toml_edit::{DocumentMut, Table}; + +type ConfigListeners = HashMap>; +type ConfigRegistry = HashMap; + +lazy_static! { + /// Global configuration instances and change listeners mapped to config path. + static ref CONFIG_INSTANCES: Arc> = + Arc::new(RwLock::new(HashMap::new())); + static ref CONFIG_SAVE_LOCK: Mutex<()> = Mutex::new(()); +} + +fn cache_loaded_config( + configs: &mut ConfigRegistry, + config_path: &Path, + config: GlobalWalletConfig, +) -> GlobalWalletConfig { + match configs.entry(config_path.to_path_buf()) { + Entry::Vacant(entry) => { + entry.insert((config.clone(), HashMap::new())); + config + } + Entry::Occupied(entry) => entry.get().0.clone(), + } +} /// Wallet configuration file name pub const WALLET_CONFIG_FILE_NAME: &str = "grin-wallet.toml"; +/// Wallet logging file name const WALLET_LOG_FILE_NAME: &str = "grin-wallet.log"; /// .grin folder, usually in home/.grin pub const GRIN_HOME: &str = ".grin"; @@ -42,6 +75,91 @@ pub const API_SECRET_FILE_NAME: &str = ".foreign_api_secret"; /// Owner API secret pub const OWNER_API_SECRET_FILE_NAME: &str = ".owner_api_secret"; +fn set_global_config(config: GlobalWalletConfig) { + let mut configs = CONFIG_INSTANCES.write(); + let mut listeners = if let Some((_, l)) = configs.get(&config.config_file_path) { + l.clone() + } else { + HashMap::new() + }; + // Update config. + configs.insert( + config.config_file_path.clone(), + (config.clone(), listeners.clone()), + ); + // Notify listeners. + let mut failed = vec![]; + for l in listeners.clone() { + match l.1.send(()) { + Ok(_) => {} + Err(_) => { + failed.push(l.0.to_string()); + } + } + } + for f in &failed { + listeners.remove(f); + } + // Update listener list. + if !failed.is_empty() { + configs.insert(config.config_file_path.clone(), (config, listeners)); + } +} + +/// Reload configuration from disk and update the global instance. +pub fn reload_global_config(config_path: &Path) -> Result { + let _save_lock = CONFIG_SAVE_LOCK.lock(); + let config = GlobalWalletConfig::new(config_path.to_path_buf())?; + set_global_config(config.clone()); + Ok(config) +} + +/// Get global configuration using provided path. +pub fn get_global_config(config_path: &Path) -> Result { + { + let configs = CONFIG_INSTANCES.read(); + if let Some((config, _)) = configs.get(config_path) { + return Ok(config.clone()); + } + } + let config = GlobalWalletConfig::new(config_path.to_path_buf())?; + let mut configs = CONFIG_INSTANCES.write(); + Ok(cache_loaded_config(&mut configs, config_path, config)) +} + +/// Load, update and save a configuration as one operation. +pub fn update_global_config(config_path: &Path, update: F) -> Result<(), ConfigError> +where + F: FnOnce(&mut GlobalWalletConfig) -> Result<(), ConfigError>, +{ + let _save_lock = CONFIG_SAVE_LOCK.lock(); + let mut config = GlobalWalletConfig::new(config_path.to_path_buf())?; + update(&mut config)?; + config.save_locked() +} + +/// Add listener on config change. +pub fn add_global_config_listener(config_path: &PathBuf, listener_id: &str, tx: Sender<()>) { + let mut w_l = CONFIG_INSTANCES.write(); + match w_l.get_mut(config_path) { + None => {} + Some((_, listeners)) => { + listeners.insert(listener_id.to_string(), tx); + } + } +} + +/// Remove listener on config change. +pub fn remove_global_config_listener(config_path: &PathBuf, listener_id: &str) { + let mut w_l = CONFIG_INSTANCES.write(); + match w_l.get_mut(config_path) { + None => {} + Some((_, listeners)) => { + listeners.remove(listener_id); + } + } +} + /// Function to locate the wallet dir and grin-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 @@ -50,7 +168,7 @@ pub fn get_wallet_path( create_path: bool, ) -> Result { // A - Detect grin-wallet.toml in working dir - let mut config_path = std::env::current_dir()?; + let mut config_path = env::current_dir()?; config_path.push(WALLET_CONFIG_FILE_NAME); if create_path == false && config_path.exists() { config_path.pop(); @@ -58,10 +176,7 @@ pub fn get_wallet_path( return Ok(config_path); }; // B - Select home directory - let mut wallet_path = match dirs::home_dir() { - Some(p) => p, - None => PathBuf::new(), - }; + let mut wallet_path = dirs::home_dir().unwrap_or_else(|| PathBuf::new()); wallet_path.push(GRIN_HOME); wallet_path.push(chain_type.shortname()); // Create if the default path doesn't exist @@ -96,10 +211,7 @@ pub fn get_node_path( 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(), - }; + let mut node_path = dirs::home_dir().unwrap_or_else(|| PathBuf::new()); node_path.push(GRIN_HOME); node_path.push(chain_type.shortname()); Ok(node_path) @@ -107,10 +219,7 @@ pub fn get_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(), - }; + let mut node_path = dirs::home_dir().unwrap_or_else(|| PathBuf::new()); node_path.push(GRIN_HOME); node_path.push(chain_type.shortname()); Ok(node_path) @@ -187,8 +296,11 @@ fn check_api_secret_file( } /// 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 +/// 1) Load wallet config if run without 'init' 2) create wallet if run with 'init' +/// Try in this 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 @@ -214,8 +326,7 @@ pub fn initial_setup_wallet( } else { env::current_dir()?.join(&fixed_path).canonicalize()? }; - let absolute_path = - std::path::PathBuf::from(absolute_path.to_str().unwrap().replace(r"\\?\", "")); + let absolute_path = PathBuf::from(absolute_path.to_str().unwrap().replace(r"\\?\", "")); data_path = Some(absolute_path); // Store the updated path } } @@ -234,12 +345,11 @@ pub fn initial_setup_wallet( 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 + // Check if a config exists in the working 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()); + let mut default_config = GlobalWalletConfig::for_chain(chain_type, &config_path); default_config.update_paths(&wallet_path, &node_path); // Write config file @@ -260,7 +370,7 @@ pub fn initial_setup_wallet( // Return config if not run with init true => { - // If run with init and seed does not yet exists, continue, else throw error + // If run with init and seed do 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", @@ -269,12 +379,15 @@ pub fn initial_setup_wallet( ); return Err(ConfigError::SerializationError(msg)); } else { - let config = GlobalWalletConfig::new(config_path.to_str().unwrap())?; + let config = GlobalWalletConfig::new(config_path)?; (wallet_path, config) } } }; + // Set global config instance. + set_global_config(config.clone()); + // 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)?; @@ -293,21 +406,15 @@ impl Default for GlobalWalletConfigMembers { } } -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; + pub fn for_chain(chain_type: &global::ChainTypes, file_path: &PathBuf) -> GlobalWalletConfig { + let mut defaults_conf = GlobalWalletConfig { + config_file_path: file_path.clone(), + members: GlobalWalletConfigMembers::default(), + }; + let defaults = &mut defaults_conf.members.wallet; defaults.chain_type = Some(*chain_type); match *chain_type { @@ -324,17 +431,29 @@ impl GlobalWalletConfig { } defaults_conf } + + /// Return the configured Tor settings, resolving the legacy default when the + /// config file does not yet contain a `[tor]` section. + pub fn tor_config(&self) -> TorConfig { + self.members.tor.clone().unwrap_or_else(|| TorConfig { + send_config_dir: self.members.wallet.data_file_dir.clone(), + ..TorConfig::default() + }) + } + /// Requires the path to a config file - pub fn new(file_path: &str) -> Result { - let mut return_value = GlobalWalletConfig::default(); - return_value.config_file_path = Some(PathBuf::from(&file_path)); + pub fn new(config_file_path: PathBuf) -> Result { + let return_value = GlobalWalletConfig { + config_file_path, + members: GlobalWalletConfigMembers::default(), + }; // Config file path is given but not valid - let config_file = return_value.config_file_path.clone().unwrap(); + let config_file = &return_value.config_file_path; if !config_file.exists() { - return Err(ConfigError::FileNotFoundError(String::from( - config_file.to_str().unwrap(), - ))); + return Err(ConfigError::FileNotFoundError( + config_file.display().to_string(), + )); } // Try to parse the config file if it exists, explode if it does exist but @@ -344,7 +463,7 @@ impl GlobalWalletConfig { /// Read config fn read_config(mut self) -> Result { - let config_file_path = self.config_file_path.as_mut().unwrap(); + let config_file_path = &self.config_file_path; let contents = fs::read_to_string(config_file_path.clone())?; let migrated = GlobalWalletConfig::migrate_config_file_version_none_to_2( contents, @@ -354,11 +473,11 @@ impl GlobalWalletConfig { let decoded: Result = toml::from_str(&fixed); match decoded { Ok(gc) => { - self.members = Some(gc); + self.members = gc; Ok(self) } Err(e) => Err(ConfigError::ParseError( - String::from(self.config_file_path.as_mut().unwrap().to_str().unwrap()), + self.config_file_path.display().to_string(), format!("{}", e), )), } @@ -375,32 +494,18 @@ impl GlobalWalletConfig { 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 = + self.members.wallet.data_file_dir = data_file_dir.to_str().unwrap().to_owned(); + self.members.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(); + self.members.wallet.api_secret_path = Some(secret_path.to_str().unwrap().to_owned()); + self.members.logging.as_mut().unwrap().log_file_path = + log_path.to_str().unwrap().to_owned(); + self.members.tor.as_mut().unwrap().send_config_dir = tor_path.to_str().unwrap().to_owned(); } /// Serialize config pub fn ser_config(&mut self) -> Result { - let encoded: Result = - toml::to_string(self.members.as_mut().unwrap()); + let encoded: Result = toml::to_string(&self.members); match encoded { Ok(enc) => Ok(enc), Err(e) => Err(ConfigError::SerializationError(format!("{}", e))), @@ -415,17 +520,111 @@ impl GlobalWalletConfig { old_config: Option, old_version: Option, ) -> Result<(), ConfigError> { - let conf_out = self.ser_config()?; + self.write_to_path(Path::new(name), migration, old_config, old_version) + } + + fn write_to_path( + &mut self, + name: &Path, + migration: bool, + old_config: Option, + old_version: Option, + ) -> Result<(), ConfigError> { + let conf_out = GlobalWalletConfig::fix_log_level(self.ser_config()?); let commented_config = if migration { - migrate_comments(old_config.unwrap(), conf_out, old_version) + let old_config = old_config.unwrap(); + let new_config = insert_comments(conf_out); + GlobalWalletConfig::merge_config(&old_config, &new_config, old_version)? } else { - let fixed_config = GlobalWalletConfig::fix_log_level(conf_out); - insert_comments(fixed_config) + insert_comments(conf_out) }; let mut file = File::create(name)?; file.write_all(commented_config.as_bytes())?; Ok(()) } + + fn merge_config( + old_config: &str, + new_config: &str, + old_version: Option, + ) -> Result { + fn update( + current: &mut Table, + new: &Table, + known: Option<&Table>, + path: &str, + replace_logging_comments: bool, + ) { + if let Some(known) = known { + let removed = known + .iter() + .filter(|(key, _)| !new.contains_key(key)) + .map(|(key, _)| key.to_owned()) + .collect::>(); + for key in removed { + current.remove(&key); + } + } + + for (key, new_item) in new { + let item_path = if path.is_empty() { + key.to_owned() + } else { + format!("{}.{}", path, key) + }; + if let (Some(current_table), Some(new_table)) = ( + current.get_mut(key).and_then(|item| item.as_table_mut()), + new_item.as_table(), + ) { + if replace_logging_comments && item_path == "logging" { + *current_table.decor_mut() = new_table.decor().clone(); + } + update( + current_table, + new_table, + known + .and_then(|table| table.get(key)) + .and_then(|item| item.as_table()), + &item_path, + replace_logging_comments, + ); + } else if let Some(current_item) = current.get_mut(key) { + let mut replacement = new_item.clone(); + if let (Some(current_value), Some(new_value)) = + (current_item.as_value(), replacement.as_value_mut()) + { + *new_value.decor_mut() = current_value.decor().clone(); + } + *current_item = replacement; + } else { + current.insert_formatted(new.key(key).unwrap(), new_item.clone()); + } + } + } + + let mut current = old_config + .parse::() + .map_err(|e| ConfigError::SerializationError(format!("{}", e)))?; + let new = new_config + .parse::() + .map_err(|e| ConfigError::SerializationError(format!("{}", e)))?; + let known_members: GlobalWalletConfigMembers = toml::from_str( + &GlobalWalletConfig::fix_warning_level(old_config.to_owned()), + ) + .map_err(|e| ConfigError::SerializationError(format!("{}", e)))?; + let known = toml::to_string(&known_members) + .map_err(|e| ConfigError::SerializationError(format!("{}", e)))? + .parse::() + .map_err(|e| ConfigError::SerializationError(format!("{}", e)))?; + update( + current.as_table_mut(), + new.as_table(), + Some(known.as_table()), + "", + old_version.is_none(), + ); + Ok(current.to_string()) + } /// This migration does the following: /// - Adds "config_file_version = 2" /// - Introduce new key config_file_version, [tor.bridge] and [tor.proxy] @@ -449,31 +648,265 @@ impl GlobalWalletConfig { ..config }; let mut gc = GlobalWalletConfig { - members: Some(adjusted_config), - config_file_path: Some(config_file_path.clone()), + members: adjusted_config, + config_file_path: config_file_path.clone(), }; - let str_path = config_file_path.into_os_string().into_string().unwrap(); - gc.write_to_file( - &str_path, + gc.write_to_path( + &config_file_path, true, Some(config_str), config.config_file_version, )?; - let adjusted_config_str = fs::read_to_string(str_path.clone())?; + let adjusted_config_str = fs::read_to_string(config_file_path)?; 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") + GlobalWalletConfig::replace_log_levels(conf, &[("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") + GlobalWalletConfig::replace_log_levels( + conf, + &[ + ("TRACE", "Trace"), + ("DEBUG", "Debug"), + ("INFO", "Info"), + ("WARN", "Warning"), + ("ERROR", "Error"), + ], + ) + } + + fn replace_log_levels(conf: String, replacements: &[(&str, &str)]) -> String { + conf.split_inclusive('\n') + .map(|line| { + let trimmed = line.trim_start(); + if trimmed.starts_with("stdout_log_level =") + || trimmed.starts_with("file_log_level =") + { + replacements + .iter() + .fold(line.to_owned(), |line, (from, to)| line.replace(from, to)) + } else { + line.to_owned() + } + }) + .collect() + } + + /// Save config to file and update global state after editing. + pub fn save(&mut self) -> Result<(), ConfigError> { + let _save_lock = CONFIG_SAVE_LOCK.lock(); + self.save_locked() + } + + fn save_locked(&mut self) -> Result<(), ConfigError> { + let path = self.config_file_path.clone(); + let mut tmp_name = path.as_os_str().to_os_string(); + tmp_name.push(format!("-{}.tmp", thread_rng().gen::())); + let tmp_path = PathBuf::from(tmp_name); + let contents = fs::read_to_string(&path)?; + let permissions = fs::metadata(&path)?.permissions(); + + let save_result = (|| { + OpenOptions::new() + .write(true) + .create_new(true) + .open(&tmp_path)?; + fs::set_permissions(&tmp_path, permissions)?; + self.write_to_path( + &tmp_path, + true, + Some(contents), + self.members.config_file_version, + )?; + fs::rename(&tmp_path, &path)?; + Ok::<(), ConfigError>(()) + })(); + if save_result.is_err() { + let _ = fs::remove_file(&tmp_path); + } + save_result?; + + set_global_config(self.clone()); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::core::global::ChainTypes; + use std::sync::{Arc, Barrier}; + use std::thread; + use tempfile::tempdir; + + #[test] + fn save_roundtrip() { + let dir = tempdir().unwrap(); + let path = dir.path().join(WALLET_CONFIG_FILE_NAME); + let mut config = GlobalWalletConfig::for_chain(&ChainTypes::AutomatedTesting, &path); + config.members.tor.as_mut().unwrap().proxy.password = Some("secretERROR#[value]".into()); + config.write_to_path(&path, false, None, None).unwrap(); + + let contents = fs::read_to_string(&path) + .unwrap() + .replace( + "api_listen_port = 3415", + "api_listen_port = 3415\n# wallet future\nfuture_setting = \"wallet\"\n# future list\nfuture_values = [\n 1,\n 2,\n]", + ) + .replace( + "use_tor_listener = true", + "# custom key comment\nuse_tor_listener = true # custom inline comment\n# tor future\nfuture_setting = \"tor\"", + ); + fs::write(&path, format!("{contents}\n# custom trailing comment\n")).unwrap(); + let before = fs::read_to_string(&path).unwrap(); + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + fs::set_permissions(&path, fs::Permissions::from_mode(0o600)).unwrap(); + } + + config.members.tor.as_mut().unwrap().use_tor_listener = false; + config.members.tor.as_mut().unwrap().proxy.password = Some("secretERROR#value".into()); + config.save().unwrap(); + + let contents = fs::read_to_string(&path).unwrap(); + assert_eq!( + contents, + before + .replace("use_tor_listener = true", "use_tor_listener = false") + .replace("secretERROR#[value]", "secretERROR#value") + ); + assert!(contents.contains("# custom trailing comment")); + assert!(contents.contains("# custom key comment")); + assert!(contents.contains("use_tor_listener = false # custom inline comment")); + let stored = GlobalWalletConfig::new(path.clone()).unwrap().tor_config(); + assert!(!stored.use_tor_listener); + assert_eq!(stored.proxy.password.as_deref(), Some("secretERROR#value")); + + config.members.tor.as_mut().unwrap().proxy.transport = Some("socks5".into()); + config.members.tor.as_mut().unwrap().bridge.client_option = Some("option [value]".into()); + config.save().unwrap(); + let stored = GlobalWalletConfig::new(path.clone()).unwrap().tor_config(); + assert_eq!(stored.proxy.transport.as_deref(), Some("socks5")); + assert_eq!( + stored.bridge.client_option.as_deref(), + Some("option [value]") + ); + + config.members.tor.as_mut().unwrap().proxy.transport = None; + config.members.tor.as_mut().unwrap().bridge.client_option = None; + config.save().unwrap(); + let stored = GlobalWalletConfig::new(path.clone()).unwrap().tor_config(); + assert_eq!(stored.proxy.transport, None); + assert_eq!(stored.bridge.client_option, None); + + let legacy = + fs::read_to_string(&path) + .unwrap() + .replacen("config_file_version = 2\n", "", 1); + let mut legacy = legacy.parse::().unwrap(); + let tor = legacy["tor"].as_table_mut().unwrap(); + tor.remove("bridge"); + tor.remove("proxy"); + fs::write(&path, legacy.to_string()).unwrap(); + let migrated = GlobalWalletConfig::new(path.clone()).unwrap(); + assert_eq!(migrated.members.config_file_version, Some(2)); + let migrated = fs::read_to_string(&path).unwrap(); + assert!(migrated.contains("# future list\nfuture_values = [\n 1,\n 2,\n]")); + assert!(migrated.contains("### TOR BRIDGE")); + assert!(migrated.contains("### TOR PROXY")); + assert!(fs::read_dir(dir.path()).unwrap().all(|entry| { + !entry + .unwrap() + .file_name() + .to_string_lossy() + .ends_with(".tmp") + })); + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + assert_eq!( + fs::metadata(&path).unwrap().permissions().mode() & 0o777, + 0o600 + ); + } + } + + #[test] + fn legacy_tor() { + let path = PathBuf::from("legacy-wallet.toml"); + let mut config = GlobalWalletConfig::for_chain(&ChainTypes::AutomatedTesting, &path); + config.members.wallet.data_file_dir = "legacy-wallet-data".into(); + config.members.tor = None; + + let resolved = config.tor_config(); + assert_eq!(resolved.send_config_dir, "legacy-wallet-data"); + assert!(resolved.use_tor_listener); + + let disabled = TorConfig { + use_tor_listener: false, + skip_send_attempt: Some(true), + ..TorConfig::default() + }; + config.members.tor = Some(disabled.clone()); + assert_eq!(config.tor_config(), disabled); + } + + #[test] + fn cached_config_wins() { + let path = PathBuf::from("concurrent-wallet.toml"); + let mut cached = GlobalWalletConfig::for_chain(&ChainTypes::AutomatedTesting, &path); + cached.members.tor.as_mut().unwrap().use_tor_listener = false; + let loaded = GlobalWalletConfig::for_chain(&ChainTypes::AutomatedTesting, &path); + let mut configs = HashMap::new(); + configs.insert(path.clone(), (cached.clone(), HashMap::new())); + + let returned = cache_loaded_config(&mut configs, &path, loaded); + + assert_eq!(returned, cached); + assert_eq!(configs.get(&path).unwrap().0, cached); + } + + #[test] + fn concurrent_update() { + let dir = tempdir().unwrap(); + let path = dir.path().join(WALLET_CONFIG_FILE_NAME); + let mut config = GlobalWalletConfig::for_chain(&ChainTypes::AutomatedTesting, &path); + config.write_to_path(&path, false, None, None).unwrap(); + let barrier = Arc::new(Barrier::new(2)); + + let handles = [false, true].map(|update_tor| { + let path = path.clone(); + let barrier = barrier.clone(); + thread::spawn(move || { + barrier.wait(); + update_global_config(&path, |config| { + if update_tor { + config.members.tor.as_mut().unwrap().socks_proxy_addr = + "127.0.0.1:59051".into(); + } else { + config.members.wallet.api_listen_port = 3416; + } + Ok(()) + }) + .unwrap(); + }) + }); + for handle in handles { + handle.join().unwrap(); + } + + let stored = GlobalWalletConfig::new(path.clone()).unwrap(); + let cached = get_global_config(&path).unwrap(); + assert_eq!(cached, stored); + assert_eq!(stored.members.wallet.api_listen_port, 3416); + assert_eq!(stored.tor_config().socks_proxy_addr, "127.0.0.1:59051"); } } diff --git a/config/src/types.rs b/config/src/types.rs index 6a57c5428..aff6687ad 100644 --- a/config/src/types.rs +++ b/config/src/types.rs @@ -14,12 +14,12 @@ //! Public types for config modules +use crate::core::global::ChainTypes; +use crate::util::logger::LoggingConfig; use std::fmt; use std::io; use std::path::PathBuf; - -use crate::core::global::ChainTypes; -use crate::util::logger::LoggingConfig; +use std::time::Duration; /// Command-line wallet configuration #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] @@ -28,6 +28,9 @@ pub struct WalletConfig { pub chain_type: Option, /// The port this wallet will run on pub api_listen_port: u16, + /// Listen interface for the owner API, should be hidden from config by default + #[serde(skip_serializing)] + pub owner_api_listen_interface: Option, /// The port this wallet's owner API will run on pub owner_api_listen_port: Option, /// Location of the secret for basic auth on the Owner API @@ -37,6 +40,8 @@ pub struct WalletConfig { /// The api address of a running server node against which transaction inputs /// will be checked during send pub check_node_api_http_addr: String, + /// Request timeout for requests to node API. + pub node_api_request_timeout_secs: Option, /// Whether to include foreign API endpoints on the Owner API pub owner_api_include_foreign: Option, /// The directory in which wallet files are stored @@ -61,10 +66,12 @@ impl Default for WalletConfig { WalletConfig { chain_type: Some(ChainTypes::Mainnet), api_listen_port: 3415, + owner_api_listen_interface: Some(WalletConfig::default_owner_api_listen_interface()), 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(), + node_api_request_timeout_secs: Some(Self::NODE_API_REQUEST_TIMEOUT_SECS), owner_api_include_foreign: Some(false), data_file_dir: ".".to_string(), no_commit_cache: Some(false), @@ -77,11 +84,19 @@ impl Default for WalletConfig { } impl WalletConfig { + /// Default Node API request timeout in seconds. + pub const NODE_API_REQUEST_TIMEOUT_SECS: u64 = 60; + /// 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_interface() -> String { + "127.0.0.1".to_string() + } + /// Default listener port pub fn default_owner_api_listen_port() -> u16 { 3420 @@ -92,6 +107,13 @@ impl WalletConfig { 500_000 } + /// Use value from config file, defaulting to sensible value if missing. + pub fn owner_api_listen_interface(&self) -> String { + self.owner_api_listen_interface + .clone() + .unwrap_or_else(|| WalletConfig::default_owner_api_listen_interface()) + } + /// Use value from config file, defaulting to sensible value if missing. pub fn owner_api_listen_port(&self) -> u16 { self.owner_api_listen_port @@ -100,7 +122,11 @@ impl WalletConfig { /// Owner API listen address pub fn owner_api_listen_addr(&self) -> String { - format!("127.0.0.1:{}", self.owner_api_listen_port()) + format!( + "{}:{}", + self.owner_api_listen_interface(), + self.owner_api_listen_port() + ) } /// Accept fee base @@ -108,6 +134,15 @@ impl WalletConfig { self.accept_fee_base .unwrap_or_else(|| WalletConfig::default_accept_fee_base()) } + + /// Node API requests timeout. + pub fn api_request_timeout(&self) -> Duration { + Duration::from_secs( + self.node_api_request_timeout_secs + .filter(|s| *s > 0) + .unwrap_or(Self::NODE_API_REQUEST_TIMEOUT_SECS), + ) + } } /// Error type wrapping config errors. #[derive(Debug)] @@ -162,18 +197,24 @@ impl From for ConfigError { /// Tor configuration #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct TorConfig { - /// whether to skip any attempts to send via TOR + /// whether to use integrated Tor library + pub use_integrated: Option, + /// whether to skip any attempts to send via Tor pub skip_send_attempt: Option, - /// Whether to start tor listener on listener startup (default true) + /// 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 + /// Total timeout for request in seconds. + pub request_timeout_secs: Option, + /// Timeout for bootstrap in seconds. + pub bootstrap_timeout_secs: Option, + /// Tor bridge config #[serde(default)] pub bridge: TorBridgeConfig, - /// tor proxy config + /// Tor proxy config #[serde(default)] pub proxy: TorProxyConfig, } @@ -181,19 +222,57 @@ pub struct TorConfig { impl Default for TorConfig { fn default() -> TorConfig { TorConfig { + use_integrated: Some(true), skip_send_attempt: Some(false), use_tor_listener: true, socks_proxy_addr: "127.0.0.1:59050".to_owned(), send_config_dir: ".".into(), + request_timeout_secs: Some(Self::REQUEST_TIMEOUT_SECS), + bootstrap_timeout_secs: Some(Self::BOOTSTRAP_TIMEOUT_SECS), bridge: TorBridgeConfig::default(), proxy: TorProxyConfig::default(), } } } +impl TorConfig { + /// Tor request timeout in seconds. + const REQUEST_TIMEOUT_SECS: u64 = 60; + /// Tor bootstrap timeout in seconds. + const BOOTSTRAP_TIMEOUT_SECS: u64 = 60; + + /// Check if attempt to send over Tor is needed using provided possible argument at priority. + pub fn send_tor(&self, skip_arg: Option) -> bool { + if let Some(skip_tor) = skip_arg { + return !skip_tor; + } + !self.skip_send_attempt.unwrap_or(false) + } + + /// Request timeout. + pub fn request_timeout(&self) -> Duration { + Duration::from_secs( + self.request_timeout_secs + .filter(|s| *s > 0) + .unwrap_or(Self::REQUEST_TIMEOUT_SECS), + ) + } + + /// Bootstrap timeout. + pub fn bootstrap_timeout(&self) -> Duration { + Duration::from_secs( + self.bootstrap_timeout_secs + .filter(|s| *s > 0) + .unwrap_or(Self::BOOTSTRAP_TIMEOUT_SECS), + ) + } +} + /// Tor Bridge Config #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct TorBridgeConfig { + /// Path to bridge binary to use with integrated Tor library + pub bridge_bin_path: Option, /// Bridge Line pub bridge_line: Option, /// Client Option @@ -203,6 +282,7 @@ pub struct TorBridgeConfig { impl Default for TorBridgeConfig { fn default() -> TorBridgeConfig { TorBridgeConfig { + bridge_bin_path: None, bridge_line: None, client_option: None, } @@ -252,9 +332,9 @@ impl fmt::Display for TorProxyConfig { #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct GlobalWalletConfig { /// Keep track of the file we've read - pub config_file_path: Option, + pub config_file_path: PathBuf, /// Wallet members - pub members: Option, + pub members: GlobalWalletConfigMembers, } /// Wallet internal members diff --git a/controller/Cargo.toml b/controller/Cargo.toml index 578c1e863..32e03bc1a 100644 --- a/controller/Cargo.toml +++ b/controller/Cargo.toml @@ -7,24 +7,19 @@ license = "Apache-2.0" repository = "https://github.com/mimblewimble/grin-wallet" keywords = [ "crypto", "grin", "mimblewimble" ] exclude = ["**/*.grin", "**/*.grin2"] -#build = "src/build/build.rs" edition = "2021" [dependencies] -futures = "0.3" -hyper = "0.14" +tokio = { version = "1", features = ["sync"] } +hyper = { version = "1.10.1", features = ["full"] } +http-body-util = "0.1.3" 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"] } +uuid = { version = "1.23.4", features = ["serde", "v4"] } easy-jsonrpc-mw = "0.5.4" lazy_static = "1" thiserror = "1" @@ -38,51 +33,13 @@ grin_wallet_config = { path = "../config", version = "5.4.1" } ##### Grin Imports -# For Release -grin_core = "5.4.1" -grin_keychain = "5.4.1" -grin_util = "5.4.1" -grin_api = "5.4.1" - -# 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"} +grin_core = { path = "../grin/core"} +grin_keychain = { path = "../grin/keychain"} +grin_util = { path = "../grin/util"} +grin_api = { path = "../grin/api"} +grin_chain = { path = "../grin/chain"} ##### [dev-dependencies] -ed25519-dalek = "1.0.0-pre.4" -remove_dir_all = "0.7" - -##### Grin Imports - -# For Release -grin_chain = "5.4.1" - -# 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"} - -##### - +ed25519-dalek = "2" \ No newline at end of file diff --git a/controller/src/command.rs b/controller/src/command.rs index 77fdb791f..d99c09288 100644 --- a/controller/src/command.rs +++ b/controller/src/command.rs @@ -13,7 +13,6 @@ // limitations under the License. //! Grin wallet command-line function implementations - use crate::api::TLSConfig; use crate::apiwallet::{try_slatepack_sync_workflow, Owner}; use crate::config::{TorConfig, WalletConfig, WALLET_CONFIG_FILE_NAME}; @@ -22,18 +21,27 @@ use crate::error::Error; use crate::impls::PathToSlatepack; use crate::impls::SlateGetter as _; use crate::keychain; +use crate::libwallet::api_impl::owner; +use crate::libwallet::api_impl::types::update_tx_slate_state; +use crate::libwallet::contract::can_finalize; +use crate::libwallet::contract::types::{ + ContractNewArgsAPI, ContractRevokeArgsAPI, ContractSetupArgsAPI, OutputSelectionArgs, +}; use crate::libwallet::{ - self, InitTxArgs, IssueInvoiceTxArgs, NodeClient, PaymentProof, Slate, SlateState, Slatepack, + self, InitTxArgs, IssueInvoiceTxArgs, NodeClient, PaymentProof, Slate, SlateState, SlatepackAddress, Slatepacker, SlatepackerArgs, WalletLCProvider, }; use crate::util::secp::key::SecretKey; use crate::util::{Mutex, ZeroingString}; use crate::{controller, display}; -use ::core::time; + use qr_code::QrCode; +use serde::{Deserialize, Serialize}; use serde_json as json; use std::convert::TryFrom; +use std::fmt; use std::fs::File; +use std::io; use std::io::{Read, Write}; use std::sync::atomic::Ordering; use std::sync::Arc; @@ -128,15 +136,12 @@ where C: NodeClient + 'static, K: keychain::Keychain + 'static, { - controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { - let rewind_hash = api.get_rewind_hash(m)?; - println!(); - println!("Wallet Rewind Hash"); - println!("-------------------------------------"); - println!("{}", rewind_hash); - println!(); - Ok(()) - })?; + let rewind_hash = owner_api.get_rewind_hash(keychain_mask)?; + println!(); + println!("Wallet Rewind Hash"); + println!("-------------------------------------"); + println!("{}", rewind_hash); + println!(); Ok(()) } @@ -157,50 +162,41 @@ where C: NodeClient + 'static, K: keychain::Keychain + 'static, { - controller::owner_single_use(None, None, Some(owner_api), |api, m| { - let rewind_hash = args.rewind_hash; - let tip_height = api.node_height(m)?.height; - let start_height = match args.backwards_from_tip { - Some(b) => tip_height.saturating_sub(b), - None => match args.start_height { - Some(s) => s, - None => 1, - }, - }; - warn!( - "Starting view wallet output scan from height {} ...", - start_height - ); - let result = api.scan_rewind_hash(rewind_hash, Some(start_height)); - let deci_sec = time::Duration::from_millis(100); - thread::sleep(deci_sec); - match result { - Ok(res) => { - warn!("View wallet check complete"); - if res.total_balance != 0 { - display::view_wallet_output(res.clone(), tip_height, dark_scheme)?; - } - display::view_wallet_balance(res.clone(), tip_height, dark_scheme); - Ok(()) - } - Err(e) => { - error!("View wallet check failed: {}", e); - Err(e) + let rewind_hash = args.rewind_hash; + let tip_height = owner_api.node_height(None)?.height; + let start_height = match args.backwards_from_tip { + Some(b) => tip_height.saturating_sub(b), + None => args.start_height.unwrap_or_else(|| 1), + }; + warn!( + "Starting view wallet output scan from height {} ...", + start_height + ); + let result = owner_api.scan_rewind_hash(rewind_hash, Some(start_height)); + let deci_sec = Duration::from_millis(100); + thread::sleep(deci_sec); + match result { + Ok(res) => { + warn!("View wallet check complete"); + if res.total_balance != 0 { + display::view_wallet_output(res.clone(), tip_height, dark_scheme)?; } + display::view_wallet_balance(res.clone(), tip_height, dark_scheme); + Ok(()) } - })?; - Ok(()) + Err(e) => { + error!("View wallet check failed: {}", e); + Err(Error::from(e)) + } + } } -/// Arguments for listen command -pub struct ListenArgs {} - pub fn listen( owner_api: &mut Owner, keychain_mask: Arc>>, - config: &WalletConfig, - tor_config: &TorConfig, - _args: &ListenArgs, + config: WalletConfig, + bridge: Option, + use_tor: Option, g_args: &GlobalArgs, cli_mode: bool, test_mode: bool, @@ -211,20 +207,20 @@ where K: keychain::Keychain + 'static, { let wallet_inst = owner_api.wallet_inst.clone(); - let config = config.clone(); - let tor_config = tor_config.clone(); + let config_path = owner_api.config_path(); let g_args = g_args.clone(); let api_thread = thread::Builder::new() .name("wallet-http-listener".to_string()) .spawn(move || { let res = controller::foreign_listener( wallet_inst, + config_path, + bridge, + use_tor, keychain_mask, &config.api_listen_addr(), g_args.tls_conf.clone(), - tor_config.use_tor_listener, test_mode, - Some(tor_config.clone()), ); if let Err(e) = res { error!("Error starting listener: {}", e); @@ -246,7 +242,6 @@ pub fn owner_api( owner_api: &mut Owner, keychain_mask: Option, config: &WalletConfig, - tor_config: &TorConfig, g_args: &GlobalArgs, test_mode: bool, ) -> Result<(), Error> @@ -259,13 +254,12 @@ where // also being run at the same time let km = Arc::new(Mutex::new(keychain_mask)); let res = controller::owner_listener( - owner_api.wallet_inst.clone(), + owner_api, km, config.owner_api_listen_addr().as_str(), g_args.api_secret.clone(), g_args.tls_conf.clone(), config.owner_api_include_foreign, - Some(tor_config.clone()), test_mode, ); if let Err(e) = res { @@ -290,25 +284,25 @@ where K: keychain::Keychain + 'static, { if args.create.is_none() { - let res = controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { - let acct_mappings = api.accounts(m)?; + let res = { + let acct_mappings = owner_api.accounts(keychain_mask)?; // give logging thread a moment to catch up thread::sleep(Duration::from_millis(200)); display::accounts(acct_mappings); Ok(()) - }); + }; if let Err(e) = res { error!("Error listing accounts: {}", e); return Err(Error::LibWallet(e)); } } else { let label = args.create.unwrap(); - let res = controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { - api.create_account_path(m, &label)?; + let res = { + owner_api.create_account_path(keychain_mask, &label)?; thread::sleep(Duration::from_millis(200)); info!("Account: '{}' Created!", label); Ok(()) - }); + }; if let Err(e) = res { thread::sleep(Duration::from_millis(200)); error!("Error creating account '{}': {}", label, e); @@ -328,14 +322,14 @@ pub struct SendArgs { pub selection_strategy: String, pub estimate_selection_strategies: bool, pub late_lock: bool, - pub dest: String, + pub dest: Option, pub change_outputs: usize, pub fluff: bool, pub max_outputs: usize, pub target_slate_version: Option, - pub payment_proof_address: Option, + pub payment_proof_address: Option, pub ttl_blocks: Option, - pub skip_tor: bool, + pub skip_tor: Option, pub outfile: Option, pub bridge: Option, pub slatepack_qr: bool, @@ -344,8 +338,8 @@ pub struct SendArgs { pub fn send( owner_api: &mut Owner, keychain_mask: Option<&SecretKey>, - tor_config: Option, args: SendArgs, + mut tor_config: TorConfig, dark_scheme: bool, test_mode: bool, ) -> Result<(), Error> @@ -354,122 +348,131 @@ where C: NodeClient + 'static, K: keychain::Keychain + 'static, { + let dest = if args.estimate_selection_strategies { + None + } else { + args.dest + .as_deref() + .map(SlatepackAddress::try_from) + .transpose()? + }; + let mut slate = Slate::blank(2, false); let mut amount = args.amount; if args.use_max_amount { - controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { - let (_, wallet_info) = - api.retrieve_summary_info(m, true, args.minimum_confirmations)?; - amount = wallet_info.amount_currently_spendable; - Ok(()) - })?; - }; - controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { - if args.estimate_selection_strategies { - let strategies = vec!["smallest", "all"] - .into_iter() - .map(|strategy| { - let init_args = InitTxArgs { - src_acct_name: None, - amount: amount, - amount_includes_fee: Some(args.amount_includes_fee), - minimum_confirmations: args.minimum_confirmations, - max_outputs: args.max_outputs as u32, - num_change_outputs: args.change_outputs as u32, - selection_strategy_is_use_all: strategy == "all", - estimate_only: Some(true), - ..Default::default() - }; - let slate = api.init_send_tx(m, init_args)?; - Ok((strategy, slate.amount, slate.fee_fields)) - }) - .collect::, grin_wallet_libwallet::Error>>()?; - display::estimate(amount, strategies, dark_scheme); - return Ok(()); - } else { - let init_args = InitTxArgs { - src_acct_name: None, - amount: amount, - amount_includes_fee: Some(args.amount_includes_fee), - minimum_confirmations: args.minimum_confirmations, - max_outputs: args.max_outputs as u32, - num_change_outputs: args.change_outputs as u32, - selection_strategy_is_use_all: args.selection_strategy == "all", - target_slate_version: args.target_slate_version, - payment_proof_recipient_address: args.payment_proof_address.clone(), - ttl_blocks: args.ttl_blocks, - send_args: None, - late_lock: Some(args.late_lock), - ..Default::default() - }; - let result = api.init_send_tx(m, init_args); - slate = match result { - Ok(s) => { - info!( - "Tx created: {} grin to {} (strategy '{}')", - core::amount_to_hr_string(amount, false), - args.dest, - args.selection_strategy, - ); - s - } - Err(e) => { - info!("Tx not created: {}", e); - return Err(e); - } - }; - } - Ok(()) - })?; + let (_, wallet_info) = + owner_api.retrieve_summary_info(keychain_mask, true, args.minimum_confirmations)?; + amount = wallet_info.amount_currently_spendable; + } + if args.estimate_selection_strategies { + let strategies = vec!["smallest", "all"] + .into_iter() + .map(|strategy| { + let init_args = InitTxArgs { + src_acct_name: None, + amount, + amount_includes_fee: Some(args.amount_includes_fee), + minimum_confirmations: args.minimum_confirmations, + max_outputs: args.max_outputs as u32, + num_change_outputs: args.change_outputs as u32, + selection_strategy_is_use_all: strategy == "all", + estimate_only: Some(true), + ..Default::default() + }; + let slate = owner_api.init_send_tx(keychain_mask, init_args)?; + Ok((strategy, slate.amount, slate.fee_fields)) + }) + .collect::, grin_wallet_libwallet::Error>>()?; + display::estimate(amount, strategies, dark_scheme); + } else { + let payment_proof_recipient_address = args + .payment_proof_address + .as_deref() + .map(SlatepackAddress::try_from) + .transpose()?; + let init_args = InitTxArgs { + src_acct_name: None, + amount, + amount_includes_fee: Some(args.amount_includes_fee), + minimum_confirmations: args.minimum_confirmations, + max_outputs: args.max_outputs as u32, + num_change_outputs: args.change_outputs as u32, + selection_strategy_is_use_all: args.selection_strategy == "all", + target_slate_version: args.target_slate_version, + payment_proof_recipient_address, + ttl_blocks: args.ttl_blocks, + send_args: None, + late_lock: Some(args.late_lock), + ..Default::default() + }; + let result = owner_api.init_send_tx(keychain_mask, init_args); + slate = match result { + Ok(s) => { + info!( + "Tx created: {} grin to {} (strategy '{}')", + core::amount_to_hr_string(amount, false), + dest.as_ref() + .map(ToString::to_string) + .unwrap_or_else(|| "no destination".to_string()), + args.selection_strategy, + ); + s + } + Err(e) => { + info!("Tx not created: {}", e); + return Err(Error::from(e)); + } + }; + } if args.estimate_selection_strategies { return Ok(()); } - let tor_config = match tor_config { - Some(mut c) => { - if let Some(b) = args.bridge.clone() { - c.bridge.bridge_line = Some(b); - } - c.skip_send_attempt = Some(args.skip_tor); - Some(c) - } - None => None, + if let Some(b) = args.bridge.clone() { + tor_config.bridge.bridge_line = Some(b); + } + + let output_sp = || -> Result<(), Error> { + Ok(output_slatepack( + owner_api, + keychain_mask, + &slate, + dest.clone(), + args.outfile, + true, + false, + args.slatepack_qr, + )?) }; - let res = try_slatepack_sync_workflow(&slate, &args.dest, tor_config, None, false, test_mode); + let can_send = tor_config.send_tor(args.skip_tor); + if test_mode || !can_send || dest.is_none() { + return output_sp(); + } + + let dest = dest.as_ref().unwrap(); + let res = try_slatepack_sync_workflow(&slate, dest, Some(tor_config), None, false); match res { - Ok(Some(s)) => { - controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { - api.tx_lock_outputs(m, &s)?; - let ret_slate = api.finalize_tx(m, &s)?; - let result = api.post_tx(m, &ret_slate, args.fluff); - match result { - Ok(_) => { - println!("Tx sent successfully",); - Ok(()) - } - Err(e) => { - error!("Tx sent fail: {}", e); - Err(e.into()) - } + Ok(s) => { + owner_api.tx_lock_outputs(keychain_mask, &s)?; + let ret_slate = owner_api.finalize_tx(keychain_mask, &s)?; + let result = owner_api.post_tx(keychain_mask, &ret_slate, args.fluff); + match result { + Ok(_) => { + println!("Tx sent successfully",); } - })?; + Err(e) => { + error!("Tx sent fail: {}", e); + return Err(e.into()); + } + } } - Ok(None) => { - output_slatepack( - owner_api, - keychain_mask, - &slate, - args.dest.as_str(), - args.outfile, - true, - false, - args.slatepack_qr, - )?; + Err(e) => { + error!("Error sending slate sync: {}", e); + output_sp()?; } - Err(e) => return Err(e.into()), } Ok(()) } @@ -478,7 +481,7 @@ pub fn output_slatepack( owner_api: &mut Owner, keychain_mask: Option<&SecretKey>, slate: &Slate, - dest: &str, + dest: Option, out_file_override: Option, lock: bool, finalizing: bool, @@ -490,23 +493,13 @@ where K: keychain::Keychain + 'static, { // Output the slatepack file to stdout and to a file - let mut message = String::from(""); - let mut address = None; - let mut tld = String::from(""); - controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { - address = match SlatepackAddress::try_from(dest) { - Ok(a) => Some(a), - Err(_) => None, - }; - // encrypt for recipient by default - let recipients = match address.clone() { - Some(a) => vec![a], - None => vec![], - }; - message = api.create_slatepack_message(m, &slate, Some(0), recipients)?; - tld = api.get_top_level_directory()?; - Ok(()) - })?; + // encrypt for recipient by default + let recipients = match dest.clone() { + Some(a) => vec![a], + None => vec![], + }; + let message = owner_api.create_slatepack_message(keychain_mask, &slate, Some(0), recipients)?; + let tld = owner_api.get_top_level_directory()?; // create a directory to which files will be output let slate_dir = format!("{}/{}", tld, "slatepack"); @@ -517,10 +510,7 @@ where }; if lock { - controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { - api.tx_lock_outputs(m, &slate)?; - Ok(()) - })?; + owner_api.tx_lock_outputs(keychain_mask, &slate)?; } println!("{}", out_file_name); @@ -550,7 +540,7 @@ where println!(); } } - if address.is_some() { + if dest.is_some() { println!("The slatepack data is encrypted for the recipient only"); } else { println!("The slatepack data is NOT encrypted"); @@ -559,6 +549,144 @@ where Ok(()) } +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct SlatepackOut { + /// Is slatepack encrypted + pub is_encrypted: bool, + /// Is slatepack finalized + pub is_finalized: bool, + /// File where slatepack is saved + pub out_file: String, + /// Slatepack message. Encrypted or not. + pub message: String, +} + +impl fmt::Display for SlatepackOut { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let start_meta = "--------------- SLATEPACK METADATA --------------"; + let meta = format!( + "Slate encrypted: {}\nSlate finalized: {}\nSlate saved to file: {}", + self.is_encrypted, self.is_finalized, self.out_file + ); + let start_slatepack = "-------------- CUT BELOW THIS LINE --------------"; + let end_slatepack = "-------------- CUT ABOVE THIS LINE --------------"; + write!( + f, + "{start_meta}\n\n{meta}\n\n{start_slatepack}\n\n{}\n\n{end_slatepack}", + self.message + ) + } +} + +impl SlatepackOut { + fn as_json(&self) -> String { + serde_json::to_string_pretty(&self).unwrap() + } + + pub fn print(&self, as_json: bool) -> () { + if !self.is_finalized { + if as_json { + println!("{}", self.as_json()); + } else { + println!("{}", self); + } + } else { + println!("Transaction was broadcasted."); // TODO: as_json makes no sense here, fix later. + } + } +} + +pub fn print_slatepack( + api: &mut Owner, + keychain_mask: Option<&SecretKey>, + slate: &Slate, + counterparty_addr: &str, + out_file: Option, + as_json: bool, +) -> Result<(), libwallet::Error> +where + L: WalletLCProvider<'static, C, K> + 'static, + C: NodeClient + 'static, + K: keychain::Keychain + 'static, +{ + // For now, we don't compact slates with sl.compact(). We first make them work without compaction. + // Writing the file, serializing and encrypting can all fail for ordinary reasons, so + // report them through the normal CLI error path rather than unwrapping. + let slate_out = prepare_slatepack(api, keychain_mask, &slate, &counterparty_addr, out_file)?; + slate_out.print(as_json); + Ok(()) +} + +pub fn prepare_slatepack( + owner_api: &mut Owner, + keychain_mask: Option<&SecretKey>, + slate: &Slate, + dest: &str, + out_file_override: Option, +) -> Result +where + L: WalletLCProvider<'static, C, K> + 'static, + C: NodeClient + 'static, + K: keychain::Keychain + 'static, +{ + // Same as output_slatepack except that we don't write to stdout, care about locking or whether the slate was finalized. + + // Output the slatepack file to stdout and to a file + let mut message = String::from(""); + let mut address = None; + let mut tld = String::from(""); + let wallet_inst = owner_api.wallet_inst.clone(); + let config_path = owner_api.config_path(); + controller::owner_single_use(wallet_inst, keychain_mask, config_path, |api, m| { + address = match SlatepackAddress::try_from(dest) { + Ok(a) => Some(a), + Err(_) => None, + }; + // encrypt for recipient by default + let recipients = match address.clone() { + Some(a) => vec![a], + None => vec![], + }; + // TODO: what is sender_index? + message = api.create_slatepack_message(m, &slate, Some(0), recipients)?; + // Trim the \n at the end. + let len_withoutcrlf = message.trim_end().len(); + message.truncate(len_withoutcrlf); + + tld = api.get_top_level_directory()?; + Ok(()) + })?; + + // create a directory to which files will be output + let slate_dir = format!("{}/{}", tld, "slatepack"); + let _ = std::fs::create_dir_all(slate_dir.clone()); + let out_file_name = match out_file_override { + None => format!("{}/{}.{}.slatepack", slate_dir, slate.id, slate.state), + Some(f) => f, + }; + + let mut output = File::create(out_file_name.clone())?; + output.write_all(&message.as_bytes())?; + output.sync_all()?; + + // Since we always finalize if we can, we can also use this to know if the tx is finalized + let is_finalized = can_finalize(slate); + + let slate_out = SlatepackOut { + is_encrypted: address.is_some(), + is_finalized: is_finalized, + out_file: out_file_name, + message: message, + }; + + // TODO: We save the slatepack, but it is encrypted for the counterparty. It seems hard to + // know which slatepack is which if we can't decrypt them. Either add some more metadata + // to slatepacks e.g. timestamp, counterparty address or save also a version that is encrypted with + // our own address so we can view it. + + Ok(slate_out) +} + // Parse a slate and slatepack from a message pub fn parse_slatepack( owner_api: &mut Owner, @@ -575,19 +703,15 @@ where let slate = match filename { Some(f) => { // otherwise, get slate from slatepack - let mut sl = None; - controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { - let dec_key = api.get_slatepack_secret_key(m, 0)?; - let packer = Slatepacker::new(SlatepackerArgs { - sender: None, - recipients: vec![], - dec_key: Some(&dec_key), - }); - let pts = PathToSlatepack::new(f.into(), &packer, true); - sl = Some(pts.get_tx()?.0); - ret_address = pts.get_slatepack(true)?.sender; - Ok(()) - })?; + let dec_key = owner_api.get_slatepack_secret_key(keychain_mask, 0)?; + let packer = Slatepacker::new(SlatepackerArgs { + sender: None, + recipients: vec![], + dec_key: Some(&dec_key), + }); + let pts = PathToSlatepack::new(f.into(), &packer, true); + let sl = Some(pts.get_tx()?.0); + ret_address = pts.get_slatepack(true)?.sender; sl } None => None, @@ -597,29 +721,26 @@ where Some(s) => s, None => { // try and parse directly from input_slatepack_message - let mut slate = Slate::blank(2, false); match message { Some(message) => { - controller::owner_single_use( - None, + let slate = owner_api.slate_from_slatepack_message( keychain_mask, - Some(owner_api), - |api, m| { - slate = - api.slate_from_slatepack_message(m, message.clone(), vec![0])?; - let slatepack = - api.decode_slatepack_message(m, message.clone(), vec![0])?; - ret_address = slatepack.sender; - Ok(()) - }, + message.clone(), + vec![0], )?; + let slatepack = owner_api.decode_slatepack_message( + keychain_mask, + message.clone(), + vec![0], + )?; + ret_address = slatepack.sender; + slate } None => { let msg = "No slate provided via file or direct input"; return Err(Error::GenericError(msg.into()).into()); } } - slate } }; Ok((slate, ret_address)) @@ -630,7 +751,7 @@ where pub struct ReceiveArgs { pub input_file: Option, pub input_slatepack_message: Option, - pub skip_tor: bool, + pub skip_tor: Option, pub outfile: Option, pub bridge: Option, pub slatepack_qr: bool, @@ -641,7 +762,7 @@ pub fn receive( keychain_mask: Option<&SecretKey>, g_args: &GlobalArgs, args: ReceiveArgs, - tor_config: Option, + mut tor_config: TorConfig, test_mode: bool, ) -> Result<(), Error> where @@ -661,54 +782,66 @@ where Some(&m) => Some(m.to_owned()), }; - let tor_config = match tor_config { - Some(mut c) => { - if let Some(b) = args.bridge { - c.bridge.bridge_line = Some(b); - } - c.skip_send_attempt = Some(args.skip_tor); - Some(c) - } - None => None, - }; + if let Some(b) = args.bridge { + tor_config.bridge.bridge_line = Some(b); + } - controller::foreign_single_use(owner_api.wallet_inst.clone(), km, |api| { - slate = api.receive_tx(&slate, Some(&g_args.account), None)?; - Ok(()) - })?; + controller::foreign_single_use( + owner_api.wallet_inst.clone(), + owner_api.config_path(), + km, + |api| { + slate = api.receive_tx(&slate, Some(&g_args.account), None)?; + Ok(()) + }, + )?; - let dest = match ret_address { - Some(a) => String::try_from(&a).unwrap(), - None => String::from(""), + let output_sp = || -> Result<(), Error> { + Ok(output_slatepack( + owner_api, + keychain_mask, + &slate, + ret_address.clone(), + args.outfile, + false, + false, + args.slatepack_qr, + )?) }; - let res = try_slatepack_sync_workflow(&slate, &dest, tor_config, None, true, test_mode); + let can_send = tor_config.send_tor(args.skip_tor); + if test_mode || !can_send || ret_address.is_none() { + return output_sp(); + } + + let dest = ret_address.as_ref().unwrap(); + let res = try_slatepack_sync_workflow(&slate, dest, Some(tor_config), None, true); match res { - Ok(Some(_)) => { + Ok(s) => { + // Update slate state. + { + let mut w_lock = owner_api.wallet_inst.lock(); + let w = w_lock.lc_provider()?.wallet_inst()?; + let parent_key_id = w.parent_key_id(); + match update_tx_slate_state(w, keychain_mask, &parent_key_id, &s) { + Ok(_) => {} + Err(e) => error!("Error on updating slate state: {}", e), + } + } println!(); println!( - "Transaction recieved and sent back to sender at {} for finalization.", + "Transaction received and sent back to sender at {} for finalization.", dest ); println!(); - Ok(()) } - Ok(None) => { - output_slatepack( - owner_api, - keychain_mask, - &slate, - &dest, - args.outfile, - false, - false, - args.slatepack_qr, - )?; - Ok(()) + Err(e) => { + error!("Error sending slate sync: {}", e); + output_sp()?; } - Err(e) => Err(e.into()), } + Ok(()) } pub fn unpack( @@ -731,14 +864,7 @@ where PathToSlatepack::new(f.into(), &packer, true).get_slatepack(false)? } None => match args.input_slatepack_message { - Some(mes) => { - let mut sp = Slatepack::default(); - controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { - sp = api.decode_slatepack_message(m, mes, vec![])?; - Ok(()) - })?; - sp - } + Some(mes) => owner_api.decode_slatepack_message(keychain_mask, mes, vec![])?, None => { return Err(Error::ArgumentError("Invalid Slatepack Input".into()).into()); } @@ -757,27 +883,24 @@ where }); if slatepack.mode == 1 { - controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { - let dec_key = api.get_slatepack_secret_key(m, 0)?; - match slatepack.try_decrypt_payload(Some(&dec_key)) { - Ok(_) => { - println!("Slatepack is encrypted for this wallet"); - println!(); - println!("DECRYPTED SLATEPACK"); - println!("-------------------"); - println!("{}", slatepack); - let slate = packer.get_slate(&slatepack)?; - println!(); - println!("DECRYPTED SLATE"); - println!("---------------"); - println!("{}", slate); - } - Err(_) => { - println!("Slatepack payload cannot be decrypted by this wallet"); - } + let dec_key = owner_api.get_slatepack_secret_key(keychain_mask, 0)?; + match slatepack.try_decrypt_payload(Some(&dec_key)) { + Ok(_) => { + println!("Slatepack is encrypted for this wallet"); + println!(); + println!("DECRYPTED SLATEPACK"); + println!("-------------------"); + println!("{}", slatepack); + let slate = packer.get_slate(&slatepack)?; + println!(); + println!("DECRYPTED SLATE"); + println!("---------------"); + println!("{}", slate); } - Ok(()) - })?; + Err(_) => { + println!("Slatepack payload cannot be decrypted by this wallet"); + } + } } else { let slate = packer.get_slate(&slatepack)?; println!("Slatepack is not encrypted"); @@ -827,34 +950,31 @@ where None => None, Some(&m) => Some(m.to_owned()), }; - controller::foreign_single_use(owner_api.wallet_inst.clone(), km, |api| { - slate = api.finalize_tx(&slate, false)?; - Ok(()) - })?; + controller::foreign_single_use( + owner_api.wallet_inst.clone(), + owner_api.config_path(), + km, + |api| { + slate = api.finalize_tx(&slate, false)?; + Ok(()) + }, + )?; } else { - controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { - slate = api.finalize_tx(m, &slate)?; - Ok(()) - })?; + slate = owner_api.finalize_tx(keychain_mask, &slate)? } if !&args.nopost { - controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { - let result = api.post_tx(m, &slate, args.fluff); - match result { - Ok(_) => { - info!( - "Transaction sent successfully, check the wallet again for confirmation." - ); - println!("Transaction posted"); - Ok(()) - } - Err(e) => { - error!("Tx not sent: {}", e); - Err(e) - } + let result = owner_api.post_tx(keychain_mask, &slate, args.fluff); + match result { + Ok(_) => { + info!("Transaction sent successfully, check the wallet again for confirmation."); + println!("Transaction posted"); } - })?; + Err(e) => { + error!("Tx not sent: {}", e); + return Err(Error::from(e)); + } + } } println!("Transaction finalized successfully"); @@ -863,7 +983,7 @@ where owner_api, keychain_mask, &slate, - "", + None, args.outfile, false, true, @@ -876,7 +996,7 @@ where /// Issue Invoice Args pub struct IssueInvoiceArgs { /// Slatepack address - pub dest: String, + pub dest: Option, /// issue invoice tx args pub issue_args: IssueInvoiceTxArgs, /// output file override @@ -895,19 +1015,20 @@ where C: NodeClient + 'static, K: keychain::Keychain + 'static, { + let dest = args + .dest + .as_deref() + .map(SlatepackAddress::try_from) + .transpose()?; let issue_args = args.issue_args.clone(); - let mut slate = Slate::blank(2, false); - controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { - slate = api.issue_invoice_tx(m, issue_args)?; - Ok(()) - })?; + let slate = owner_api.issue_invoice_tx(keychain_mask, issue_args)?; output_slatepack( owner_api, keychain_mask, &slate, - args.dest.as_str(), + dest, args.outfile, false, false, @@ -920,12 +1041,12 @@ where pub struct ProcessInvoiceArgs { pub minimum_confirmations: u64, pub selection_strategy: String, - pub ret_address: Option, + pub ret_address: Option, pub max_outputs: usize, pub slate: Slate, pub estimate_selection_strategies: bool, pub ttl_blocks: Option, - pub skip_tor: bool, + pub skip_tor: Option, pub outfile: Option, pub bridge: Option, pub slatepack_qr: bool, @@ -935,7 +1056,7 @@ pub struct ProcessInvoiceArgs { pub fn process_invoice( owner_api: &mut Owner, keychain_mask: Option<&SecretKey>, - tor_config: Option, + mut tor_config: TorConfig, args: ProcessInvoiceArgs, dark_scheme: bool, test_mode: bool, @@ -945,102 +1066,111 @@ where C: NodeClient + 'static, K: keychain::Keychain + 'static, { + let ret_address = args + .ret_address + .as_deref() + .map(SlatepackAddress::try_from) + .transpose()?; let mut slate = args.slate.clone(); - let dest = match args.ret_address.clone() { - Some(a) => String::try_from(&a).unwrap(), - None => String::from(""), - }; - controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { - if args.estimate_selection_strategies { - let strategies = vec!["smallest", "all"] - .into_iter() - .map(|strategy| { - let init_args = InitTxArgs { - src_acct_name: None, - amount: slate.amount, - minimum_confirmations: args.minimum_confirmations, - max_outputs: args.max_outputs as u32, - num_change_outputs: 1u32, - selection_strategy_is_use_all: strategy == "all", - estimate_only: Some(true), - ..Default::default() - }; - let slate = api.init_send_tx(m, init_args).unwrap(); - (strategy, slate.amount, slate.fee_fields) - }) - .collect(); - display::estimate(slate.amount, strategies, dark_scheme); - return Ok(()); - } else { - let init_args = InitTxArgs { - src_acct_name: None, - amount: 0, - minimum_confirmations: args.minimum_confirmations, - max_outputs: args.max_outputs as u32, - num_change_outputs: 1u32, - selection_strategy_is_use_all: args.selection_strategy == "all", - ttl_blocks: args.ttl_blocks, - send_args: None, - ..Default::default() - }; - let result = api.process_invoice_tx(m, &slate, init_args); - slate = match result { - Ok(s) => { - info!( - "Invoice processed: {} grin (strategy '{}')", - core::amount_to_hr_string(slate.amount, false), - args.selection_strategy, - ); - s - } - Err(e) => { - info!("Tx not created: {}", e); - return Err(e); - } - }; - } - Ok(()) - })?; - - let tor_config = match tor_config { - Some(mut c) => { - if let Some(b) = args.bridge { - c.bridge.bridge_line = Some(b); + if args.estimate_selection_strategies { + let strategies = vec!["smallest", "all"] + .into_iter() + .map(|strategy| { + let init_args = InitTxArgs { + src_acct_name: None, + amount: slate.amount, + minimum_confirmations: args.minimum_confirmations, + max_outputs: args.max_outputs as u32, + num_change_outputs: 1u32, + selection_strategy_is_use_all: strategy == "all", + estimate_only: Some(true), + ..Default::default() + }; + let slate = owner_api.init_send_tx(keychain_mask, init_args).unwrap(); + (strategy, slate.amount, slate.fee_fields) + }) + .collect(); + display::estimate(slate.amount, strategies, dark_scheme); + } else { + let init_args = InitTxArgs { + src_acct_name: None, + amount: 0, + minimum_confirmations: args.minimum_confirmations, + max_outputs: args.max_outputs as u32, + num_change_outputs: 1u32, + selection_strategy_is_use_all: args.selection_strategy == "all", + ttl_blocks: args.ttl_blocks, + send_args: None, + ..Default::default() + }; + let result = owner_api.process_invoice_tx(keychain_mask, &slate, init_args); + slate = match result { + Ok(s) => { + info!( + "Invoice processed: {} grin (strategy '{}')", + core::amount_to_hr_string(slate.amount, false), + args.selection_strategy, + ); + s } - c.skip_send_attempt = Some(args.skip_tor); - Some(c) - } - None => None, + Err(e) => { + info!("Tx not created: {}", e); + return Err(Error::from(e)); + } + }; + } + + if let Some(b) = args.bridge { + tor_config.bridge.bridge_line = Some(b); + } + + let output_sp = || -> Result<(), Error> { + Ok(output_slatepack( + owner_api, + keychain_mask, + &slate, + ret_address.clone(), + args.outfile, + true, + false, + args.slatepack_qr, + )?) }; - let res = try_slatepack_sync_workflow(&slate, &dest, tor_config, None, true, test_mode); + let can_send = tor_config.send_tor(args.skip_tor); + if test_mode || !can_send || ret_address.is_none() { + return output_sp(); + } + + let dest = ret_address.as_ref().unwrap(); + let res = try_slatepack_sync_workflow(&slate, dest, Some(tor_config), None, true); match res { - Ok(Some(_)) => { + Ok(s) => { + // Update slate state. + { + let mut w_lock = owner_api.wallet_inst.lock(); + let w = w_lock.lc_provider()?.wallet_inst()?; + let parent_key_id = w.parent_key_id(); + match update_tx_slate_state(w, keychain_mask, &parent_key_id, &s) { + Ok(_) => {} + Err(e) => error!("Error on updating slate state: {}", e), + } + } println!(); println!( "Transaction paid and sent back to initiator at {} for finalization.", dest ); println!(); - Ok(()) } - Ok(None) => { - output_slatepack( - owner_api, - keychain_mask, - &slate, - &dest, - args.outfile, - true, - false, - args.slatepack_qr, - )?; - Ok(()) + Err(e) => { + error!("Error sending slate sync: {}", e); + output_sp()?; } - Err(e) => Err(e.into()), } + Ok(()) } /// Info command args @@ -1061,17 +1191,14 @@ where K: keychain::Keychain + 'static, { let updater_running = owner_api.updater_running.load(Ordering::Relaxed); - controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { - let (validated, wallet_info) = - api.retrieve_summary_info(m, true, args.minimum_confirmations)?; - display::info( - &g_args.account, - &wallet_info, - validated || updater_running, - dark_scheme, - ); - Ok(()) - })?; + let (validated, wallet_info) = + owner_api.retrieve_summary_info(keychain_mask, true, args.minimum_confirmations)?; + display::info( + &g_args.account, + &wallet_info, + validated || updater_running, + dark_scheme, + ); Ok(()) } @@ -1087,18 +1214,16 @@ where K: keychain::Keychain + 'static, { let updater_running = owner_api.updater_running.load(Ordering::Relaxed); - controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { - let res = api.node_height(m)?; - let (validated, outputs) = api.retrieve_outputs(m, g_args.show_spent, true, None)?; - display::outputs( - &g_args.account, - res.height, - validated || updater_running, - outputs, - dark_scheme, - )?; - Ok(()) - })?; + let res = owner_api.node_height(keychain_mask)?; + let (validated, outputs) = + owner_api.retrieve_outputs(keychain_mask, g_args.show_spent, true, None)?; + display::outputs( + &g_args.account, + res.height, + validated || updater_running, + outputs, + dark_scheme, + )?; Ok(()) } @@ -1122,56 +1247,53 @@ where K: keychain::Keychain + 'static, { let updater_running = owner_api.updater_running.load(Ordering::Relaxed); - controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { - let res = api.node_height(m)?; - // Note advanced query args not currently supported by command line client - let (validated, txs) = api.retrieve_txs(m, true, args.id, args.tx_slate_id, None)?; - let include_status = !args.id.is_some() && !args.tx_slate_id.is_some(); - // If view count is specified, restrict the TX list to `txs.len() - count` - let first_tx = args - .count - .map_or(0, |c| txs.len().saturating_sub(c as usize)); - display::txs( + let res = owner_api.node_height(keychain_mask)?; + // Note advanced query args not currently supported by command line client + let (validated, txs) = + owner_api.retrieve_txs(keychain_mask, true, args.id, args.tx_slate_id, None)?; + let include_status = !args.id.is_some() && !args.tx_slate_id.is_some(); + // If view count is specified, restrict the TX list to `txs.len() - count` + let first_tx = args + .count + .map_or(0, |c| txs.len().saturating_sub(c as usize)); + display::txs( + &g_args.account, + res.height, + validated || updater_running, + &txs[first_tx..], + include_status, + dark_scheme, + )?; + + // if given a particular transaction id or uuid, also get and display associated + // inputs/outputs and messages + let id = if args.id.is_some() { + args.id + } else if args.tx_slate_id.is_some() { + if let Some(tx) = txs.iter().find(|t| t.tx_slate_id == args.tx_slate_id) { + Some(tx.id) + } else { + println!("Could not find a transaction matching given txid.\n"); + None + } + } else { + None + }; + + if id.is_some() { + let (_, outputs) = owner_api.retrieve_outputs(keychain_mask, true, false, id)?; + display::outputs( &g_args.account, res.height, validated || updater_running, - &txs[first_tx..], - include_status, + outputs, dark_scheme, )?; - - // if given a particular transaction id or uuid, also get and display associated - // inputs/outputs and messages - let id = if args.id.is_some() { - args.id - } else if args.tx_slate_id.is_some() { - if let Some(tx) = txs.iter().find(|t| t.tx_slate_id == args.tx_slate_id) { - Some(tx.id) - } else { - println!("Could not find a transaction matching given txid.\n"); - None - } - } else { - None - }; - - if id.is_some() { - let (_, outputs) = api.retrieve_outputs(m, true, false, id)?; - display::outputs( - &g_args.account, - res.height, - validated || updater_running, - outputs, - dark_scheme, - )?; - // should only be one here, but just in case - for tx in txs { - display::payment_proof(&tx)?; - } + // should only be one here, but just in case + for tx in txs { + display::payment_proof(&tx)?; } - - Ok(()) - })?; + } Ok(()) } @@ -1201,11 +1323,8 @@ where )?; let fluff = args.fluff; - controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { - api.post_tx(m, &slate, fluff)?; - info!("Posted transaction"); - return Ok(()); - })?; + owner_api.post_tx(keychain_mask, &slate, fluff)?; + info!("Posted transaction"); Ok(()) } @@ -1226,53 +1345,55 @@ where C: NodeClient + 'static, K: keychain::Keychain + 'static, { - controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { - let stored_tx_slate = match api.get_stored_tx(m, Some(args.id), None)? { - None => { + let stored_tx_slate = match owner_api.get_stored_tx(keychain_mask, Some(args.id), None)? { + None => { + error!( + "Transaction with id {} does not have transaction data. Not reposting.", + args.id + ); + return Ok(()); + } + Some(s) => s, + }; + let (_, txs) = owner_api.retrieve_txs(keychain_mask, true, Some(args.id), None, None)?; + match args.dump_file { + None => { + if txs[0].confirmed { error!( - "Transaction with id {} does not have transaction data. Not reposting.", + "Transaction with id {} is confirmed. Not reposting.", args.id ); return Ok(()); } - Some(s) => s, - }; - let (_, txs) = api.retrieve_txs(m, true, Some(args.id), None, None)?; - match args.dump_file { - None => { - if txs[0].confirmed { - error!( - "Transaction with id {} is confirmed. Not reposting.", - args.id - ); - return Ok(()); - } - if libwallet::sig_is_blank( - &stored_tx_slate.tx.as_ref().unwrap().kernels()[0].excess_sig, - ) { - error!("Transaction at {} has not been finalized.", args.id); - return Ok(()); - } - - match api.post_tx(m, &stored_tx_slate, args.fluff) { - Ok(_) => info!("Reposted transaction at {}", args.id), - Err(e) => error!("Could not repost transaction at {}. Reason: {}", args.id, e), - } + if libwallet::sig_is_blank( + &stored_tx_slate.tx.as_ref().unwrap().kernels()[0].excess_sig, + ) { + error!("Transaction at {} has not been finalized.", args.id); return Ok(()); } - Some(f) => { - let mut tx_file = File::create(f.clone())?; - tx_file.write_all( + + match owner_api.post_tx(keychain_mask, &stored_tx_slate, args.fluff) { + Ok(_) => info!("Reposted transaction at {}", args.id), + Err(e) => error!("Could not repost transaction at {}. Reason: {}", args.id, e), + } + return Ok(()); + } + Some(f) => { + let mut tx_file = + File::create(f.clone()).map_err(|e| Error::GenericError(format!("{}", e)))?; + tx_file + .write_all( json::to_string(&stored_tx_slate.tx.unwrap()) .unwrap() .as_bytes(), - )?; - tx_file.sync_all()?; - info!("Dumped transaction data for tx {} to {}", args.id, f); - return Ok(()); - } + ) + .map_err(|e| Error::GenericError(format!("{}", e)))?; + tx_file + .sync_all() + .map_err(|e| Error::GenericError(format!("{}", e)))?; + info!("Dumped transaction data for tx {} to {}", args.id, f); } - })?; + } Ok(()) } @@ -1293,20 +1414,17 @@ where C: NodeClient + 'static, K: keychain::Keychain + 'static, { - controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { - let result = api.cancel_tx(m, args.tx_id, args.tx_slate_id); - match result { - Ok(_) => { - info!("Transaction {} Cancelled", args.tx_id_string); - Ok(()) - } - Err(e) => { - error!("TX Cancellation failed: {}", e); - Err(e) - } + let result = owner_api.cancel_tx(keychain_mask, args.tx_id, args.tx_slate_id); + match result { + Ok(_) => { + info!("Transaction {} Cancelled", args.tx_id_string); + Ok(()) } - })?; - Ok(()) + Err(e) => { + error!("TX Cancellation failed: {}", e); + Err(Error::from(e)) + } + } } /// wallet check @@ -1326,29 +1444,23 @@ where C: NodeClient + 'static, K: keychain::Keychain + 'static, { - controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { - let tip_height = api.node_height(m)?.height; - let start_height = match args.backwards_from_tip { - Some(b) => tip_height.saturating_sub(b), - None => match args.start_height { - Some(s) => s, - None => 1, - }, - }; - warn!("Starting output scan from height {} ...", start_height); - let result = api.scan(m, Some(start_height), args.delete_unconfirmed); - match result { - Ok(_) => { - warn!("Wallet check complete",); - Ok(()) - } - Err(e) => { - error!("Wallet check failed: {}", e); - Err(e) - } + let tip_height = owner_api.node_height(keychain_mask)?.height; + let start_height = match args.backwards_from_tip { + Some(b) => tip_height.saturating_sub(b), + None => args.start_height.unwrap_or_else(|| 1), + }; + warn!("Starting output scan from height {} ...", start_height); + let result = owner_api.scan(keychain_mask, Some(start_height), args.delete_unconfirmed); + match result { + Ok(_) => { + warn!("Wallet check complete",); + Ok(()) } - })?; - Ok(()) + Err(e) => { + error!("Wallet check failed: {}", e); + Err(Error::from(e)) + } + } } /// Payment Proof Address @@ -1362,16 +1474,13 @@ where C: NodeClient + 'static, K: keychain::Keychain + 'static, { - controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { - // Just address at derivation index 0 for now - let address = api.get_slatepack_address(m, 0)?; - println!(); - println!("Address for account - {}", g_args.account); - println!("-------------------------------------"); - println!("{}", address); - println!(); - Ok(()) - })?; + // Just address at derivation index 0 for now + let address = owner_api.get_slatepack_address(keychain_mask, 0)?; + println!(); + println!("Address for account - {}", g_args.account); + println!("-------------------------------------"); + println!("{}", address); + println!(); Ok(()) } @@ -1392,24 +1501,26 @@ where C: NodeClient + 'static, K: keychain::Keychain + 'static, { - controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { - let result = api.retrieve_payment_proof(m, true, args.id, args.tx_slate_id); - match result { - Ok(p) => { - // actually export proof - let mut proof_file = File::create(args.output_file.clone())?; - proof_file.write_all(json::to_string_pretty(&p).unwrap().as_bytes())?; - proof_file.sync_all()?; - warn!("Payment proof exported to {}", args.output_file); - Ok(()) - } - Err(e) => { - error!("Proof export failed: {}", e); - Err(e) - } + let result = owner_api.retrieve_payment_proof(keychain_mask, true, args.id, args.tx_slate_id); + match result { + Ok(p) => { + // actually export proof + let mut proof_file = File::create(args.output_file.clone()) + .map_err(|e| Error::GenericError(format!("{}", e)))?; + proof_file + .write_all(json::to_string_pretty(&p).unwrap().as_bytes()) + .map_err(|e| Error::GenericError(format!("{}", e)))?; + proof_file + .sync_all() + .map_err(|e| Error::GenericError(format!("{}", e)))?; + warn!("Payment proof exported to {}", args.output_file); + Ok(()) } - })?; - Ok(()) + Err(e) => { + error!("Proof export failed: {}", e); + Err(Error::from(e)) + } + } } /// Proof Verify Args @@ -1427,51 +1538,363 @@ where C: NodeClient + 'static, K: keychain::Keychain + 'static, { - controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { - let mut proof_f = match File::open(&args.input_file) { - Ok(p) => p, - Err(e) => { - let msg = format!("{}", e); - error!( - "Unable to open payment proof file at {}: {}", - args.input_file, e + let mut proof_f = match File::open(&args.input_file) { + Ok(p) => p, + Err(e) => { + let msg = format!("{}", e); + error!( + "Unable to open payment proof file at {}: {}", + args.input_file, e + ); + return Err(Error::from(libwallet::Error::PaymentProofParsing(msg))); + } + }; + let mut proof = String::new(); + proof_f + .read_to_string(&mut proof) + .map_err(|e| Error::GenericError(format!("{}", e)))?; + // read + let proof: PaymentProof = match json::from_str(&proof) { + Ok(p) => p, + Err(e) => { + let msg = format!("{}", e); + error!("Unable to parse payment proof file: {}", e); + return Err(Error::from(libwallet::Error::PaymentProofParsing(msg))); + } + }; + let result = owner_api.verify_payment_proof(keychain_mask, &proof); + match result { + Ok((iam_sender, iam_recipient)) => { + println!("Payment proof's signatures are valid."); + if iam_sender { + println!("The proof's sender address belongs to this wallet."); + } + if iam_recipient { + println!("The proof's recipient address belongs to this wallet."); + } + if !iam_recipient && !iam_sender { + println!( + "Neither the proof's sender nor recipient address belongs to this wallet." ); - return Err(libwallet::Error::PaymentProofParsing(msg)); } + Ok(()) + } + Err(e) => { + error!("Proof not valid: {}", e); + Err(Error::from(e)) + } + } +} + +/// Create new contract command arguments +#[derive(Clone)] +pub struct ContractNewArgs { + /// Address of the counterparty (None = produce an unencrypted slatepack) + pub counterparty_addr: Option, + /// Receive amount + pub receive: Option, + /// Send amount + pub send: Option, + /// The human readable account name from which to draw outputs + /// for the transaction, overriding whatever the active account is as set via the + /// [`set_active_account`](../grin_wallet_api/owner/struct.Owner.html#method.set_active_account) method. + pub src_acct_name: Option, + /// Number of participants in a contract (either 1 or 2) + pub num_participants: u8, + /// Show the resulting slatepack as JSON + pub as_json: bool, + /// Use the specified inputs (comma separated input commitments) + pub use_inputs: Option, + /// Output amounts in nanogrin (one entry per output) + pub make_outputs: Option>, + + // Future features + /// Custom fee contribution + pub fee_rate: Option, + /// Save slatepack to a specific filename + pub outfile: Option, + /// Select outputs early + pub add_outputs: bool, +} + +impl ContractNewArgs { + fn get_net_change(&self) -> Result { + let to_i64 = |v: u64| { + i64::try_from(v).map_err(|_| Error::ArgumentError(format!("Amount {} is too large", v))) }; - let mut proof = String::new(); - proof_f.read_to_string(&mut proof)?; - // read - let proof: PaymentProof = match json::from_str(&proof) { - Ok(p) => p, - Err(e) => { - let msg = format!("{}", e); - error!("Unable to parse payment proof file: {}", e); - return Err(libwallet::Error::PaymentProofParsing(msg)); - } + match self.receive { + None => match self.send { + None => Err(Error::ArgumentError( + "Send or receive not specified.".into(), + )), + Some(v) => Ok(-to_i64(v)?), // negative net change on send + }, + Some(v) => to_i64(v), // positive net change on receive + } + } + + // Create a ContractNewArgsAPI from the ContractNewArgs + fn to_api_args(&self) -> Result { + let net_change = self.get_net_change()?; + Ok(ContractNewArgsAPI { + setup_args: ContractSetupArgsAPI { + src_acct_name: match self.src_acct_name.as_ref() { + Some(v) => Some(v.to_string()), + None => None, + }, + net_change: Some(net_change), + num_participants: self.num_participants, + add_outputs: self.add_outputs, + selection_args: OutputSelectionArgs { + use_inputs: match self.use_inputs.as_ref() { + Some(v) => Some(v.to_string()), + None => None, + }, + make_outputs: self.make_outputs.clone(), + ..Default::default() + }, + proof_args: Default::default(), + }, + ..Default::default() + }) + } +} + +pub fn contract_new( + owner_api: &mut Owner, + keychain_mask: Option<&SecretKey>, + args: ContractNewArgs, +) -> Result<(), Error> +where + L: WalletLCProvider<'static, C, K>, + C: NodeClient + 'static, + K: keychain::Keychain + 'static, +{ + let contract_new_args = args.to_api_args()?; + let wallet_inst = owner_api.wallet_inst.clone(); + let config_path = owner_api.config_path(); + controller::owner_single_use(wallet_inst, keychain_mask, config_path, |api, m| { + let slate = api.contract_new(m, &contract_new_args)?; + + print_slatepack( + api, + keychain_mask, + &slate, + args.counterparty_addr.as_deref().unwrap_or(""), + args.outfile, + args.as_json, + )?; + + Ok(()) + })?; + + Ok(()) +} + +/// Sign contract command argument +#[derive(Clone)] +pub struct ContractSetupArgs { + /// Address of the counterparty + pub counterparty_addr: Option, + /// Receive amount + pub receive: Option, + /// Send amount + pub send: Option, + /// Show the resulting slatepack as JSON + pub as_json: bool, + /// Use the specified inputs (comma separated input commitments) + pub use_inputs: Option, + /// Output amounts in nanogrin (one entry per output) + pub make_outputs: Option>, + + // Future features + /// Whether we should automatically sign a receive of any value + // pub auto_receive: Option, + /// Custom fee contribution + pub fee_rate: Option, + /// Save slatepack to a specific filename + pub outfile: Option, + /// Add outputs + pub add_outputs: bool, // lock outputs early +} + +impl ContractSetupArgs { + fn get_net_change(&self) -> Result, Error> { + if self.receive.is_some() && self.send.is_some() { + return Err(Error::ArgumentError( + "Can't pass both --receive and --send parameters.".into(), + )); + } + let to_i64 = |v: u64| { + i64::try_from(v).map_err(|_| Error::ArgumentError(format!("Amount {} is too large", v))) }; - let result = api.verify_payment_proof(m, &proof); - match result { - Ok((iam_sender, iam_recipient)) => { - println!("Payment proof's signatures are valid."); - if iam_sender { - println!("The proof's sender address belongs to this wallet."); - } - if iam_recipient { - println!("The proof's recipient address belongs to this wallet."); - } - if !iam_recipient && !iam_sender { - println!( - "Neither the proof's sender nor recipient address belongs to this wallet." - ); - } - Ok(()) - } - Err(e) => { - error!("Proof not valid: {}", e); - Err(e) + let net_change = match (self.receive, self.send) { + (Some(v), _) => Some(to_i64(v)?), + (_, Some(v)) => Some(-to_i64(v)?), + (None, None) => None, + }; + Ok(net_change) + } + + // Create a ContractSetupArgsAPI from the ContractSetupArgs + fn to_api_args(&self) -> Result { + let net_change = self.get_net_change()?; + Ok(ContractSetupArgsAPI { + // TODO: num_participants is derived here. It should be an Option and read from the slate. + // Need to check no attack are possible regarding kernel fee contribution. + net_change: net_change, + add_outputs: self.add_outputs, + selection_args: OutputSelectionArgs { + use_inputs: match self.use_inputs.as_ref() { + Some(v) => Some(v.to_string()), + None => None, + }, + make_outputs: self.make_outputs.clone(), + ..Default::default() + }, + ..Default::default() + }) + } +} + +pub fn contract_sign( + owner_api: &mut Owner, + keychain_mask: Option<&SecretKey>, + args: ContractSetupArgs, + broadcast_tx: bool, +) -> Result<(), Error> +where + L: WalletLCProvider<'static, C, K>, + C: NodeClient + 'static, + K: keychain::Keychain + 'static, +{ + // Args for signing are just setup args + let contract_sign_args = args.to_api_args()?; + let wallet_inst = owner_api.wallet_inst.clone(); + let config_path = owner_api.config_path(); + controller::owner_single_use(wallet_inst, keychain_mask, config_path, |api, m| { + // Read the slatepack from stdin + println!("Paste slatepack:"); + let mut slatepack_msg = String::new(); + io::stdin().read_line(&mut slatepack_msg).map_err(|e| { + libwallet::Error::GenericError(format!("Failed to read from stdin: {}", e)) + })?; + + // Decrypt the slate, sign it and encrypt it for the next party + // TODO: Make sure you get the counterparty_addr and slate with 1 call. + let slatepack = owner::decode_slatepack_message( + api.wallet_inst.clone(), + keychain_mask, + String::from(slatepack_msg.clone()), + vec![0], + )?; + + // Encrypt the reply for --encrypt-for if given, else for the incoming slatepack's + // sender. If neither is known the incoming slate was plaintext, so reply plaintext + // (empty dest -> unencrypted slatepack). + let counterparty_addr = if let Some(addr) = args.counterparty_addr { + addr + } else if let Some(sender) = slatepack.sender { + String::try_from(&sender)? + } else { + String::new() + }; + let mut slate = owner::slate_from_slatepack_message( + api.wallet_inst.clone(), + keychain_mask, + String::from(slatepack_msg), + vec![0], + )?; + + slate = api.contract_sign(m, &slate, &contract_sign_args)?; + + print_slatepack( + api, + keychain_mask, + &slate, + &counterparty_addr, + args.outfile, + args.as_json, + )?; + + if broadcast_tx { + let is_finalized = can_finalize(&slate); + if is_finalized { + api.post_tx(keychain_mask, &slate, true)?; } } + + Ok(()) })?; + + Ok(()) +} + +#[derive(Clone)] +pub struct ContractViewArgs { + /// Slatepack file to read the contract from + pub input_file: Option, + /// Slatepack message to read the contract from + pub input_slatepack_message: Option, +} + +pub fn contract_view( + owner_api: &mut Owner, + keychain_mask: Option<&SecretKey>, + args: ContractViewArgs, +) -> Result<(), Error> +where + L: WalletLCProvider<'static, C, K>, + C: NodeClient + 'static, + K: keychain::Keychain + 'static, +{ + let (slate, _) = parse_slatepack( + owner_api, + keychain_mask, + args.input_file, + args.input_slatepack_message, + )?; + + let wallet_inst = owner_api.wallet_inst.clone(); + let config_path = owner_api.config_path(); + controller::owner_single_use(wallet_inst, keychain_mask, config_path, |api, m| { + let view = api.contract_view(m, &slate)?; + display::contract_view(&slate, &view); + Ok(()) + })?; + + Ok(()) +} + +#[derive(Clone)] +pub struct ContractRevokeArgs { + /// Id of a transaction we want to cancel + pub tx_id: u32, +} + +pub fn contract_revoke( + owner_api: &mut Owner, + keychain_mask: Option<&SecretKey>, + args: ContractRevokeArgs, +) -> Result<(), Error> +where + L: WalletLCProvider<'static, C, K>, + C: NodeClient + 'static, + K: keychain::Keychain + 'static, +{ + let wallet_inst = owner_api.wallet_inst.clone(); + let config_path = owner_api.config_path(); + controller::owner_single_use(wallet_inst, keychain_mask, config_path, |api, m| { + let slate_opt = api.contract_revoke(m, &ContractRevokeArgsAPI { tx_id: args.tx_id })?; + // TODO: replace dest="nope" with our own address and add --as-json support + if slate_opt.is_some() { + let slate_out = + prepare_slatepack(api, keychain_mask, &slate_opt.unwrap(), "nope", None)?; + println!("{}", slate_out); + } + + Ok(()) + })?; + Ok(()) } diff --git a/controller/src/controller.rs b/controller/src/controller.rs index 388c4a06b..c2f2c4e99 100644 --- a/controller/src/controller.rs +++ b/controller/src/controller.rs @@ -12,10 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! Controller for wallet.. instantiates and handles listeners (or single-run +//! 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::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::keychain::Keychain; use crate::libwallet::{ address, Error, NodeClient, NodeVersionInfo, Slate, SlatepackAddress, WalletInst, @@ -23,31 +26,38 @@ use crate::libwallet::{ }; 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 hyper::{Request, Response, StatusCode}; use qr_code::QrCode; use serde::{Deserialize, Serialize}; use serde_json; use std::collections::HashMap; use std::convert::TryFrom; +use std::io::Cursor; use std::net::SocketAddr; +use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicBool, Ordering}; 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 std::thread; use crate::apiwallet::{ - EncryptedRequest, EncryptedResponse, EncryptionErrorResponse, Foreign, + ConfigPath, EncryptedRequest, EncryptedResponse, EncryptionErrorResponse, Foreign, ForeignCheckMiddlewareFn, ForeignRpc, Owner, OwnerRpc, }; use easy_jsonrpc_mw; use easy_jsonrpc_mw::{Handler, MaybeReply}; +use grin_api::ApiBody; +use grin_wallet_config::config::{ + add_global_config_listener, get_global_config, remove_global_config_listener, +}; +use grin_wallet_impls::tor::arti::{start_tor_service, stop_tor_service}; +use grin_wallet_impls::tor::process::TorProcess; +use http_body_util::{BodyExt, Full}; +use hyper::body::Incoming; +use tokio::sync::mpsc; lazy_static! { pub static ref GRIN_OWNER_BASIC_REALM: HeaderValue = @@ -82,38 +92,20 @@ fn check_middleware( } /// initiate the tor listener -fn init_tor_listener( - wallet: Arc + 'static>>>, - keychain_mask: Arc>>, +fn init_tor_listener( + sec_key: SecretKey, + tor_dir: String, 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, -{ + tor_config: TorConfig, +) -> Result { + info!("Starting external Tor Process listener."); + 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 tor_timeout = tor_config.bootstrap_timeout().as_secs(); let mut hm_tor_bridge: HashMap = HashMap::new(); - let mut tor_timeout = 20; - if bridge.bridge_line.is_some() { - tor_timeout = 40; - let bridge_config = tor_bridge::TorBridge::try_from(bridge) + if tor_config.bridge.bridge_line.is_some() { + let bridge_config = tor_bridge::TorBridge::try_from(tor_config.bridge) .map_err(|e| Error::TorConfig(format!("{}", e).into()))?; hm_tor_bridge = bridge_config .to_hashmap() @@ -121,18 +113,14 @@ where } let mut hm_tor_poxy: HashMap = HashMap::new(); - if tor_proxy.transport.is_some() || tor_proxy.allowed_port.is_some() { - let proxy_config = tor_proxy::TorProxy::try_from(tor_proxy) + if tor_config.proxy.transport.is_some() || tor_config.proxy.allowed_port.is_some() { + let proxy_config = tor_proxy::TorProxy::try_from(tor_config.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, @@ -149,15 +137,15 @@ where .completion_percent(100) .launch() .map_err(|e| Error::TorProcess(format!("{:?}", e)))?; - Ok((process, sp_address)) + Ok(process) } /// Instantiate wallet Owner API for a single-use (command line) call /// Return a function containing a loaded API context to call pub fn owner_single_use( - wallet: Option>>>>, + wallet: Arc>>>, keychain_mask: Option<&SecretKey>, - api_context: Option<&mut Owner>, + config_path: PathBuf, f: F, ) -> Result<(), Error> where @@ -166,27 +154,14 @@ where 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(()) + f(&mut Owner::new(wallet, None, config_path), keychain_mask) } /// 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>>>, + config_path: PathBuf, keychain_mask: Option, f: F, ) -> Result<(), Error> @@ -198,6 +173,7 @@ where { f(&mut Foreign::new( wallet, + config_path, keychain_mask, Some(check_middleware), false, @@ -210,13 +186,12 @@ where /// Note keychain mask is only provided here in case the foreign listener is also being used /// in the same wallet instance pub fn owner_listener( - wallet: Arc + 'static>>>, + owner_api: &mut Owner, keychain_mask: Arc>>, addr: &str, api_secret: Option, tls_config: Option, owner_api_include_foreign: Option, - tor_config: Option, test_mode: bool, ) -> Result<(), Error> where @@ -240,12 +215,8 @@ where running_foreign = true; } - let api_handler_v3 = OwnerAPIHandlerV3::new( - wallet.clone(), - keychain_mask.clone(), - tor_config.clone(), - running_foreign, - ); + let wallet = owner_api.wallet_inst.clone(); + let api_handler_v3 = OwnerAPIHandlerV3::new(owner_api, keychain_mask.clone(), running_foreign); router .add_route("/v3/owner", Arc::new(api_handler_v3)) @@ -254,15 +225,18 @@ where // 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)); + let foreign_api_handler_v2 = ForeignAPIHandlerV2::new( + wallet, + owner_api.shared_config_path(), + keychain_mask, + test_mode, + ); 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 api_chan: (mpsc::Sender<()>, mpsc::Receiver<()>) = mpsc::channel::<()>(1); let mut apis = ApiServer::new(); warn!("Starting HTTP Owner API server at {}.", addr); @@ -280,82 +254,162 @@ where /// port and wrapping the calls pub fn foreign_listener( wallet: Arc + 'static>>>, + config_path: PathBuf, + bridge: Option, + use_tor: Option, keychain_mask: Arc>>, addr: &str, tls_config: Option, - use_tor: bool, test_mode: bool, - tor_config: Option, ) -> Result<(), Error> where L: WalletLCProvider<'static, C, K> + 'static, C: NodeClient + 'static, K: Keychain + 'static, { - // Check if wallet has been opened first - { - let mut w_lock = wallet.lock(); - let lc = w_lock.lc_provider()?; - let _ = lc.wallet_inst()?; - } - - let (tor_bridge, tor_proxy) = match tor_config.clone() { - Some(s) => (s.bridge, s.proxy), - None => (TorBridgeConfig::default(), TorProxyConfig::default()), - }; - - // need to keep in scope while the main listener is running - let (_tor_process, address) = match use_tor { - true => { - match init_tor_listener( - wallet.clone(), - keychain_mask.clone(), - addr, - tor_bridge, - tor_proxy, - ) { - Ok((tp, addr)) => (Some(tp), Some(addr)), + loop { + let (restart_tx, restart_rx) = std::sync::mpsc::channel::<()>(); + register_config_listener(&config_path, "foreign_listener", restart_tx)?; + + let tor_config = listener_tor_config(&config_path, bridge.clone(), use_tor)?; + + // Check if wallet has been opened first + let (sec_key, tor_dir, onion_address) = { + let mask = keychain_mask.lock(); + 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 sec_key = address::address_from_derivation_path(&k, &parent_key_id, 0) + .map_err(|e| Error::TorConfig(format!("{:?}", e)))?; + let tor_dir = format!("{}/tor/listener", lc.get_top_level_directory()?); + let onion_address = OnionV3Address::from_private(&sec_key.0) + .map_err(|e| Error::TorConfig(format!("{:?}", e)))?; + (sec_key, tor_dir, onion_address) + }; + + let api_handler_v2 = ForeignAPIHandlerV2::new( + wallet.clone(), + config_path.clone().into(), + keychain_mask.clone(), + test_mode, + ); + 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: (mpsc::Sender<()>, mpsc::Receiver<()>) = mpsc::channel::<()>(1); + let stop_tx = api_chan.0.clone(); + + 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.clone(), api_chan) + .map_err(|_| Error::GenericError("API thread failed to start".to_string()))?; + warn!("HTTP Foreign listener started."); + + // Need to keep external process in scope while the listener is running. + let _tor_service = if tor_config.use_tor_listener { + let use_integrated = tor_config.use_integrated.unwrap_or(false); + + let res: Result, Error> = if use_integrated { + match start_tor_service(sec_key, addr, &tor_config.clone()) { + Ok(_) => Ok(None), + Err(e) => { + error!("Error starting integrated Tor service: {}", e); + Err(e) + } + } + } else { + match init_tor_listener(sec_key, tor_dir, addr, tor_config.clone()) { + Ok(p) => Ok(Some(p)), + Err(e) => { + error!("Error starting external Tor listener: {}", e); + Err(e) + } + } + }; + match res { + Ok(service) => { + warn!( + "Starting Tor Hidden Service for API listener at address {}, binding to {}", + onion_address, addr + ); + let sp_address = SlatepackAddress::try_from(onion_address.clone())?; + let qr_string = match QrCode::new(sp_address.to_string()) { + Ok(qr) => qr.to_string(false, 3), + Err(_) => "Failed to generate QR code!".to_string(), + }; + warn!("Slatepack Address is: {}\n{}", sp_address, qr_string); + Ok(service) + } Err(e) => { - warn!("Unable to start TOR listener; Check that TOR executable is installed and on your path"); + warn!("Unable to start TOR listener"); error!("Tor Error: {}", e); - warn!("Listener will be available via HTTP only"); - (None, None) + warn!("Listener is available on {}", addr); + Err(e) } } - } - false => (None, None), - }; + } else { + warn!("Listener is available on {}", addr); + Ok(None) + }; - let api_handler_v2 = - ForeignAPIHandlerV2::new(wallet, keychain_mask, test_mode, Mutex::new(tor_config)); - let mut router = Router::new(); + // Start thread to check if restart is needed. + let restart_needed = Arc::new(AtomicBool::new(false)); + let t_restart_needed = restart_needed.clone(); + thread::spawn(move || { + if restart_rx.recv().is_ok() { + t_restart_needed.store(true, Ordering::SeqCst); + let _ = stop_tx.try_send(()); + } + }); - router - .add_route("/v2/foreign", Arc::new(api_handler_v2)) - .map_err(|_| Error::GenericError("Router failed to add route".to_string()))?; + let res = api_thread + .join() + .map_err(|e| Error::GenericError(format!("API thread panicked :{:?}", e))); - let api_chan: &'static mut (oneshot::Sender<()>, oneshot::Receiver<()>) = - Box::leak(Box::new(oneshot::channel::<()>())); + if tor_config.use_tor_listener && tor_config.use_integrated.unwrap_or(false) { + stop_tor_service(onion_address.to_string())?; + } - 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()))?; + if restart_needed.load(Ordering::Relaxed) { + continue; + } - 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); + remove_global_config_listener(&config_path, "foreign_listener"); + + return res; } +} - api_thread - .join() - .map_err(|e| Error::GenericError(format!("API thread panicked :{:?}", e))) +fn register_config_listener( + config_path: &PathBuf, + listener_id: &str, + tx: std::sync::mpsc::Sender<()>, +) -> Result<(), Error> { + get_global_config(config_path)?; + add_global_config_listener(config_path, listener_id, tx); + Ok(()) +} + +fn listener_tor_config( + config_path: &Path, + bridge: Option, + use_tor: Option, +) -> Result { + let mut tor_config = get_global_config(config_path)?.tor_config(); + if let Some(bridge) = bridge { + tor_config.bridge.bridge_line = Some(bridge); + } + if let Some(use_tor) = use_tor { + tor_config.use_tor_listener = use_tor; + } + Ok(tor_config) } /// V3 API Handler/Wrapper for owner functions, which include a secure @@ -551,19 +605,17 @@ impl OwnerV3Helpers { None }; match err_string { - Some(s) => { - return ( - true, - serde_json::json!({ - "jsonrpc": "2.0", - "id": val["id"], - "error": { - "message": s, - "code": -32099 - } - }), - ) - } + Some(s) => ( + true, + serde_json::json!({ + "jsonrpc": "2.0", + "id": val["id"], + "error": { + "message": s, + "code": -32099 + } + }), + ), None => (false, val.clone()), } } @@ -577,25 +629,24 @@ where { /// Create a new owner API handler for GET methods pub fn new( - wallet: Arc + 'static>>>, + owner_api: &Owner, keychain_mask: Arc>>, - tor_config: Option, running_foreign: bool, ) -> OwnerAPIHandlerV3 { - let owner_api = Owner::new(wallet.clone(), None); - owner_api.set_tor_config(tor_config); - let owner_api = Arc::new(owner_api); + let wallet = owner_api.wallet_inst.clone(); + let config_path = owner_api.shared_config_path(); + let owner_api = Owner::new(wallet.clone(), None, config_path); OwnerAPIHandlerV3 { wallet, - owner_api, + owner_api: Arc::new(owner_api), shared_key: Arc::new(Mutex::new(None)), - keychain_mask: keychain_mask, + keychain_mask, running_foreign, } } async fn call_api( - req: Request, + req: Request, key: Arc>>, mask: Arc>>, running_foreign: bool, @@ -661,12 +712,12 @@ where } async fn handle_post_request( - req: Request, + req: Request, key: Arc>>, mask: Arc>>, running_foreign: bool, api: Arc>, - ) -> Result, Error> { + ) -> Result, Error> { let res = Self::call_api(req, key, mask, running_foreign, api).await?; Ok(json_response_pretty(&res)) } @@ -678,7 +729,7 @@ where C: NodeClient + 'static, K: Keychain + 'static, { - fn post(&self, req: Request) -> ResponseFuture { + fn post(&self, req: Request) -> ResponseFuture { let key = self.shared_key.clone(); let mask = self.keychain_mask.clone(); let running_foreign = self.running_foreign; @@ -695,7 +746,7 @@ where }) } - fn options(&self, _req: Request) -> ResponseFuture { + fn options(&self, _req: Request) -> ResponseFuture { Box::pin(async { Ok(create_ok_response("{}")) }) } } @@ -706,14 +757,14 @@ where C: NodeClient + 'static, K: Keychain + 'static, { - /// Wallet instance + /// Wallet instance. pub wallet: Arc + 'static>>>, + /// Wallet configuration path. + pub config_path: ConfigPath, /// Keychain mask pub keychain_mask: Arc>>, /// run in doctest mode pub test_mode: bool, - /// tor config - pub tor_config: Mutex>, } impl ForeignAPIHandlerV2 @@ -725,20 +776,20 @@ where /// Create a new foreign API handler for GET methods pub fn new( wallet: Arc + 'static>>>, + config_path: ConfigPath, keychain_mask: Arc>>, test_mode: bool, - tor_config: Mutex>, ) -> ForeignAPIHandlerV2 { ForeignAPIHandlerV2 { wallet, + config_path, keychain_mask, test_mode, - tor_config, } } async fn call_api( - req: Request, + req: Request, api: Foreign<'static, L, C, K>, ) -> Result { let val: serde_json::Value = parse_body(req).await?; @@ -753,14 +804,13 @@ where } async fn handle_post_request( - req: Request, + req: Request, mask: Option, wallet: Arc + 'static>>>, + config_path: ConfigPath, test_mode: bool, - tor_config: Option, - ) -> Result, Error> { - let api = Foreign::new(wallet, mask, Some(check_middleware), test_mode); - api.set_tor_config(tor_config); + ) -> Result, Error> { + let api = Foreign::new(wallet, config_path, mask, Some(check_middleware), test_mode); let res = Self::call_api(req, api).await?; Ok(json_response_pretty(&res)) } @@ -772,14 +822,14 @@ where C: NodeClient + 'static, K: Keychain + 'static, { - fn post(&self, req: Request) -> ResponseFuture { + fn post(&self, req: Request) -> ResponseFuture { let mask = self.keychain_mask.lock().clone(); let wallet = self.wallet.clone(); let test_mode = self.test_mode; - let tor_config = self.tor_config.lock().clone(); + let config_path = self.config_path.clone(); Box::pin(async move { - match Self::handle_post_request(req, mask, wallet, test_mode, tor_config).await { + match Self::handle_post_request(req, mask, wallet, config_path, test_mode).await { Ok(v) => Ok(v), Err(e) => { error!("Request Error: {:?}", e); @@ -789,35 +839,24 @@ where }) } - fn options(&self, _req: Request) -> ResponseFuture { + fn options(&self, _req: Request) -> ResponseFuture { Box::pin(async { Ok(create_ok_response("{}")) }) } } -// Utility to serialize a struct into JSON and produce a sensible Response -// out of it. -fn _json_response(s: &T) -> Response -where - T: Serialize, -{ - match serde_json::to_string(s) { - Ok(json) => response(StatusCode::OK, json), - Err(_) => response(StatusCode::INTERNAL_SERVER_ERROR, ""), - } -} - -// pretty-printed version of above -fn json_response_pretty(s: &T) -> Response +/// Utility to serialize a struct into JSON and produce a sensible Response +/// out of it. +fn json_response_pretty(s: &T) -> Response where T: Serialize, { match serde_json::to_string_pretty(s) { - Ok(json) => response(StatusCode::OK, json), - Err(_) => response(StatusCode::INTERNAL_SERVER_ERROR, ""), + Ok(json) => response(StatusCode::OK, Full::from(json).boxed()), + Err(_) => response(StatusCode::INTERNAL_SERVER_ERROR, Full::from("").boxed()), } } -fn create_error_response(e: Error) -> Response { +fn create_error_response(e: Error) -> Response { Response::builder() .status(StatusCode::INTERNAL_SERVER_ERROR) .header("access-control-allow-origin", "*") @@ -825,11 +864,11 @@ fn create_error_response(e: Error) -> Response { "access-control-allow-headers", "Content-Type, Authorization", ) - .body(format!("{}", e).into()) + .body(Full::from(format!("{}", e)).boxed()) .unwrap() } -fn create_ok_response(json: &str) -> Response { +fn create_ok_response(json: &str) -> Response { Response::builder() .status(StatusCode::OK) .header("access-control-allow-origin", "*") @@ -838,7 +877,7 @@ fn create_ok_response(json: &str) -> Response { "Content-Type, Authorization", ) .header(hyper::header::CONTENT_TYPE, "application/json") - .body(json.to_string().into()) + .body(Full::from(json.to_string()).boxed()) .unwrap() } @@ -846,7 +885,7 @@ fn create_ok_response(json: &str) -> Response { /// /// Whenever the status code is `StatusCode::OK` the text parameter should be /// valid JSON as the content type header will be set to `application/json' -fn response>(status: StatusCode, text: T) -> Response { +fn response>(status: StatusCode, text: T) -> Response { let mut builder = Response::builder() .status(status) .header("access-control-allow-origin", "*") @@ -862,14 +901,63 @@ fn response>(status: StatusCode, text: T) -> Response { builder.body(text.into()).unwrap() } -async fn parse_body(req: Request) -> Result +async fn parse_body(req: Request) -> Result where for<'de> T: Deserialize<'de> + Send + 'static, { - let body = body::to_bytes(req.into_body()) + let body_req = req + .into_body() + .collect() .await - .map_err(|_| Error::GenericError("Failed to read request".to_string()))?; + .map_err(|e| Error::GenericError(format!("Failed to read request: {}", e)))?; + let raw = body_req.to_bytes(); - serde_json::from_reader(&body[..]) + let cursor = Cursor::new(raw); + serde_json::from_reader(cursor) .map_err(|e| Error::GenericError(format!("Invalid request body: {}", e))) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::core::global::ChainTypes; + use grin_wallet_config::config::update_global_config; + use grin_wallet_config::GlobalWalletConfig; + use std::fs; + use std::sync::mpsc::channel; + use std::time::Duration; + use uuid::Uuid; + + #[test] + fn listener_reload() { + let dir = std::env::temp_dir().join(format!("grin-wallet-listener-{}", Uuid::new_v4())); + fs::create_dir(&dir).unwrap(); + let path = dir.join("grin-wallet.toml"); + let mut config = GlobalWalletConfig::for_chain(&ChainTypes::AutomatedTesting, &path); + config + .write_to_file(path.to_str().unwrap(), false, None, None) + .unwrap(); + + let (restart_tx, restart_rx) = channel(); + register_config_listener(&path, "test_listener", restart_tx).unwrap(); + + update_global_config(&path, |config| { + config.members.tor.as_mut().unwrap().socks_proxy_addr = "127.0.0.1:59051".into(); + Ok(()) + }) + .unwrap(); + restart_rx.recv_timeout(Duration::from_secs(1)).unwrap(); + + let tor_config = + listener_tor_config(&path, Some("obfs4 test-bridge".into()), Some(false)).unwrap(); + assert_eq!(tor_config.socks_proxy_addr, "127.0.0.1:59051"); + assert_eq!( + tor_config.bridge.bridge_line.as_deref(), + Some("obfs4 test-bridge") + ); + assert!(!tor_config.use_tor_listener); + + remove_global_config_listener(&path, "test_listener"); + fs::remove_dir_all(dir).unwrap(); + } +} diff --git a/controller/src/display.rs b/controller/src/display.rs index 5668485b3..05e2ce0f0 100644 --- a/controller/src/display.rs +++ b/controller/src/display.rs @@ -15,8 +15,10 @@ use crate::core::core::FeeFields; use crate::core::core::{self, amount_to_hr_string}; use crate::core::global; +use crate::libwallet::contract::types::ContractView; use crate::libwallet::{ - AcctPathMapping, Error, OutputCommitMapping, OutputStatus, TxLogEntry, ViewWallet, WalletInfo, + AcctPathMapping, Error, OutputCommitMapping, OutputStatus, Slate, TxLogEntry, ViewWallet, + WalletInfo, }; use crate::util::ToHex; use grin_wallet_util::OnionV3Address; @@ -152,6 +154,7 @@ pub fn txs( table.set_titles(row![ bMG->"Id", bMG->"Type", + bMG->"State", bMG->"Shared Transaction Id", bMG->"Creation Time", bMG->"TTL Cutoff Height", @@ -174,6 +177,10 @@ pub fn txs( Some(m) => format!("{}", m), None => "None".to_owned(), }; + let slate_state = match t.tx_slate_state.as_ref() { + 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 { @@ -220,6 +227,7 @@ pub fn txs( table.add_row(row![ bFC->id, bFC->entry_type, + bFC->slate_state, bFC->slate_id, bFB->creation_ts, bFB->ttl_cutoff_height, @@ -628,3 +636,29 @@ pub fn payment_proof(tx: &TxLogEntry) -> Result<(), Error> { Ok(()) } + +/// Display a summary of a contract slate +pub fn contract_view(slate: &Slate, view: &ContractView) { + println!("\n____ Contract ____\n"); + let mut table = table!(); + + table.add_row(row![bFC->"Slate Id", bGC->slate.id]); + table.add_row(row![bFC->"State", bGC->slate.state]); + table.add_row(row![bFC->"Participants", bGC->view.num_participants]); + table.add_row(row![bFC->"Signatures", bGC->view.num_sigs]); + let suggested = match view.suggested_net_change { + Some(v) => amount_to_hr_string(v.unsigned_abs(), false), + None => String::from("None"), + }; + table.add_row(row![bFC->"Suggested Net Change", bGC->suggested]); + let agreed = match view.agreed_net_change { + Some(v) => amount_to_hr_string(v.unsigned_abs(), false), + None => String::from("None"), + }; + table.add_row(row![bFC->"Agreed Net Change", bGC->agreed]); + table.add_row(row![bFC->"Executed", bGC->view.is_executed]); + + table.set_format(*prettytable::format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); + table.printstd(); + println!(); +} diff --git a/controller/tests/accounts.rs b/controller/tests/accounts.rs index 744a5e14e..35413bdb6 100644 --- a/controller/tests/accounts.rs +++ b/controller/tests/accounts.rs @@ -19,6 +19,7 @@ extern crate grin_wallet_impls as impls; use grin_core as core; use grin_keychain as keychain; +use std::path::PathBuf; use self::core::global; use self::keychain::{ExtKeychain, Keychain}; @@ -78,33 +79,48 @@ fn accounts_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { 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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet2.clone(), + mask2, + PathBuf::from(test_dir), + |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 { @@ -128,17 +144,22 @@ fn accounts_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { 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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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 { @@ -149,115 +170,150 @@ fn accounts_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { 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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet2.clone(), + mask2, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet2.clone(), + mask2, + PathBuf::from(test_dir), + |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); diff --git a/controller/tests/build_chain.rs b/controller/tests/build_chain.rs index cd9a2d809..7120e4473 100644 --- a/controller/tests/build_chain.rs +++ b/controller/tests/build_chain.rs @@ -21,6 +21,7 @@ extern crate grin_wallet_impls as impls; extern crate grin_wallet_libwallet as libwallet; use grin_core as core; +use std::path::PathBuf; use self::libwallet::{InitTxArgs, Slate}; use impls::test_framework::{self, LocalWalletClient}; @@ -73,15 +74,25 @@ fn build_chain(test_dir: &'static str, block_height: usize) -> Result<(), libwal // 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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |api, _m| { + api.stop_updater()?; + Ok(()) + }, + )?; + + wallet::controller::owner_single_use( + wallet2.clone(), + mask2, + PathBuf::from(test_dir), + |api, _m| { + api.stop_updater()?; + Ok(()) + }, + )?; // few values to keep things shorter let reward = core::consensus::REWARD; @@ -94,17 +105,22 @@ fn build_chain(test_dir: &'static str, block_height: usize) -> Result<(), libwal 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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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; @@ -121,14 +137,14 @@ fn build_chain(test_dir: &'static str, block_height: usize) -> Result<(), libwal let mut slate = Slate::blank(1, false); debug!("Creating TX for {}", amount); wallet::controller::owner_single_use( - Some(wallet1.clone()), + wallet1.clone(), mask1, - None, + PathBuf::from(test_dir), |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, minimum_confirmations: 1, max_outputs: 500, num_change_outputs: 1, diff --git a/controller/tests/build_output.rs b/controller/tests/build_output.rs index 7f05a2aa7..491e916fa 100644 --- a/controller/tests/build_output.rs +++ b/controller/tests/build_output.rs @@ -25,6 +25,7 @@ use grin_util::{secp, ZeroingString}; use grin_wallet_libwallet as libwallet; use impls::test_framework::LocalWalletClient; use rand::{thread_rng, Rng}; +use std::path::PathBuf; use std::sync::atomic::Ordering; use std::thread; use std::time::Duration; @@ -66,23 +67,28 @@ fn build_output_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error 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)?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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 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 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()?; + let output = built_output.output; + assert_eq!(output.features(), features); + assert_eq!(output.commitment(), secp.commit(amount, key)?); + output.verify_proof()?; - Ok(()) - })?; + Ok(()) + }, + )?; // let logging finish stopper.store(false, Ordering::Relaxed); diff --git a/controller/tests/check.rs b/controller/tests/check.rs index 634cdd226..d52633501 100644 --- a/controller/tests/check.rs +++ b/controller/tests/check.rs @@ -26,6 +26,8 @@ use grin_wallet_libwallet as libwallet; use impls::test_framework::{self, LocalWalletClient}; use impls::{PathToSlate, SlatePutter as _}; use libwallet::{InitTxArgs, NodeClient}; +use std::cmp; +use std::path::PathBuf; use std::sync::atomic::Ordering; use std::thread; use std::time::Duration; @@ -91,20 +93,30 @@ fn scan_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { 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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet2.clone(), + mask2, + PathBuf::from(test_dir), + |api, m| { + api.create_account_path(m, "account_1")?; + api.set_active_account(m, "account_1")?; + Ok(()) + }, + )?; // Do some mining let bh = 20u64; @@ -112,26 +124,36 @@ fn scan_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { 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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |api, m| { + w1_outputs_commits = api.retrieve_outputs(m, false, true, None)?.1; + Ok(()) + }, + )?; let w1_outputs: Vec = w1_outputs_commits.into_iter().map(|m| m.output).collect(); { @@ -148,73 +170,108 @@ fn scan_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { } // 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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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_ne!(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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |api, m| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(wallet1_refreshed); + assert_eq!(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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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); @@ -411,13 +468,18 @@ fn two_wallets_one_seed_impl(test_dir: &'static str) -> Result<(), libwallet::Er 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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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!( @@ -461,33 +523,53 @@ fn two_wallets_one_seed_impl(test_dir: &'static str) -> Result<(), libwallet::Er // 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(()) - })?; + wallet::controller::owner_single_use( + wallet3.clone(), + mask3, + PathBuf::from(test_dir), + |api, m| { + api.scan(m, None, false)?; + Ok(()) + }, + )?; + + wallet::controller::owner_single_use( + wallet3.clone(), + mask3, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |api, m| { + api.scan(m, None, true)?; + Ok(()) + }, + )?; + + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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 @@ -519,26 +601,41 @@ fn two_wallets_one_seed_impl(test_dir: &'static str) -> Result<(), libwallet::Er 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(()) - })?; + wallet::controller::owner_single_use( + wallet4.clone(), + mask4, + PathBuf::from(test_dir), + |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( + wallet5.clone(), + mask5, + PathBuf::from(test_dir), + |api, m| { + api.scan(m, None, false)?; + Ok(()) + }, + )?; + + wallet::controller::owner_single_use( + wallet5.clone(), + mask5, + PathBuf::from(test_dir), + |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 @@ -574,26 +671,41 @@ fn two_wallets_one_seed_impl(test_dir: &'static str) -> Result<(), libwallet::Er ); 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(()) - })?; + wallet::controller::owner_single_use( + wallet6.clone(), + mask6, + PathBuf::from(test_dir), + |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( + wallet6.clone(), + mask6, + PathBuf::from(test_dir), + |api, m| { + api.scan(m, None, true)?; + Ok(()) + }, + )?; + + wallet::controller::owner_single_use( + wallet6.clone(), + mask6, + PathBuf::from(test_dir), + |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 @@ -622,11 +734,16 @@ fn two_wallets_one_seed_impl(test_dir: &'static str) -> Result<(), libwallet::Er 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(()) - })?; + wallet::controller::owner_single_use( + wallet7.clone(), + mask7, + PathBuf::from(test_dir), + |api, m| { + api.create_account_path(m, "account_1")?; + api.set_active_account(m, "account_1")?; + Ok(()) + }, + )?; send_to_dest!( miner.clone(), @@ -655,42 +772,57 @@ fn two_wallets_one_seed_impl(test_dir: &'static str) -> Result<(), libwallet::Er 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(()) - })?; + wallet::controller::owner_single_use( + wallet7.clone(), + mask7, + PathBuf::from(test_dir), + |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( + wallet8.clone(), + mask8, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet9.clone(), + mask9, + PathBuf::from(test_dir), + |api, m| { + api.create_account_path(m, "account_1")?; + api.set_active_account(m, "account_1")?; + Ok(()) + }, + )?; send_to_dest!( miner.clone(), @@ -716,43 +848,53 @@ fn two_wallets_one_seed_impl(test_dir: &'static str) -> Result<(), libwallet::Er 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(()) - })?; + wallet::controller::owner_single_use( + wallet9.clone(), + mask9, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet10.clone(), + mask10, + PathBuf::from(test_dir), + |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); @@ -847,6 +989,220 @@ fn output_scanning_impl(test_dir: &'static str) -> Result<(), libwallet::Error> Ok(()) } +fn multi_batch_scan_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(); + + thread::spawn(move || { + if let Err(e) = wallet_proxy.run() { + error!("Wallet Proxy error: {}", e); + } + }); + + let reward = consensus::REWARD; + let bh = 12u64; + let _ = + test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, bh as usize, false); + + let mut output_commits = vec![]; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |api, m| { + let (_, info) = api.retrieve_summary_info(m, true, 1)?; + assert_eq!(info.last_confirmed_height, bh); + assert_eq!(info.total, bh * reward); + output_commits = api.retrieve_outputs(m, false, true, None)?.1; + Ok(()) + }, + )?; + + { + wallet_inst!(wallet1, w); + let mut batch = w.batch(mask1)?; + batch.delete(&output_commits[8].output.key_id, &None)?; + batch.commit()?; + } + + let tip = { + wallet_inst!(wallet1, w); + w.w2n_client().get_chain_tip()? + }; + + let start_height = 1; + let batch_size = 5; + let mut total_pmmr_range = None; + let mut batches = 0; + let status_send_channel = None; + for h in (start_height..tip.0).step_by((batch_size + 1) as usize) { + let batch_end_height = cmp::min(tip.0, h + batch_size); + let (mut info, range) = libwallet::scan( + wallet1.clone(), + mask1, + false, + h, + batch_end_height, + start_height, + tip.0, + total_pmmr_range, + &status_send_channel, + )?; + info.hash = if batch_end_height == tip.0 { + tip.1.clone() + } else { + "".to_owned() + }; + total_pmmr_range = Some(range); + batches += 1; + + wallet_inst!(wallet1, w); + let mut batch = w.batch(mask1)?; + batch.save_last_scanned_block(info)?; + batch.commit()?; + } + assert!(batches > 1); + + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |api, m| { + let (_, restored_outputs) = api.retrieve_outputs(m, false, true, None)?; + let (_, info) = api.retrieve_summary_info(m, false, 1)?; + assert_eq!(info.total, bh * reward); + assert_eq!(restored_outputs.len(), bh as usize); + Ok(()) + }, + )?; + + stopper.store(false, Ordering::Relaxed); + thread::sleep(Duration::from_millis(200)); + Ok(()) +} + +fn restore_corrupted_outputs_across_batches_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(); + + thread::spawn(move || { + if let Err(e) = wallet_proxy.run() { + error!("Wallet Proxy error: {}", e); + } + }); + + let reward = consensus::REWARD; + let bh = 12u64; + let _ = + test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, bh as usize, false); + + let mut output_commits = vec![]; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |api, m| { + let (_, info) = api.retrieve_summary_info(m, true, 1)?; + assert_eq!(info.total, bh * reward); + output_commits = api.retrieve_outputs(m, false, true, None)?.1; + Ok(()) + }, + )?; + + { + wallet_inst!(wallet1, w); + let mut batch = w.batch(mask1)?; + batch.delete(&output_commits[2].output.key_id, &None)?; + batch.delete(&output_commits[8].output.key_id, &None)?; + batch.commit()?; + } + + let tip = { + wallet_inst!(wallet1, w); + w.w2n_client().get_chain_tip()? + }; + let status_send_channel = None; + let (info, _) = libwallet::scan( + wallet1.clone(), + mask1, + false, + 1, + 6, + 1, + tip.0, + None, + &status_send_channel, + )?; + { + wallet_inst!(wallet1, w); + { + let mut batch = w.batch(mask1)?; + batch.save_last_scanned_block(info)?; + batch.commit()?; + } + let last_scanned = w.last_scanned_block()?; + assert_eq!(last_scanned.height, 6); + assert_eq!(last_scanned.hash, ""); + } + + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |api, m| { + let (_, outputs_after_first_batch) = api.retrieve_outputs(m, false, false, None)?; + assert_eq!(outputs_after_first_batch.len(), (bh - 1) as usize); + + let (_, info) = api.retrieve_summary_info(m, true, 1)?; + let (_, restored_outputs) = api.retrieve_outputs(m, false, true, None)?; + let unique_keys = restored_outputs + .iter() + .map(|o| o.output.key_id.clone()) + .collect::>(); + + assert_eq!(info.total, bh * reward); + assert_eq!(restored_outputs.len(), bh as usize); + assert_eq!(unique_keys.len(), bh as usize); + assert!(restored_outputs + .iter() + .all(|o| o.output.status == libwallet::OutputStatus::Unspent)); + Ok(()) + }, + )?; + + stopper.store(false, Ordering::Relaxed); + thread::sleep(Duration::from_millis(200)); + Ok(()) +} + #[test] fn scan() { let test_dir = "test_output/scan"; @@ -876,3 +1232,23 @@ fn output_scanning() { } clean_output_dir(test_dir); } + +#[test] +fn multi_batch_scan() { + let test_dir = "test_output/multi_batch_scan"; + setup(test_dir); + if let Err(e) = multi_batch_scan_impl(test_dir) { + panic!("Libwallet Error: {}", e); + } + clean_output_dir(test_dir); +} + +#[test] +fn restore_corrupted_outputs_across_batches() { + let test_dir = "test_output/restore_corrupted_outputs_across_batches"; + setup(test_dir); + if let Err(e) = restore_corrupted_outputs_across_batches_impl(test_dir) { + panic!("Libwallet Error: {}", e); + } + clean_output_dir(test_dir); +} diff --git a/controller/tests/common/mod.rs b/controller/tests/common/mod.rs index 24f600d60..f9b714118 100644 --- a/controller/tests/common/mod.rs +++ b/controller/tests/common/mod.rs @@ -15,18 +15,25 @@ extern crate grin_wallet_controller as wallet; extern crate grin_wallet_impls as impls; extern crate grin_wallet_libwallet as libwallet; +extern crate log; +use grin_chain as chain; 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::keychain::{ExtKeychain, Keychain}; use self::libwallet::WalletInst; -use impls::test_framework::{LocalWalletClient, WalletProxy}; +use chain::Chain; +use grin_wallet_controller::Error; +use impls::test_framework::{self, LocalWalletClient, WalletProxy}; use impls::{DefaultLCProvider, DefaultWalletImpl}; +use std::path::PathBuf; +use std::sync::atomic::AtomicBool; use std::sync::Arc; +use std::thread; use util::secp::key::SecretKey; use util::{Mutex, ZeroingString}; @@ -76,7 +83,7 @@ macro_rules! open_wallet_and_add { 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(); + std::fs::remove_dir_all(test_dir).unwrap(); } } @@ -98,7 +105,7 @@ pub fn create_wallet_proxy( test_dir: &str, ) -> WalletProxy< '_, - DefaultLCProvider<'_, LocalWalletClient, ExtKeychain>, + DefaultLCProvider, LocalWalletClient, ExtKeychain, > { @@ -117,7 +124,7 @@ pub fn create_local_wallet( Box< dyn WalletInst< 'static, - DefaultLCProvider<'static, LocalWalletClient, ExtKeychain>, + DefaultLCProvider, LocalWalletClient, ExtKeychain, >, @@ -129,7 +136,7 @@ pub fn create_local_wallet( let mut wallet = Box::new(DefaultWalletImpl::::new(client).unwrap()) as Box< dyn WalletInst< - DefaultLCProvider<'static, LocalWalletClient, ExtKeychain>, + DefaultLCProvider, LocalWalletClient, ExtKeychain, >, @@ -156,7 +163,7 @@ pub fn open_local_wallet( Box< dyn WalletInst< 'static, - DefaultLCProvider<'static, LocalWalletClient, ExtKeychain>, + DefaultLCProvider, LocalWalletClient, ExtKeychain, >, @@ -168,7 +175,7 @@ pub fn open_local_wallet( let mut wallet = Box::new(DefaultWalletImpl::::new(client).unwrap()) as Box< dyn WalletInst< - DefaultLCProvider<'static, LocalWalletClient, ExtKeychain>, + DefaultLCProvider, LocalWalletClient, ExtKeychain, >, @@ -180,3 +187,144 @@ pub fn open_local_wallet( .unwrap(); (Arc::new(Mutex::new(wallet)), mask) } + +// Creates the given number of wallets and spawns a thread that runs the wallet proxy +#[allow(dead_code)] +pub fn create_wallets( + wallets_def: Vec>, // a vector of boolean that represent whether we mine into a wallet + test_dir: &'static str, +) -> Result< + ( + Vec<( + Arc< + Mutex< + Box< + dyn WalletInst< + 'static, + DefaultLCProvider, + LocalWalletClient, + ExtKeychain, + >, + >, + >, + >, + Option, + )>, // wallets + Arc, // chain + Arc, // stopper + u64, // block height + ), + 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(); + + let mut wallets = vec![]; + for i in 0..wallets_def.len() { + let name = format!("wallet{}", i + 1); + let wclient = LocalWalletClient::new(&name, wallet_proxy.tx.clone()); + let (wallet1, mask1) = create_local_wallet( + test_dir, + &name, + None, + // $seed_phrase.clone(), + wclient.clone(), + true, + ); + wallet_proxy.add_wallet( + &name, + wclient.get_send_instance(), + wallet1.clone(), + mask1.clone(), + ); + wallets.push((wallet1, mask1)); + } + // Set the wallet proxy listener running + thread::spawn(move || { + if let Err(e) = wallet_proxy.run() { + log::error!("Wallet Proxy error: {}", e); + } + }); + + // // Mine values into wallets + // // few values to keep things shorter + let reward = core::consensus::REWARD; + let mut bh = 0u64; + + for (idx, accs) in wallets_def.iter().enumerate() { + let wallet1 = wallets[idx].0.clone(); + let mask1 = wallets[idx].1.as_ref(); + + for (acc_idx, (acc_name, num_mined_blocks)) in accs.iter().enumerate() { + // create the account + if acc_name.to_string() != "default" { + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |api, m| { + let new_path = api.create_account_path(m, acc_name)?; + assert_eq!( + new_path, + ExtKeychain::derive_key_id(2, acc_idx as u32, 0, 0, 0) // NOTE: default should always be at 0 and is already created + ); + Ok(()) + }, + )?; + } + + // Get some mining done + if *num_mined_blocks == 0 { + continue; + } + { + wallet_inst!(wallet1, w); + w.set_parent_key_id_by_name(acc_name)?; + } + let _ = test_framework::award_blocks_to_wallet( + &chain, + wallet1.clone(), + mask1, + *num_mined_blocks as usize, + false, + ); + bh += num_mined_blocks; + + // Sanity check wallet 1 contents + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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, num_mined_blocks * reward); + Ok(()) + }, + )?; + } + + // Sanity check the number of accounts + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |api, m| { + let accounts = api.accounts(m)?; + assert_eq!(accounts.len(), accs.len()); + Ok(()) + }, + )?; + // Set the account on the wallet to "default" + { + wallet_inst!(wallet1, w); + w.set_parent_key_id_by_name("default")?; + } + } + + Ok((wallets, chain, stopper, bh)) +} diff --git a/controller/tests/config_api.rs b/controller/tests/config_api.rs new file mode 100644 index 000000000..5936a7f4f --- /dev/null +++ b/controller/tests/config_api.rs @@ -0,0 +1,228 @@ +// Copyright 2026 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_wallet_api as api; +extern crate grin_wallet_config as config; +extern crate grin_wallet_impls as impls; +extern crate grin_wallet_libwallet as libwallet; + +use config::config::{get_global_config, reload_global_config}; +use config::GlobalWalletConfig; +use impls::test_framework::LocalWalletClient; +use libwallet::{InitTxArgs, InitTxSendArgs}; +use std::fs; +use std::path::{Path, PathBuf}; +use std::sync::atomic::Ordering; + +mod common; +use common::{clean_output_dir, create_wallet_proxy, setup}; + +fn snapshot(path: &Path) -> Vec<(PathBuf, Vec)> { + fn collect(root: &Path, path: &Path, files: &mut Vec<(PathBuf, Vec)>) { + for entry in fs::read_dir(path).unwrap() { + let entry = entry.unwrap(); + let path = entry.path(); + if path.is_dir() { + collect(root, &path, files); + } else if path.file_name().and_then(|name| name.to_str()) != Some("lock.mdb") { + files.push(( + path.strip_prefix(root).unwrap().into(), + fs::read(path).unwrap(), + )); + } + } + } + + let mut files = vec![]; + collect(path, path, &mut files); + files.sort_by(|left, right| left.0.cmp(&right.0)); + files +} + +#[test] +fn tor_disable() { + let test_dir = "test_output/config_api_tor"; + setup(test_dir); + let mut wallet_proxy = create_wallet_proxy(test_dir); + create_wallet_and_add!( + client, + wallet, + mask, + test_dir, + "wallet", + None, + &mut wallet_proxy, + false + ); + let path = PathBuf::from(test_dir).join("grin-wallet.toml"); + let mut config = + GlobalWalletConfig::for_chain(&grin_core::global::ChainTypes::AutomatedTesting, &path); + config + .write_to_file(path.to_str().unwrap(), false, None, None) + .unwrap(); + reload_global_config(&path).unwrap(); + let mut contents = fs::read_to_string(&path).unwrap(); + let tor = contents.find("[tor]").unwrap(); + let logging = contents.find("[logging]").unwrap(); + contents.replace_range(tor..logging, ""); + let contents = contents.replace("api_listen_port = 3415", "api_listen_port = 3416"); + fs::write(&path, contents).unwrap(); + + let owner = api::Owner::new(wallet.clone(), None, path.clone()); + owner.set_tor_config(None).unwrap(); + + let stored_config = GlobalWalletConfig::new(path.clone()).unwrap(); + assert_eq!(stored_config.members.wallet.api_listen_port, 3416); + let stored = stored_config.tor_config(); + let cached = get_global_config(&path).unwrap().tor_config(); + assert!(!stored.use_tor_listener); + assert_eq!(stored.skip_send_attempt, Some(true)); + assert_eq!( + stored.send_config_dir, + stored_config.members.wallet.data_file_dir + ); + assert_eq!(cached, stored); + + drop(owner); + drop(wallet); + drop(wallet_proxy); + clean_output_dir(test_dir); +} + +#[test] +fn send_preflight() { + let test_dir = "test_output/config_api_send"; + setup(test_dir); + let mut wallet_proxy = create_wallet_proxy(test_dir); + create_wallet_and_add!( + client, + wallet, + mask, + test_dir, + "wallet", + None, + &mut wallet_proxy, + false + ); + let owner = api::Owner::new( + wallet.clone(), + None, + PathBuf::from(test_dir).join("missing.toml"), + ); + let mask = mask.as_ref(); + let (_, before_txs) = owner.retrieve_txs(mask, false, None, None, None).unwrap(); + assert!(before_txs.is_empty()); + let wallet_data = PathBuf::from(test_dir).join("wallet/wallet_data"); + let before_data = snapshot(&wallet_data); + assert!(before_data.iter().any(|(path, _)| path.starts_with("db"))); + let args = InitTxArgs { + amount: 1, + send_args: Some(InitTxSendArgs { + dest: "tgrin1xtxavwfgs48ckf3gk8wwgcndmn0nt4tvkl8a7ltyejjcy2mc6nfs9gm2lp".into(), + post_tx: false, + fluff: false, + skip_tor: Some(true), + }), + ..Default::default() + }; + + let error = owner.init_send_tx(mask, args).unwrap_err(); + assert!(error.to_string().contains("Configuration file not found")); + + let (_, after_txs) = owner.retrieve_txs(mask, false, None, None, None).unwrap(); + assert!(after_txs.is_empty()); + assert_eq!(snapshot(&wallet_data), before_data); + + drop(owner); + drop(wallet); + drop(wallet_proxy); + clean_output_dir(test_dir); +} + +#[test] +fn directory_change() { + let test_dir = "test_output/config_api_directory"; + setup(test_dir); + let mut wallet_proxy = create_wallet_proxy(test_dir); + create_wallet_and_add!( + client, + wallet, + mask, + test_dir, + "wallet", + None, + &mut wallet_proxy, + false + ); + let old_dir = PathBuf::from(test_dir).join("wallet"); + let old_config = old_dir.join("grin-wallet.toml"); + let new_dir = PathBuf::from(test_dir).join("other"); + let owner = api::Owner::new(wallet.clone(), None, old_config.clone()); + + let error = owner + .set_top_level_directory(new_dir.to_str().unwrap()) + .unwrap_err(); + assert!(error.to_string().contains("Close the wallet")); + assert_eq!(owner.config_path(), old_config); + assert_eq!( + PathBuf::from(owner.get_top_level_directory().unwrap()), + old_dir + ); + + owner.close_wallet(None).unwrap(); + owner.updater_running.store(true, Ordering::Relaxed); + let error = owner + .set_top_level_directory(new_dir.to_str().unwrap()) + .unwrap_err(); + assert!(error.to_string().contains("Stop the updater")); + owner.updater_running.store(false, Ordering::Relaxed); + + fs::create_dir_all(&new_dir).unwrap(); + let new_config = new_dir.join("grin-wallet.toml"); + let mut config = GlobalWalletConfig::for_chain( + &grin_core::global::ChainTypes::AutomatedTesting, + &new_config, + ); + config + .write_to_file(new_config.to_str().unwrap(), false, None, None) + .unwrap(); + reload_global_config(&new_config).unwrap(); + let expected_port = config.members.wallet.api_listen_port + 1; + config.members.wallet.api_listen_port = expected_port; + config + .write_to_file(new_config.to_str().unwrap(), false, None, None) + .unwrap(); + + owner + .set_top_level_directory(new_dir.to_str().unwrap()) + .unwrap(); + assert_eq!(owner.config_path(), new_config); + assert_eq!( + get_global_config(&owner.config_path()) + .unwrap() + .members + .wallet + .api_listen_port, + expected_port + ); + assert_eq!( + PathBuf::from(owner.get_top_level_directory().unwrap()), + new_dir + ); + + drop(owner); + drop(wallet); + drop(wallet_proxy); + clean_output_dir(test_dir); +} diff --git a/controller/tests/contract_accounts.rs b/controller/tests/contract_accounts.rs new file mode 100644 index 000000000..fb2fd8284 --- /dev/null +++ b/controller/tests/contract_accounts.rs @@ -0,0 +1,398 @@ +// 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. + +//! Test a wallet doing contracts with different accounts +// #[macro_use] +extern crate grin_wallet_controller as wallet; +extern crate grin_wallet_impls as impls; +extern crate log; + +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}; +use libwallet::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI}; +use libwallet::{Slate, SlateState}; +use std::sync::atomic::Ordering; +use std::thread; +use std::time::Duration; + +#[macro_use] +mod common; +use common::{clean_output_dir, create_wallets, setup}; +use std::path::PathBuf; + +/// contract accounts testing (mostly the same as accounts.rs) +fn contract_accounts_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { + // create two wallets with some extra accounts and don't mine anything in them + let (wallets, chain, stopper, _bh) = create_wallets( + vec![ + vec![ + ("default", 0), + ("account1", 0), + ("account2", 0), + ("account3", 0), + ], + vec![("default", 0), ("listener_account", 0)], + ], + test_dir, + ) + .unwrap(); + let wallet1 = wallets[0].0.clone(); + let mask1 = wallets[0].1.as_ref(); + let wallet2 = wallets[1].0.clone(); + let mask2 = wallets[1].1.as_ref(); + + // few values to keep things shorter + let reward = core::consensus::REWARD; + let cm = global::coinbase_maturity(); // assume all testing precedes soft fork height + + // 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( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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 + { + wallet_inst!(wallet1, w); + w.set_parent_key_id_by_name("account1")?; + } + + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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(()) + }, + )?; + + // TODO: check what send_tx_slate_direct call does in accounts.rs test + // TODO: check that you can't call send on the default account because you have no funds + + // Send a tx from wallet1::account1 -> wallet2::listener_account + { + wallet_inst!(wallet1, w); + w.set_parent_key_id_by_name("account1")?; + } + + let mut slate = Slate::blank(0, true); // this gets overriden below + + let mut sender_address = None; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |api, m| { + // Send wallet inititates a standard transaction with --send=5 + let args = &ContractNewArgsAPI { + setup_args: ContractSetupArgsAPI { + net_change: Some(-5_000_000_000), + ..Default::default() + }, + ..Default::default() + }; + slate = api.contract_new(m, args)?; + sender_address = Some(api.get_slatepack_address(mask1, 0)?.pub_key); + Ok(()) + }, + )?; + assert_eq!(slate.state, SlateState::Standard1); + + let mut recipient_address = None; + wallet::controller::owner_single_use( + wallet2.clone(), + mask2, + PathBuf::from(test_dir), + |api, m| { + // Receive wallet calls --receive=5 + let args = &mut ContractSetupArgsAPI { + net_change: Some(5_000_000_000), + ..Default::default() + }; + // Proofs are opt-in; enable and supply the sender address. + args.proof_args.suppress_proof = false; + args.proof_args.sender_address = sender_address; + slate = api.contract_sign(m, &slate, args)?; + recipient_address = Some(api.get_slatepack_address(mask2, 0)?.pub_key); + Ok(()) + }, + )?; + assert_eq!(slate.state, SlateState::Standard2); + + // Send wallet finalizes and posts + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |api, m| { + let args = &ContractSetupArgsAPI { + ..Default::default() + }; + slate = api.contract_sign(m, &slate, args)?; + Ok(()) + }, + )?; + assert_eq!(slate.state, SlateState::Standard3); + + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |api, m| { + api.post_tx(m, &slate, false)?; + Ok(()) + }, + )?; + + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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( + wallet2.clone(), + mask2, + PathBuf::from(test_dir), + |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( + wallet2.clone(), + mask2, + PathBuf::from(test_dir), + |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 wallet_contract_accounts() -> Result<(), libwallet::Error> { + let test_dir = "test_output/contract_accounts"; + setup(test_dir); + contract_accounts_impl(test_dir)?; + clean_output_dir(test_dir); + Ok(()) +} + +/// A contract referencing an unknown account must error rather than silently +/// falling back to the active account. +fn contract_unknown_account_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { + let (wallets, _chain, stopper, _bh) = + create_wallets(vec![vec![("default", 4)]], test_dir).unwrap(); + let wallet1 = wallets[0].0.clone(); + let mask1 = wallets[0].1.as_ref(); + + let res = wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |api, m| { + let args = &ContractNewArgsAPI { + setup_args: ContractSetupArgsAPI { + net_change: Some(-1_000_000_000), + src_acct_name: Some("does_not_exist".to_string()), + ..Default::default() + }, + ..Default::default() + }; + api.contract_new(m, args)?; + Ok(()) + }, + ); + assert!(res.is_err(), "expected an error for an unknown account"); + + stopper.store(false, Ordering::Relaxed); + thread::sleep(Duration::from_millis(200)); + Ok(()) +} + +#[test] +fn wallet_contract_unknown_account() -> Result<(), libwallet::Error> { + let test_dir = "test_output/contract_unknown_account"; + setup(test_dir); + contract_unknown_account_impl(test_dir)?; + clean_output_dir(test_dir); + Ok(()) +} + +/// A contract with no net change (neither --send nor --receive) must error +/// rather than panic on an unwrap of the optional field. +fn contract_missing_net_change_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { + let (wallets, _chain, stopper, _bh) = + create_wallets(vec![vec![("default", 1)]], test_dir).unwrap(); + let wallet1 = wallets[0].0.clone(); + let mask1 = wallets[0].1.as_ref(); + + let res = wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |api, m| { + let args = &ContractNewArgsAPI { + setup_args: ContractSetupArgsAPI { + net_change: None, + ..Default::default() + }, + ..Default::default() + }; + api.contract_new(m, args)?; + Ok(()) + }, + ); + assert!(res.is_err(), "expected an error when net_change is missing"); + + stopper.store(false, Ordering::Relaxed); + thread::sleep(Duration::from_millis(200)); + Ok(()) +} + +#[test] +fn wallet_contract_missing_net_change() -> Result<(), libwallet::Error> { + let test_dir = "test_output/contract_missing_net_change"; + setup(test_dir); + contract_missing_net_change_impl(test_dir)?; + clean_output_dir(test_dir); + Ok(()) +} diff --git a/controller/tests/contract_accounts_switch.rs b/controller/tests/contract_accounts_switch.rs new file mode 100644 index 000000000..d8fabe3a5 --- /dev/null +++ b/controller/tests/contract_accounts_switch.rs @@ -0,0 +1,258 @@ +// 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. + +//! Test a wallet doing contracts with different accounts and switching between them +// #[macro_use] +extern crate grin_wallet_controller as wallet; +extern crate grin_wallet_impls as impls; +extern crate log; + +use grin_core as core; +use grin_wallet_libwallet as libwallet; +use libwallet::contract::my_fee_contribution; +use libwallet::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI}; +use libwallet::{Slate, SlateState}; +use std::sync::atomic::Ordering; +use std::thread; +use std::time::Duration; + +#[macro_use] +mod common; +use common::{clean_output_dir, create_wallets, setup}; +use std::path::PathBuf; + +/// contract accounts testing when switching between accounts during transaction building +fn contract_accounts_switch_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { + // create two wallets with some extra accounts and don't mine anything in them + let (wallets, _chain, stopper, _bh) = create_wallets( + vec![ + vec![("default", 0), ("account1", 1), ("account2", 2)], + vec![("default", 0), ("account1", 3), ("account2", 4)], + ], + test_dir, + ) + .unwrap(); + let wallet1 = wallets[0].0.clone(); + let mask1 = wallets[0].1.as_ref(); + let wallet2 = wallets[1].0.clone(); + let mask2 = wallets[1].1.as_ref(); + + let reward = core::consensus::REWARD; + + // wallet1::account1 should have 1 in account1 (1 spendable) + { + wallet_inst!(wallet1, w); + w.set_parent_key_id_by_name("account1")?; + } + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |api, m| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(wallet1_refreshed); + assert_eq!(wallet1_info.last_confirmed_height, 10); + assert_eq!(wallet1_info.total, 1 * reward); + assert_eq!(wallet1_info.amount_currently_spendable, 1 * reward); + let (_, txs) = api.retrieve_txs(m, true, None, None, None)?; + assert_eq!(txs.len(), 1); + Ok(()) + }, + )?; + + // wallet1::account2 should have 2 in account1 (2 spendable) + { + wallet_inst!(wallet1, w); + w.set_parent_key_id_by_name("account2")?; + } + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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, 3); + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(wallet1_refreshed); + assert_eq!(wallet1_info.last_confirmed_height, 10); + assert_eq!(wallet1_info.total, 2 * reward); + assert_eq!(wallet1_info.amount_currently_spendable, 2 * reward); + // check tx log as well + let (_, txs) = api.retrieve_txs(m, true, None, None, None)?; + assert_eq!(txs.len(), 2); + Ok(()) + }, + )?; + + // Make a transaction by sending 5 coins from wallet1::account1 -> wallet2::account2 + { + wallet_inst!(wallet1, w); + w.set_parent_key_id_by_name("account1")?; + } + + let mut slate = Slate::blank(0, true); // this gets overriden below + + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |api, m| { + // Send wallet inititates a standard transaction with --send=5 + let args = &ContractNewArgsAPI { + setup_args: ContractSetupArgsAPI { + net_change: Some(-5_000_000_000), + ..Default::default() + }, + ..Default::default() + }; + slate = api.contract_new(m, args)?; + Ok(()) + }, + )?; + assert_eq!(slate.state, SlateState::Standard1); + + // Receiver gets their coins on account2 where they can payjoin + { + wallet_inst!(wallet2, w); + w.set_parent_key_id_by_name("account2")?; + } + wallet::controller::owner_single_use( + wallet2.clone(), + mask2, + PathBuf::from(test_dir), + |api, m| { + // Receive wallet calls --receive=5 + let args = &mut ContractSetupArgsAPI { + net_change: Some(5_000_000_000), + ..Default::default() + }; + args.proof_args.suppress_proof = true; + slate = api.contract_sign(m, &slate, args)?; + Ok(()) + }, + )?; + assert_eq!(slate.state, SlateState::Standard2); + + // Switch account for wallet1 to account2 and finish the transaction (should use account1 to complete) + { + wallet_inst!(wallet1, w); + w.set_parent_key_id_by_name("account2")?; + } + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |api, m| { + let args = &ContractSetupArgsAPI { + ..Default::default() + }; + slate = api.contract_sign(m, &slate, args)?; + Ok(()) + }, + )?; + assert_eq!(slate.state, SlateState::Standard3); + + // post tx and mine a block to wallet1::account2 + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |api, m| { + api.post_tx(m, &slate, false)?; + Ok(()) + }, + )?; + + // The currently set account (account2) should not be affected by this transaction because they weren't a part of it, + // but it did mine a block and pick the transaction fees + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |api, m| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(wallet1_refreshed); + assert_eq!(wallet1_info.last_confirmed_height, 11); + assert_eq!( + wallet1_info.total, + 3 * reward + core::libtx::tx_fee(2, 2, 1) // we have received a block reward and the tx fee (payjoin) + ); + assert_eq!(wallet1_info.amount_currently_spendable, 2 * reward); + let (_, txs) = api.retrieve_txs(m, true, None, None, None)?; + assert_eq!(txs.len(), 3); + Ok(()) + }, + )?; + + // Switch to wallet1::account1 and check that it sent 5 coins + { + wallet_inst!(wallet1, w); + w.set_parent_key_id_by_name("account1")?; + } + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |api, m| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(wallet1_refreshed); + assert_eq!(wallet1_info.last_confirmed_height, 11); + assert_eq!( + wallet1_info.total, + 1 * reward - 5_000_000_000 - my_fee_contribution(1, 1, 1, 2)?.fee() // we subtract also our fee contribution + ); + let (_, txs) = api.retrieve_txs(m, true, None, None, None)?; + assert_eq!(txs.len(), 2); + Ok(()) + }, + )?; + + // Switch to wallet2::account2 and check that it received 5 coins + { + wallet_inst!(wallet2, w); + w.set_parent_key_id_by_name("account2")?; + } + wallet::controller::owner_single_use( + wallet2.clone(), + mask2, + PathBuf::from(test_dir), + |api, m| { + let (wallet2_refreshed, wallet2_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(wallet2_refreshed); + assert_eq!(wallet2_info.last_confirmed_height, 11); + assert_eq!( + wallet2_info.total, + 4 * reward + 5_000_000_000 - my_fee_contribution(1, 1, 1, 2)?.fee() // we subtract also our fee contribution for a payjoin + ); + let (_, txs) = api.retrieve_txs(m, true, None, None, None)?; + assert_eq!(txs.len(), 5); + Ok(()) + }, + )?; + + // let logging finish + stopper.store(false, Ordering::Relaxed); + thread::sleep(Duration::from_millis(200)); + + Ok(()) +} + +#[test] +fn wallet_contract_accounts_switch() -> Result<(), libwallet::Error> { + let test_dir = "test_output/contract_accounts_switch"; + setup(test_dir); + contract_accounts_switch_impl(test_dir)?; + clean_output_dir(test_dir); + Ok(()) +} diff --git a/controller/tests/contract_early_lock.rs b/controller/tests/contract_early_lock.rs new file mode 100644 index 000000000..85eb028cd --- /dev/null +++ b/controller/tests/contract_early_lock.rs @@ -0,0 +1,119 @@ +// 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. + +//! Test a wallet doing contract early lock when using --add-outputs +// #[macro_use] +extern crate grin_wallet_controller as wallet; +extern crate grin_wallet_impls as impls; +extern crate log; + +use grin_wallet_libwallet as libwallet; + +use grin_core::consensus; +use libwallet::contract::my_fee_contribution; +use libwallet::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI}; +use libwallet::{OutputStatus, Slate, SlateState}; +use std::sync::atomic::Ordering; +use std::thread; +use std::time::Duration; + +#[macro_use] +mod common; +use common::{clean_output_dir, create_wallets, setup}; +use std::path::PathBuf; + +/// contract new with --add-outputs +fn contract_early_lock_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { + // create a single wallet and mine 5 blocks + let (wallets, _chain, stopper, _bh) = + create_wallets(vec![vec![("default", 5)]], test_dir).unwrap(); + let send_wallet = wallets[0].0.clone(); + let send_mask = wallets[0].1.as_ref(); + + // Confirm all our outputs are unspent + wallet::controller::owner_single_use( + send_wallet.clone(), + send_mask, + PathBuf::from(test_dir), + |api, m| { + let (_, commits) = api.retrieve_outputs(m, true, false, None)?; + for commit in commits.iter() { + assert_eq!(commit.output.status, OutputStatus::Unspent); + } + Ok(()) + }, + )?; + + let mut slate = Slate::blank(0, false); // this gets overriden below + + // Call contract 'new' with --add-outputs + wallet::controller::owner_single_use( + send_wallet.clone(), + send_mask, + PathBuf::from(test_dir), + |api, m| { + // Send wallet inititates a standard transaction with --send=80 + let args = &ContractNewArgsAPI { + setup_args: ContractSetupArgsAPI { + net_change: Some(-80_000_000_000), + num_participants: 2, + add_outputs: true, + ..Default::default() + }, + ..Default::default() + }; + slate = api.contract_new(m, args)?; + Ok(()) + }, + )?; + assert_eq!(slate.state, SlateState::Standard1); + + // Assert we locked 2 inputs and prepared an unconfirmed change output + wallet::controller::owner_single_use( + send_wallet.clone(), + send_mask, + PathBuf::from(test_dir), + |api, m| { + let (_, commits) = api.retrieve_outputs(m, true, false, None)?; + // we locked the first two coinbase outputs + assert_eq!(commits[0].output.status, OutputStatus::Locked); + assert_eq!(commits[1].output.status, OutputStatus::Locked); + // we added a new unconfirmed change output + let new_output_idx = commits.len() - 1; + assert_eq!( + commits[new_output_idx].output.status, + OutputStatus::Unconfirmed + ); + assert_eq!( + commits[new_output_idx].output.value, + 2 * consensus::REWARD - 80_000_000_000 - my_fee_contribution(2, 1, 1, 2)?.fee() + ); + Ok(()) + }, + )?; + + // let logging finish + stopper.store(false, Ordering::Relaxed); + thread::sleep(Duration::from_millis(200)); + + Ok(()) +} + +#[test] +fn wallet_contract_early_lock_tx() -> Result<(), libwallet::Error> { + let test_dir = "test_output/contract_early_lock_tx"; + setup(test_dir); + contract_early_lock_tx_impl(test_dir)?; + clean_output_dir(test_dir); + Ok(()) +} diff --git a/controller/tests/contract_early_proofs_rsr.rs b/controller/tests/contract_early_proofs_rsr.rs new file mode 100644 index 000000000..28a19383f --- /dev/null +++ b/controller/tests/contract_early_proofs_rsr.rs @@ -0,0 +1,250 @@ +// Copyright 2023 The Grin Developers +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Development and testing of early payment proofs, restricted at the moment +//! to contract-style transactions for experimental purposes +//! +//! https://github.com/mimblewimble/grin-rfcs/pull/70 +//! +//! + +extern crate grin_wallet_controller as wallet; +extern crate grin_wallet_impls as impls; +extern crate log; + +use grin_wallet_libwallet as libwallet; + +use impls::test_framework::{self}; +use libwallet::contract::my_fee_contribution; +use libwallet::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI}; +use libwallet::{Slate, SlateState, Slatepacker, SlatepackerArgs, TxLogEntryType}; +use std::sync::atomic::Ordering; +use std::thread; +use std::time::Duration; + +#[macro_use] +mod common; +use common::{clean_output_dir, create_wallets, setup}; +use std::path::PathBuf; + +/// Development + Tests of early payment proof functionality - RSR workflow +fn contract_early_proofs_rsr_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { + // create two wallets and mine 4 blocks in each (we want both to have balance to get a payjoin) + let (wallets, chain, stopper, mut bh) = + create_wallets(vec![vec![("default", 4)], vec![("default", 4)]], test_dir).unwrap(); + let send_wallet = wallets[0].0.clone(); + let send_mask = wallets[0].1.as_ref(); + let recv_wallet = wallets[1].0.clone(); + let recv_mask = wallets[1].1.as_ref(); + + let mut slate = Slate::blank(0, true); // this gets overriden below + + let mut sender_address = None; + // Get sender address explicitly + wallet::controller::owner_single_use( + send_wallet.clone(), + send_mask, + PathBuf::from(test_dir), + |api, _m| { + sender_address = Some(api.get_slatepack_address(send_mask, 0)?.pub_key); + Ok(()) + }, + )?; + + let mut recipient_address = None; + wallet::controller::owner_single_use( + recv_wallet.clone(), + recv_mask, + PathBuf::from(test_dir), + |api, m| { + // Receive wallet (invoice) calls --receive=5 + let args = &mut ContractNewArgsAPI { + setup_args: ContractSetupArgsAPI { + net_change: Some(5_000_000_000), + ..Default::default() + }, + ..Default::default() + }; + // Proofs are opt-in; enable and supply the sender address. + args.setup_args.proof_args.suppress_proof = false; + args.setup_args.proof_args.sender_address = sender_address; + slate = api.contract_new(m, args)?; + recipient_address = Some(api.get_slatepack_address(recv_mask, 0)?.pub_key); + Ok(()) + }, + )?; + + assert_eq!(slate.state, SlateState::Invoice1); + println!("I1 State slate: {}", slate); + + // Serialize slate into slatepack + let slatepacker_args = SlatepackerArgs { + sender: None, + recipients: vec![], + dec_key: None, + }; + + let slate_packer = Slatepacker::new(slatepacker_args); + let slate_packed = slate_packer.create_slatepack(&slate).unwrap(); + + let slate_unpacked = slate_packer.get_slate(&slate_packed).unwrap(); + println!("I2 Slate unpacked: {}", slate_unpacked); + + wallet::controller::owner_single_use( + send_wallet.clone(), + send_mask, + PathBuf::from(test_dir), + |api, m| { + // Sending wallet (invoice) signs + let args = &ContractSetupArgsAPI { + net_change: Some(-5_000_000_000), + ..Default::default() + }; + slate = api.contract_sign(m, &slate_unpacked, args)?; + Ok(()) + }, + )?; + println!("I2 State slate: {}", slate); + + assert_eq!(slate.state, SlateState::Invoice2); + + // Send wallet finalizes and posts + wallet::controller::owner_single_use( + recv_wallet.clone(), + recv_mask, + PathBuf::from(test_dir), + |api, m| { + let args = &ContractSetupArgsAPI { + ..Default::default() + }; + slate = api.contract_sign(m, &slate, args)?; + Ok(()) + }, + )?; + assert_eq!(slate.state, SlateState::Invoice3); + + wallet::controller::owner_single_use( + send_wallet.clone(), + send_mask, + PathBuf::from(test_dir), + |api, m| { + api.post_tx(m, &slate, false)?; + Ok(()) + }, + )?; + bh += 1; + + let _ = + test_framework::award_blocks_to_wallet(&chain, send_wallet.clone(), send_mask, 3, false); + bh += 3; + + // Assert changes in receive wallet + wallet::controller::owner_single_use( + recv_wallet.clone(), + recv_mask, + PathBuf::from(test_dir), + |api, m| { + let (_, wallet_info) = api.retrieve_summary_info(m, true, 1)?; + let (refreshed, txs) = api.retrieve_txs(m, true, None, None, None)?; + assert_eq!(wallet_info.last_confirmed_height, bh); + assert!(refreshed); + assert_eq!(txs.len(), 5); // 4 mined and 1 received + let tx_log = txs[4].clone(); + assert_eq!(tx_log.tx_type, TxLogEntryType::TxReceived); + assert_eq!(tx_log.amount_credited, 5_000_000_000); + assert_eq!(tx_log.amount_debited, 0); + assert_eq!(tx_log.num_inputs, 1); + assert_eq!(tx_log.num_outputs, 1); + let expected_fees_paid = Some(my_fee_contribution(1, 1, 1, 2)?); + assert_eq!(tx_log.fee, expected_fees_paid); + assert_eq!( + wallet_info.amount_currently_spendable, + 4 * 60_000_000_000 + 5_000_000_000 - expected_fees_paid.unwrap().fee() // we expect the balance of 4 mined blocks + 5 Grin - fees paid + ); + Ok(()) + }, + )?; + + // Assert changes in send wallet + wallet::controller::owner_single_use( + send_wallet.clone(), + send_mask, + PathBuf::from(test_dir), + |api, m| { + let (_, wallet_info) = api.retrieve_summary_info(m, true, 1)?; + let (refreshed, txs) = api.retrieve_txs(m, true, None, None, None)?; + assert_eq!(wallet_info.last_confirmed_height, bh); + assert!(refreshed); + assert_eq!(txs.len() as u64, bh - 4 + 1); // send wallet didn't mine 4 blocks and made 1 tx + let tx_log = txs[txs.len() - 5].clone(); // TODO: why -5 and not -4? + assert_eq!(tx_log.tx_type, TxLogEntryType::TxSent); + assert_eq!(tx_log.amount_credited, 0); + assert_eq!(tx_log.amount_debited, 5_000_000_000); + assert_eq!(tx_log.num_inputs, 1); + assert_eq!(tx_log.num_outputs, 1); + assert_eq!(tx_log.fee, Some(my_fee_contribution(1, 1, 1, 2)?)); + Ok(()) + }, + )?; + + let mut invoice_proof = None; + // Now some time has passed, sender retrieves and verify the payment proof + wallet::controller::owner_single_use( + send_wallet.clone(), + send_mask, + PathBuf::from(test_dir), + |api, _m| { + // Extract the stored data as an invoice proof + invoice_proof = + Some(api.retrieve_payment_proof_invoice(send_mask, true, None, Some(slate.id))?); + Ok(()) + }, + )?; + + let invoice_proof = invoice_proof.unwrap(); + let invoice_proof_json = serde_json::to_string(&invoice_proof).unwrap(); + + // Should have all proof fields filled out + println!("INVOICE PROOF: {}", invoice_proof_json); + + wallet::controller::foreign_single_use( + recv_wallet.clone(), + PathBuf::from(test_dir), + recv_mask.cloned(), + |api| { + let mut proof = serde_json::from_str(&invoice_proof_json).unwrap(); + api.verify_payment_proof_invoice(recipient_address.as_ref().unwrap(), &proof)?; + // tweak something and it shouldn't verify + proof.amount = 400000; + let retval = + api.verify_payment_proof_invoice(recipient_address.as_ref().unwrap(), &proof); + assert!(retval.is_err()); + Ok(()) + }, + )?; + + // let logging finish + stopper.store(false, Ordering::Relaxed); + thread::sleep(Duration::from_millis(200)); + + Ok(()) +} + +#[test] +fn contract_early_proofs_rsr() -> Result<(), libwallet::Error> { + let test_dir = "test_output/contract_early_proofs_rsr"; + setup(test_dir); + contract_early_proofs_rsr_test_impl(test_dir)?; + clean_output_dir(test_dir); + Ok(()) +} diff --git a/controller/tests/contract_early_proofs_srs.rs b/controller/tests/contract_early_proofs_srs.rs new file mode 100644 index 000000000..37faf0d95 --- /dev/null +++ b/controller/tests/contract_early_proofs_srs.rs @@ -0,0 +1,225 @@ +// Copyright 2023 The Grin Developers +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Development and testing of early payment proofs, restricted at the moment +//! to contract-style transactions for experimental purposes +//! +//! https://github.com/mimblewimble/grin-rfcs/pull/70 +//! +//! + +extern crate grin_wallet_controller as wallet; +extern crate grin_wallet_impls as impls; +extern crate log; + +use grin_wallet_libwallet as libwallet; + +use impls::test_framework::{self}; +use libwallet::contract::my_fee_contribution; +use libwallet::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI}; +use libwallet::{Slate, SlateState, TxLogEntryType}; +use std::sync::atomic::Ordering; +use std::thread; +use std::time::Duration; + +#[macro_use] +mod common; +use common::{clean_output_dir, create_wallets, setup}; +use std::path::PathBuf; + +/// Development + Tests of early payment proof functionality +fn contract_early_proofs_srs_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { + // create two wallets and mine 4 blocks in each (we want both to have balance to get a payjoin) + let (wallets, chain, stopper, mut bh) = + create_wallets(vec![vec![("default", 4)], vec![("default", 4)]], test_dir).unwrap(); + let send_wallet = wallets[0].0.clone(); + let send_mask = wallets[0].1.as_ref(); + let recv_wallet = wallets[1].0.clone(); + let recv_mask = wallets[1].1.as_ref(); + + let mut slate = Slate::blank(0, true); // this gets overriden below + + let mut sender_address = None; + wallet::controller::owner_single_use( + send_wallet.clone(), + send_mask, + PathBuf::from(test_dir), + |api, m| { + // Send wallet inititates a standard transaction with --send=5 + let args = &ContractNewArgsAPI { + setup_args: ContractSetupArgsAPI { + net_change: Some(-5_000_000_000), + ..Default::default() + }, + ..Default::default() + }; + slate = api.contract_new(m, args)?; + sender_address = Some(api.get_slatepack_address(send_mask, 0)?.pub_key); + Ok(()) + }, + )?; + assert_eq!(slate.state, SlateState::Standard1); + + let mut recipient_address = None; + wallet::controller::owner_single_use( + recv_wallet.clone(), + recv_mask, + PathBuf::from(test_dir), + |api, m| { + // Receive wallet calls --receive=5 + let args = &mut ContractSetupArgsAPI { + net_change: Some(5_000_000_000), + ..Default::default() + }; + // Proofs are opt-in; enable and supply the sender address. + args.proof_args.suppress_proof = false; + args.proof_args.sender_address = sender_address; + slate = api.contract_sign(m, &slate, args)?; + recipient_address = Some(api.get_slatepack_address(recv_mask, 0)?.pub_key); + Ok(()) + }, + )?; + assert_eq!(slate.state, SlateState::Standard2); + + // Send wallet finalizes and posts + //let mut sender_part_sig = None; + wallet::controller::owner_single_use( + send_wallet.clone(), + send_mask, + PathBuf::from(test_dir), + |api, m| { + let args = &ContractSetupArgsAPI { + ..Default::default() + }; + + slate = api.contract_sign(m, &slate, args)?; + Ok(()) + }, + )?; + assert_eq!(slate.state, SlateState::Standard3); + + wallet::controller::owner_single_use( + send_wallet.clone(), + send_mask, + PathBuf::from(test_dir), + |api, m| { + api.post_tx(m, &slate, false)?; + Ok(()) + }, + )?; + bh += 1; + + let _ = + test_framework::award_blocks_to_wallet(&chain, send_wallet.clone(), send_mask, 3, false); + bh += 3; + + // Assert changes in receive wallet + wallet::controller::owner_single_use( + recv_wallet.clone(), + recv_mask, + PathBuf::from(test_dir), + |api, m| { + let (_, wallet_info) = api.retrieve_summary_info(m, true, 1)?; + let (refreshed, txs) = api.retrieve_txs(m, true, None, None, None)?; + assert_eq!(wallet_info.last_confirmed_height, bh); + assert!(refreshed); + assert_eq!(txs.len(), 5); // 4 mined and 1 received + let tx_log = txs[4].clone(); + assert_eq!(tx_log.tx_type, TxLogEntryType::TxReceived); + assert_eq!(tx_log.amount_credited, 5_000_000_000); + assert_eq!(tx_log.amount_debited, 0); + assert_eq!(tx_log.num_inputs, 1); + assert_eq!(tx_log.num_outputs, 1); + let expected_fees_paid = Some(my_fee_contribution(1, 1, 1, 2)?); + assert_eq!(tx_log.fee, expected_fees_paid); + assert_eq!( + wallet_info.amount_currently_spendable, + 4 * 60_000_000_000 + 5_000_000_000 - expected_fees_paid.unwrap().fee() // we expect the balance of 4 mined blocks + 5 Grin - fees paid + ); + Ok(()) + }, + )?; + + // Assert changes in send wallet + wallet::controller::owner_single_use( + send_wallet.clone(), + send_mask, + PathBuf::from(test_dir), + |api, m| { + let (_, wallet_info) = api.retrieve_summary_info(m, true, 1)?; + let (refreshed, txs) = api.retrieve_txs(m, true, None, None, None)?; + assert_eq!(wallet_info.last_confirmed_height, bh); + assert!(refreshed); + assert_eq!(txs.len() as u64, bh - 4 + 1); // send wallet didn't mine 4 blocks and made 1 tx + let tx_log = txs[txs.len() - 5].clone(); // TODO: why -5 and not -4? + assert_eq!(tx_log.tx_type, TxLogEntryType::TxSent); + assert_eq!(tx_log.amount_credited, 0); + assert_eq!(tx_log.amount_debited, 5_000_000_000); + assert_eq!(tx_log.num_inputs, 1); + assert_eq!(tx_log.num_outputs, 1); + assert_eq!(tx_log.fee, Some(my_fee_contribution(1, 1, 1, 2)?)); + Ok(()) + }, + )?; + + let mut invoice_proof = None; + // Now some time has passed, sender retrieves and verify the payment proof + wallet::controller::owner_single_use( + send_wallet.clone(), + send_mask, + PathBuf::from(test_dir), + |api, _m| { + // Extract the stored data as an invoice proof + invoice_proof = + Some(api.retrieve_payment_proof_invoice(send_mask, true, None, Some(slate.id))?); + Ok(()) + }, + )?; + + let invoice_proof = invoice_proof.unwrap(); + let invoice_proof_json = serde_json::to_string(&invoice_proof).unwrap(); + + // Should have all proof fields filled out + println!("INVOICE PROOF: {}", invoice_proof_json); + + wallet::controller::foreign_single_use( + recv_wallet.clone(), + PathBuf::from(test_dir), + recv_mask.cloned(), + |api| { + let mut proof = serde_json::from_str(&invoice_proof_json).unwrap(); + api.verify_payment_proof_invoice(recipient_address.as_ref().unwrap(), &proof)?; + // tweak something and it shouldn't verify + proof.amount = 400000; + let retval = + api.verify_payment_proof_invoice(recipient_address.as_ref().unwrap(), &proof); + assert!(retval.is_err()); + Ok(()) + }, + )?; + + // let logging finish + stopper.store(false, Ordering::Relaxed); + thread::sleep(Duration::from_millis(200)); + + Ok(()) +} + +#[test] +fn contract_early_proofs_srs() -> Result<(), libwallet::Error> { + let test_dir = "test_output/contract_early_proofs_src"; + setup(test_dir); + contract_early_proofs_srs_test_impl(test_dir)?; + clean_output_dir(test_dir); + Ok(()) +} diff --git a/controller/tests/contract_revoke.rs b/controller/tests/contract_revoke.rs new file mode 100644 index 000000000..f7c151f4b --- /dev/null +++ b/controller/tests/contract_revoke.rs @@ -0,0 +1,236 @@ +// 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. + +//! Test contract revoke, including when a different account is active than the +//! one that contributed (and locked) the inputs. +// #[macro_use] +extern crate grin_wallet_controller as wallet; +extern crate grin_wallet_impls as impls; +extern crate log; + +use grin_wallet_libwallet as libwallet; + +use libwallet::contract::types::{ContractNewArgsAPI, ContractRevokeArgsAPI, ContractSetupArgsAPI}; +use libwallet::{OutputStatus, Slate, SlateState}; +use std::sync::atomic::Ordering; +use std::thread; +use std::time::Duration; + +#[macro_use] +mod common; +use common::{clean_output_dir, create_wallets, setup}; +use std::path::PathBuf; + +/// Revoke a contract while a different account is active than the one that locked +/// the inputs. The cancel + self-spend must still target the inputs' account. +fn contract_revoke_other_account_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { + // One wallet: empty "default" account plus a funded "account1". + let (wallets, _chain, stopper, _bh) = + create_wallets(vec![vec![("default", 0), ("account1", 4)]], test_dir).unwrap(); + let wallet1 = wallets[0].0.clone(); + let mask1 = wallets[0].1.as_ref(); + + // Send (with early lock) from account1, locking one of its inputs. + { + wallet_inst!(wallet1, w); + w.set_parent_key_id_by_name("account1")?; + } + let mut slate = Slate::blank(0, true); + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |api, m| { + let args = &ContractNewArgsAPI { + setup_args: ContractSetupArgsAPI { + net_change: Some(-1_000_000_000), + num_participants: 2, + add_outputs: true, + ..Default::default() + }, + ..Default::default() + }; + slate = api.contract_new(m, args)?; + Ok(()) + }, + )?; + assert_eq!(slate.state, SlateState::Standard1); + + // Grab the tx id and confirm an input is locked under account1. + let mut tx_id = 0; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |api, m| { + let (_, txs) = api.retrieve_txs(m, true, None, None, None)?; + tx_id = txs.last().unwrap().id; + let (_, outs) = api.retrieve_outputs(m, true, false, None)?; + assert!(outs.iter().any(|o| o.output.status == OutputStatus::Locked)); + Ok(()) + }, + )?; + + // Switch the active account to "default" — different from the inputs' account. + { + wallet_inst!(wallet1, w); + w.set_parent_key_id_by_name("default")?; + } + + // Revoke. With the active account wrong, this must still cancel + self-spend the + // account1 transaction (it derives the account from the locked input). + let mut revoked = None; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |api, m| { + revoked = api.contract_revoke(m, &ContractRevokeArgsAPI { tx_id })?; + Ok(()) + }, + )?; + assert!( + revoked.is_some(), + "revoke should produce a self-spend slate" + ); + + // Back on account1: the original tx is cancelled and no input is left locked. + { + wallet_inst!(wallet1, w); + w.set_parent_key_id_by_name("account1")?; + } + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |api, m| { + let query = libwallet::RetrieveTxQueryArgs { + exclude_cancelled: Some(false), + ..Default::default() + }; + let (_, txs) = api.retrieve_txs(m, true, None, None, Some(query))?; + let tx = txs.iter().find(|t| t.id == tx_id).unwrap(); + assert!( + format!("{:?}", tx.tx_type).contains("Cancelled"), + "original tx should be cancelled, was {:?}", + tx.tx_type + ); + Ok(()) + }, + )?; + + stopper.store(false, Ordering::Relaxed); + thread::sleep(Duration::from_millis(200)); + Ok(()) +} + +#[test] +fn wallet_contract_revoke_other_account() -> Result<(), libwallet::Error> { + let test_dir = "test_output/contract_revoke_other_account"; + setup(test_dir); + contract_revoke_other_account_impl(test_dir)?; + clean_output_dir(test_dir); + Ok(()) +} + +/// A revoke interrupted right after the cancel (inputs already unlocked, no self-spend +/// built yet) must be resumable: a second revoke detects the now-Unspent inputs that are +/// still tagged with the cancelled tx and still produces the self-spend, rather than +/// silently doing nothing. +fn contract_revoke_resume_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { + let (wallets, _chain, stopper, _bh) = + create_wallets(vec![vec![("default", 4)]], test_dir).unwrap(); + let wallet1 = wallets[0].0.clone(); + let mask1 = wallets[0].1.as_ref(); + + // Send (with early lock), locking an input under the contract tx. + let mut slate = Slate::blank(0, true); + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |api, m| { + let args = &ContractNewArgsAPI { + setup_args: ContractSetupArgsAPI { + net_change: Some(-1_000_000_000), + num_participants: 2, + add_outputs: true, + ..Default::default() + }, + ..Default::default() + }; + slate = api.contract_new(m, args)?; + Ok(()) + }, + )?; + assert_eq!(slate.state, SlateState::Standard1); + + let mut tx_id = 0; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |api, m| { + let (_, txs) = api.retrieve_txs(m, true, None, None, None)?; + tx_id = txs.last().unwrap().id; + Ok(()) + }, + )?; + + // Simulate a revoke that crashed right after cancelling: cancel the tx (inputs become + // Unspent, still tagged with tx_id) but do NOT build the self-spend. + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |api, m| { + api.cancel_tx(m, Some(tx_id), None)?; + let (_, outs) = api.retrieve_outputs(m, true, false, None)?; + assert!( + outs.iter().any(|o| o.output.status == OutputStatus::Unspent + && o.output.tx_log_entry == Some(tx_id)), + "input should be Unspent but still tagged with the cancelled tx" + ); + Ok(()) + }, + )?; + + // Resume: revoke must still produce the self-spend from the now-Unspent input. + let mut revoked = None; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |api, m| { + revoked = api.contract_revoke(m, &ContractRevokeArgsAPI { tx_id })?; + Ok(()) + }, + )?; + assert!( + revoked.is_some(), + "interrupted revoke should resume and produce a self-spend slate" + ); + + stopper.store(false, Ordering::Relaxed); + thread::sleep(Duration::from_millis(200)); + Ok(()) +} + +#[test] +fn wallet_contract_revoke_resume() -> Result<(), libwallet::Error> { + let test_dir = "test_output/contract_revoke_resume"; + setup(test_dir); + contract_revoke_resume_impl(test_dir)?; + clean_output_dir(test_dir); + Ok(()) +} diff --git a/controller/tests/contract_rsr.rs b/controller/tests/contract_rsr.rs new file mode 100644 index 000000000..88a9ea543 --- /dev/null +++ b/controller/tests/contract_rsr.rs @@ -0,0 +1,181 @@ +// 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. + +//! Test a wallet doing contract RSR flow +// #[macro_use] +extern crate grin_wallet_controller as wallet; +extern crate grin_wallet_impls as impls; +extern crate log; + +use grin_wallet_libwallet as libwallet; + +use impls::test_framework::{self}; +use libwallet::contract::my_fee_contribution; +use libwallet::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI}; +use libwallet::{Slate, SlateState, TxLogEntryType}; +use std::sync::atomic::Ordering; +use std::thread; +use std::time::Duration; + +#[macro_use] +mod common; +use common::{clean_output_dir, create_wallets, setup}; +use std::path::PathBuf; + +/// contract RSR flow +fn contract_rsr_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { + // create two wallets and mine 4 blocks in each (we want both to have balance to get a payjoin) + let (wallets, chain, stopper, mut bh) = + create_wallets(vec![vec![("default", 4)], vec![("default", 4)]], test_dir).unwrap(); + let send_wallet = wallets[0].0.clone(); + let send_mask = wallets[0].1.as_ref(); + let recv_wallet = wallets[1].0.clone(); + let recv_mask = wallets[1].1.as_ref(); + + let mut slate = Slate::blank(0, true); // this gets overriden below + + wallet::controller::owner_single_use( + recv_wallet.clone(), + recv_mask, + PathBuf::from(test_dir), + |api, m| { + // Receive wallet inititates an invoice transaction with --receive=5 + let args = &mut ContractNewArgsAPI { + setup_args: ContractSetupArgsAPI { + net_change: Some(5_000_000_000), + ..Default::default() + }, + ..Default::default() + }; + slate = api.contract_new(m, args)?; + Ok(()) + }, + )?; + assert_eq!(slate.state, SlateState::Invoice1); + + wallet::controller::owner_single_use( + send_wallet.clone(), + send_mask, + PathBuf::from(test_dir), + |api, m| { + // Send Wallet calls --send=5 + let args = &ContractSetupArgsAPI { + net_change: Some(-5_000_000_000), + ..Default::default() + }; + slate = api.contract_sign(m, &slate, args)?; + Ok(()) + }, + )?; + assert_eq!(slate.state, SlateState::Invoice2); + + // Receive wallet finalizes and posts + wallet::controller::owner_single_use( + recv_wallet.clone(), + recv_mask, + PathBuf::from(test_dir), + |api, m| { + let args = &mut ContractSetupArgsAPI { + ..Default::default() + }; + slate = api.contract_sign(m, &slate, args)?; + Ok(()) + }, + )?; + assert_eq!(slate.state, SlateState::Invoice3); + + // Send wallet posts so receive wallet doesn't get the mined amount + wallet::controller::owner_single_use( + send_wallet.clone(), + send_mask, + PathBuf::from(test_dir), + |api, m| { + api.post_tx(m, &slate, false)?; + Ok(()) + }, + )?; + bh += 1; + + let _ = + test_framework::award_blocks_to_wallet(&chain, send_wallet.clone(), send_mask, 3, false); + bh += 3; + + // Assert changes in receive wallet + wallet::controller::owner_single_use( + recv_wallet.clone(), + recv_mask, + PathBuf::from(test_dir), + |api, m| { + let (_, wallet_info) = api.retrieve_summary_info(m, true, 1)?; + let (refreshed, txs) = api.retrieve_txs(m, true, None, None, None)?; + assert_eq!(wallet_info.last_confirmed_height, bh); + assert!(refreshed); + assert_eq!(txs.len(), 5); // 4 mined and 1 received + let tx_log = txs[4].clone(); + assert_eq!(tx_log.tx_type, TxLogEntryType::TxReceived); + assert_eq!(tx_log.amount_credited, 5_000_000_000); + assert_eq!(tx_log.amount_debited, 0); + assert_eq!(tx_log.num_inputs, 1); + assert_eq!(tx_log.num_outputs, 1); + let expected_fees_paid = Some(my_fee_contribution(1, 1, 1, 2)?); + assert_eq!(tx_log.fee, expected_fees_paid); + assert_eq!( + wallet_info.amount_currently_spendable, + 4 * 60_000_000_000 + 5_000_000_000 - expected_fees_paid.unwrap().fee() // we expect the balance of 4 mined blocks + 5 Grin - fees paid + ); + // println!("txlogentry: {:#?}", tx_log); + // println!("wallet info: {:#?}", wallet_info); + // let (validated, commits) = api.retrieve_outputs(m, true, false, Some(tx_log.id))?; + // println!("commits: {:#?}", commits); + // panic!("lala"); + Ok(()) + }, + )?; + + // Assert changes in send wallet + wallet::controller::owner_single_use( + send_wallet.clone(), + send_mask, + PathBuf::from(test_dir), + |api, m| { + let (_, wallet_info) = api.retrieve_summary_info(m, true, 1)?; + let (refreshed, txs) = api.retrieve_txs(m, true, None, None, None)?; + assert_eq!(wallet_info.last_confirmed_height, bh); + assert!(refreshed); + assert_eq!(txs.len() as u64, bh - 4 + 1); // send_wallet didn't mine 4 blocks and made 1 tx + let tx_log = txs[txs.len() - 5].clone(); // TODO: why -5 and not -4? + assert_eq!(tx_log.tx_type, TxLogEntryType::TxSent); + assert_eq!(tx_log.amount_credited, 0); + assert_eq!(tx_log.amount_debited, 5_000_000_000); + assert_eq!(tx_log.num_inputs, 1); + assert_eq!(tx_log.num_outputs, 1); + assert_eq!(tx_log.fee, Some(my_fee_contribution(1, 1, 1, 2)?)); + Ok(()) + }, + )?; + + // let logging finish + stopper.store(false, Ordering::Relaxed); + thread::sleep(Duration::from_millis(200)); + + Ok(()) +} + +#[test] +fn wallet_contract_rsr_tx() -> Result<(), libwallet::Error> { + let test_dir = "test_output/contract_rsr_tx"; + setup(test_dir); + contract_rsr_tx_impl(test_dir)?; + clean_output_dir(test_dir); + Ok(()) +} diff --git a/controller/tests/contract_self_spend.rs b/controller/tests/contract_self_spend.rs new file mode 100644 index 000000000..2909b6f3a --- /dev/null +++ b/controller/tests/contract_self_spend.rs @@ -0,0 +1,132 @@ +// 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. + +//! Test a wallet doing contract self-spend flow +// #[macro_use] +extern crate grin_wallet_controller as wallet; +extern crate grin_wallet_impls as impls; +extern crate log; + +use grin_wallet_libwallet as libwallet; + +use impls::test_framework::{self}; +use libwallet::contract::my_fee_contribution; +use libwallet::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI}; +use libwallet::{Slate, SlateState, TxLogEntryType}; +use std::sync::atomic::Ordering; +use std::thread; +use std::time::Duration; + +#[macro_use] +mod common; +use common::{clean_output_dir, create_wallets, setup}; +use std::path::PathBuf; + +/// contract self-spend flow +fn contract_self_spend_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { + // create a single wallet and mine 4 blocks + let (wallets, chain, stopper, mut bh) = + create_wallets(vec![vec![("default", 4)]], test_dir).unwrap(); + let send_wallet = wallets[0].0.clone(); + let send_mask = wallets[0].1.as_ref(); + + let mut slate = Slate::blank(0, true); // this gets overriden below + + wallet::controller::owner_single_use( + send_wallet.clone(), + send_mask, + PathBuf::from(test_dir), + |api, m| { + // Send wallet inititates a standard transaction with --send=0 + let args = &ContractNewArgsAPI { + setup_args: ContractSetupArgsAPI { + net_change: Some(0), + num_participants: 1, + ..Default::default() + }, + ..Default::default() + }; + slate = api.contract_new(m, args)?; + Ok(()) + }, + )?; + assert_eq!(slate.state, SlateState::Standard1); + + // Send wallet finalizes and posts + wallet::controller::owner_single_use( + send_wallet.clone(), + send_mask, + PathBuf::from(test_dir), + |api, m| { + let args = &ContractSetupArgsAPI { + ..Default::default() + }; + slate = api.contract_sign(m, &slate, args)?; + Ok(()) + }, + )?; + // In the case of a self-spend, we just finish the slate when it's in the Standard2 state + assert_eq!(slate.state, SlateState::Standard2); + + wallet::controller::owner_single_use( + send_wallet.clone(), + send_mask, + PathBuf::from(test_dir), + |api, m| { + api.post_tx(m, &slate, false)?; + Ok(()) + }, + )?; + bh += 1; + + let _ = + test_framework::award_blocks_to_wallet(&chain, send_wallet.clone(), send_mask, 3, false); + bh += 3; + + // Assert changes in send wallet + wallet::controller::owner_single_use( + send_wallet.clone(), + send_mask, + PathBuf::from(test_dir), + |api, m| { + let (_, wallet_info) = api.retrieve_summary_info(m, true, 1)?; + let (refreshed, txs) = api.retrieve_txs(m, true, None, None, None)?; + assert_eq!(wallet_info.last_confirmed_height, bh); + assert!(refreshed); + assert_eq!(txs.len() as u64, bh + 1); // send wallet didn't mine 4 blocks and made 1 tx + let tx_log = txs[txs.len() - 5].clone(); // TODO: why -5 and not -4? + assert_eq!(tx_log.tx_type, TxLogEntryType::TxSelfSpend); + assert_eq!(tx_log.amount_credited, 0); + assert_eq!(tx_log.amount_debited, 0); + assert_eq!(tx_log.num_inputs, 1); + assert_eq!(tx_log.num_outputs, 1); + assert_eq!(tx_log.fee, Some(my_fee_contribution(1, 1, 1, 1)?)); + Ok(()) + }, + )?; + + // let logging finish + stopper.store(false, Ordering::Relaxed); + thread::sleep(Duration::from_millis(200)); + + Ok(()) +} + +#[test] +fn wallet_contract_self_spend_tx() -> Result<(), libwallet::Error> { + let test_dir = "test_output/contract_self_spend_tx"; + setup(test_dir); + contract_self_spend_tx_impl(test_dir)?; + clean_output_dir(test_dir); + Ok(()) +} diff --git a/controller/tests/contract_self_spend_cancel.rs b/controller/tests/contract_self_spend_cancel.rs new file mode 100644 index 000000000..661258008 --- /dev/null +++ b/controller/tests/contract_self_spend_cancel.rs @@ -0,0 +1,240 @@ +// 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. + +//! Test a wallet doing contract self-spend flow +// #[macro_use] +extern crate grin_wallet_controller as wallet; +extern crate grin_wallet_impls as impls; +extern crate log; + +use grin_wallet_libwallet as libwallet; + +use libwallet::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI}; +use libwallet::{Slate, SlateState, TxLogEntryType}; +use std::sync::atomic::Ordering; +use std::thread; +use std::time::Duration; + +#[macro_use] +mod common; +use common::{clean_output_dir, create_wallets, setup}; +use std::path::PathBuf; + +/// contract self-spend flow +fn contract_self_spend_cancel_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { + // create a single wallet and mine 4 blocks + let (wallets, _chain, stopper, _bh) = + create_wallets(vec![vec![("default", 4)]], test_dir).unwrap(); + let send_wallet = wallets[0].0.clone(); + let send_mask = wallets[0].1.as_ref(); + + let mut slate = Slate::blank(0, true); // this gets overriden below + + wallet::controller::owner_single_use( + send_wallet.clone(), + send_mask, + PathBuf::from(test_dir), + |api, m| { + // Send wallet initiates a standard transaction with --send=0 + let args = &ContractNewArgsAPI { + setup_args: ContractSetupArgsAPI { + net_change: Some(0), + num_participants: 1, + ..Default::default() + }, + ..Default::default() + }; + slate = api.contract_new(m, args)?; + Ok(()) + }, + )?; + assert_eq!(slate.state, SlateState::Standard1); + + // Send wallet finalizes and posts + wallet::controller::owner_single_use( + send_wallet.clone(), + send_mask, + PathBuf::from(test_dir), + |api, m| { + api.cancel_tx(m, None, Some(slate.id))?; + Ok(()) + }, + )?; + + // Assert tx log has been cancelled + wallet::controller::owner_single_use( + send_wallet.clone(), + send_mask, + PathBuf::from(test_dir), + |api, m| { + let query_args = libwallet::RetrieveTxQueryArgs { + exclude_cancelled: Some(false), + ..Default::default() + }; + let (refreshed, txs) = api.retrieve_txs(m, true, None, None, Some(query_args))?; + assert!(refreshed); + assert_eq!(txs.len() as u64, 5); // send wallet didn't mine 4 blocks and made 1 tx + let tx_log = txs[4].clone(); // TODO: why -5 and not -4? + assert_eq!(tx_log.tx_type, TxLogEntryType::TxSelfSpendCancelled); + Ok(()) + }, + )?; + + // let logging finish + stopper.store(false, Ordering::Relaxed); + thread::sleep(Duration::from_millis(200)); + + Ok(()) +} + +/// Find the stored transaction a signed contract wrote +fn find_stored_tx(test_dir: &str) -> std::path::PathBuf { + fn walk(dir: &std::path::Path, out: &mut Vec) { + if let Ok(entries) = std::fs::read_dir(dir) { + for e in entries.filter_map(|e| e.ok()) { + let p = e.path(); + if p.is_dir() { + walk(&p, out); + } else if p.extension().map(|x| x == "grintx").unwrap_or(false) { + out.push(p); + } + } + } + } + let mut found = vec![]; + walk(std::path::Path::new(test_dir), &mut found); + assert_eq!(found.len(), 1, "expected one stored tx under {}", test_dir); + found.pop().unwrap() +} + +/// save_step writes the signed transaction to a file after the wallet state batch has +/// committed, so that write can fail with the tx log entry and the input locks already +/// persisted. The documented way out is to cancel, which does not read the stored tx; +/// this checks that holds by removing the file and cancelling. +fn contract_self_spend_cancel_missing_stored_tx_impl( + test_dir: &'static str, +) -> Result<(), libwallet::Error> { + let (wallets, _chain, stopper, _bh) = + create_wallets(vec![vec![("default", 4)]], test_dir).unwrap(); + let send_wallet = wallets[0].0.clone(); + let send_mask = wallets[0].1.as_ref(); + + let mut slate = Slate::blank(0, true); + wallet::controller::owner_single_use( + send_wallet.clone(), + send_mask, + PathBuf::from(test_dir), + |api, m| { + let args = &ContractNewArgsAPI { + setup_args: ContractSetupArgsAPI { + net_change: Some(0), + num_participants: 1, + ..Default::default() + }, + ..Default::default() + }; + slate = api.contract_new(m, args)?; + Ok(()) + }, + )?; + + // Signing a self-spend completes the slate, so the transaction is stored + wallet::controller::owner_single_use( + send_wallet.clone(), + send_mask, + PathBuf::from(test_dir), + |api, m| { + let args = &ContractSetupArgsAPI { + ..Default::default() + }; + slate = api.contract_sign(m, &slate, args)?; + Ok(()) + }, + )?; + assert_eq!(slate.state, SlateState::Standard2); + + // Signing locked our inputs, so the cancel below has something to release, and the + // entry records where the transaction was stored, as a standard send does + wallet::controller::owner_single_use( + send_wallet.clone(), + send_mask, + PathBuf::from(test_dir), + |api, m| { + let (_, info) = api.retrieve_summary_info(m, true, 1)?; + assert!(info.amount_locked > 0); + let (_, txs) = api.retrieve_txs(m, true, None, Some(slate.id), None)?; + assert_eq!( + txs.last().unwrap().stored_tx, + Some(format!("{}.grintx", slate.id)) + ); + Ok(()) + }, + )?; + + // Stand in for the file write having failed. The transaction has not been posted: + // contract_sign returns before the caller broadcasts. + let stored = find_stored_tx(test_dir); + std::fs::remove_file(&stored).unwrap(); + + // Cancelling still releases the inputs + wallet::controller::owner_single_use( + send_wallet.clone(), + send_mask, + PathBuf::from(test_dir), + |api, m| { + api.cancel_tx(m, None, Some(slate.id))?; + Ok(()) + }, + )?; + + wallet::controller::owner_single_use( + send_wallet.clone(), + send_mask, + PathBuf::from(test_dir), + |api, m| { + let query_args = libwallet::RetrieveTxQueryArgs { + exclude_cancelled: Some(false), + ..Default::default() + }; + let (_, txs) = api.retrieve_txs(m, true, None, None, Some(query_args))?; + let tx_log = txs.last().unwrap(); + assert_eq!(tx_log.tx_type, TxLogEntryType::TxSelfSpendCancelled); + // The spendable balance is back to the mined total + let (_, info) = api.retrieve_summary_info(m, true, 1)?; + assert_eq!(info.amount_locked, 0); + Ok(()) + }, + )?; + + stopper.store(false, Ordering::Relaxed); + thread::sleep(Duration::from_millis(200)); + Ok(()) +} + +#[test] +fn wallet_contract_self_spend_cancel() -> Result<(), libwallet::Error> { + let test_dir = "test_output/contract_self_spend_cancel"; + setup(test_dir); + contract_self_spend_cancel_impl(test_dir)?; + clean_output_dir(test_dir); + Ok(()) +} + +#[test] +fn wallet_contract_self_spend_cancel_missing_stored_tx() -> Result<(), libwallet::Error> { + let test_dir = "test_output/contract_self_spend_cancel_missing_stored_tx"; + setup(test_dir); + contract_self_spend_cancel_missing_stored_tx_impl(test_dir)?; + clean_output_dir(test_dir); + Ok(()) +} diff --git a/controller/tests/contract_self_spend_custom.rs b/controller/tests/contract_self_spend_custom.rs new file mode 100644 index 000000000..376911a13 --- /dev/null +++ b/controller/tests/contract_self_spend_custom.rs @@ -0,0 +1,198 @@ +// 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. + +//! Test a wallet doing contract self-spend flow by using custom inputs and creating custom outputs +// #[macro_use] +extern crate grin_wallet_controller as wallet; +extern crate grin_wallet_impls as impls; +extern crate log; + +use grin_wallet_libwallet as libwallet; + +use grin_core::consensus; +use impls::test_framework::{self}; +use libwallet::contract::my_fee_contribution; +use libwallet::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI, OutputSelectionArgs}; +use libwallet::{OutputStatus, Slate, SlateState, TxLogEntryType}; +use std::sync::atomic::Ordering; +use std::thread; +use std::time::Duration; + +#[macro_use] +mod common; +use common::{clean_output_dir, create_wallets, setup}; +use std::path::PathBuf; + +/// contract self-spend flow with custom picked inputs and outputs +fn contract_self_spend_custom_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { + // create a single wallet and mine 4 blocks + let (wallets, chain, stopper, mut bh) = + create_wallets(vec![vec![("default", 10)]], test_dir).unwrap(); + let send_wallet = wallets[0].0.clone(); + let send_mask = wallets[0].1.as_ref(); + + let mut use_inputs = String::from(""); + + wallet::controller::owner_single_use( + send_wallet.clone(), + send_mask, + PathBuf::from(test_dir), + |api, m| { + let (_, commits) = api.retrieve_outputs(m, true, false, None)?; + println!("OOOT: {:?}", commits[0].output); + use_inputs = format!( + "{},{}", + commits[0].output.commit.as_ref().unwrap(), + commits[1].output.commit.as_ref().unwrap() + ); + Ok(()) + }, + )?; + + let mut slate = Slate::blank(0, true); // this gets overriden below + + let selection_args = OutputSelectionArgs { + min_input_confirmation: 0, + use_inputs: Some(use_inputs.clone()), // we will use two coinbase inputs + // the sum is such that it will need to pick another input making total of 3 inputs + make_outputs: Some(vec![ + 88 * consensus::GRIN_BASE, + 35 * consensus::GRIN_BASE, + 3 * consensus::GRIN_BASE, + (0.2 * consensus::GRIN_BASE as f64) as u64, + 15 * consensus::GRIN_BASE, + ]), + }; + wallet::controller::owner_single_use( + send_wallet.clone(), + send_mask, + PathBuf::from(test_dir), + |api, m| { + // Send wallet inititates a standard transaction with --send=0 + let args = &ContractNewArgsAPI { + setup_args: ContractSetupArgsAPI { + net_change: Some(0), + num_participants: 1, + selection_args: selection_args.clone(), + ..Default::default() + }, + ..Default::default() + }; + slate = api.contract_new(m, args)?; + Ok(()) + }, + )?; + assert_eq!(slate.state, SlateState::Standard1); + + // Send wallet finalizes and posts + wallet::controller::owner_single_use( + send_wallet.clone(), + send_mask, + PathBuf::from(test_dir), + |api, m| { + let args = &ContractSetupArgsAPI { + ..Default::default() + }; + slate = api.contract_sign(m, &slate, args)?; + Ok(()) + }, + )?; + // In the case of a self-spend, we just finish the slate when it's in the Standard2 state + assert_eq!(slate.state, SlateState::Standard2); + + wallet::controller::owner_single_use( + send_wallet.clone(), + send_mask, + PathBuf::from(test_dir), + |api, m| { + api.post_tx(m, &slate, false)?; + Ok(()) + }, + )?; + bh += 1; + + let _ = + test_framework::award_blocks_to_wallet(&chain, send_wallet.clone(), send_mask, 3, false); + bh += 3; + + // Assert changes in send wallet + wallet::controller::owner_single_use( + send_wallet.clone(), + send_mask, + PathBuf::from(test_dir), + |api, m| { + let (_, wallet_info) = api.retrieve_summary_info(m, true, 1)?; + let (refreshed, txs) = api.retrieve_txs(m, true, None, None, None)?; + assert_eq!(wallet_info.last_confirmed_height, bh); + assert!(refreshed); + assert_eq!(txs.len() as u64, bh + 1); // send wallet didn't mine 4 blocks and made 1 tx + let tx_log = txs[txs.len() - 5].clone(); // TODO: why -5 and not -4? + assert_eq!(tx_log.tx_type, TxLogEntryType::TxSelfSpend); + assert_eq!(tx_log.amount_credited, 0); + assert_eq!(tx_log.amount_debited, 0); + assert_eq!(tx_log.num_inputs, 3); + assert_eq!(tx_log.num_outputs, 6); + assert_eq!(tx_log.fee, Some(my_fee_contribution(3, 6, 1, 1)?)); + Ok(()) + }, + )?; + + wallet::controller::owner_single_use( + send_wallet.clone(), + send_mask, + PathBuf::from(test_dir), + |api, m| { + let (_, commits) = api.retrieve_outputs(m, true, false, None)?; + // Assert used inputs are the ones we specified + let used_inputs = use_inputs.split(",").collect::>(); + assert_eq!(commits[0].output.status, OutputStatus::Spent); + assert_eq!(commits[0].output.commit.as_ref().unwrap(), used_inputs[0]); + assert_eq!(commits[1].output.status, OutputStatus::Spent); + assert_eq!(commits[1].output.commit.as_ref().unwrap(), used_inputs[1]); + assert_eq!(commits[2].output.status, OutputStatus::Spent); + // Assert expected outputs were created + // 88, 35, 3, 0.2, 15 and a change output + assert_eq!(commits[10].output.value, 88 * consensus::GRIN_BASE); + assert_eq!(commits[11].output.value, 35 * consensus::GRIN_BASE); + assert_eq!(commits[12].output.value, 3 * consensus::GRIN_BASE); + assert_eq!( + commits[13].output.value, + (0.2 * consensus::GRIN_BASE as f64) as u64 + ); + assert_eq!(commits[14].output.value, 15 * consensus::GRIN_BASE); + // change output is 3*reward - (88-35-3-0.2-15) - my_fees + assert_eq!( + commits[15].output.value, + 3 * consensus::REWARD + - selection_args.sum_output_amounts()? + - my_fee_contribution(3, 6, 1, 1)?.fee() + ); + Ok(()) + }, + )?; + + // let logging finish + stopper.store(false, Ordering::Relaxed); + thread::sleep(Duration::from_millis(200)); + + Ok(()) +} + +#[test] +fn wallet_contract_self_spend_custom_tx() -> Result<(), libwallet::Error> { + let test_dir = "test_output/contract_self_spend_custom_tx"; + setup(test_dir); + contract_self_spend_custom_tx_impl(test_dir)?; + clean_output_dir(test_dir); + Ok(()) +} diff --git a/controller/tests/contract_srs.rs b/controller/tests/contract_srs.rs new file mode 100644 index 000000000..51c4d0016 --- /dev/null +++ b/controller/tests/contract_srs.rs @@ -0,0 +1,175 @@ +// 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. + +//! Test a wallet doing contract SRS flow +// #[macro_use] +extern crate grin_wallet_controller as wallet; +extern crate grin_wallet_impls as impls; +extern crate log; + +use grin_wallet_libwallet as libwallet; + +use impls::test_framework::{self}; +use libwallet::contract::my_fee_contribution; +use libwallet::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI}; +use libwallet::{Slate, SlateState, TxLogEntryType}; +use std::sync::atomic::Ordering; +use std::thread; +use std::time::Duration; + +#[macro_use] +mod common; +use common::{clean_output_dir, create_wallets, setup}; +use std::path::PathBuf; + +/// contract SRS flow +fn contract_srs_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { + // create two wallets and mine 4 blocks in each (we want both to have balance to get a payjoin) + let (wallets, chain, stopper, mut bh) = + create_wallets(vec![vec![("default", 4)], vec![("default", 4)]], test_dir).unwrap(); + let send_wallet = wallets[0].0.clone(); + let send_mask = wallets[0].1.as_ref(); + let recv_wallet = wallets[1].0.clone(); + let recv_mask = wallets[1].1.as_ref(); + + let mut slate = Slate::blank(0, true); // this gets overriden below + + wallet::controller::owner_single_use( + send_wallet.clone(), + send_mask, + PathBuf::from(test_dir), + |api, m| { + // Send wallet inititates a standard transaction with --send=5 + let args = &mut ContractNewArgsAPI { + setup_args: ContractSetupArgsAPI { + net_change: Some(-5_000_000_000), + ..Default::default() + }, + ..Default::default() + }; + slate = api.contract_new(m, args)?; + Ok(()) + }, + )?; + assert_eq!(slate.state, SlateState::Standard1); + + wallet::controller::owner_single_use( + recv_wallet.clone(), + recv_mask, + PathBuf::from(test_dir), + |api, m| { + // Receive wallet calls --receive=5 + let args = &mut ContractSetupArgsAPI { + net_change: Some(5_000_000_000), + ..Default::default() + }; + slate = api.contract_sign(m, &slate, args)?; + Ok(()) + }, + )?; + assert_eq!(slate.state, SlateState::Standard2); + + // Send wallet finalizes and posts + wallet::controller::owner_single_use( + send_wallet.clone(), + send_mask, + PathBuf::from(test_dir), + |api, m| { + let args = &mut ContractSetupArgsAPI { + ..Default::default() + }; + slate = api.contract_sign(m, &slate, args)?; + Ok(()) + }, + )?; + assert_eq!(slate.state, SlateState::Standard3); + + wallet::controller::owner_single_use( + send_wallet.clone(), + send_mask, + PathBuf::from(test_dir), + |api, m| { + api.post_tx(m, &slate, false)?; + Ok(()) + }, + )?; + bh += 1; + + let _ = + test_framework::award_blocks_to_wallet(&chain, send_wallet.clone(), send_mask, 3, false); + bh += 3; + + // Assert changes in receive wallet + wallet::controller::owner_single_use( + recv_wallet.clone(), + recv_mask, + PathBuf::from(test_dir), + |api, m| { + let (_, wallet_info) = api.retrieve_summary_info(m, true, 1)?; + let (refreshed, txs) = api.retrieve_txs(m, true, None, None, None)?; + assert_eq!(wallet_info.last_confirmed_height, bh); + assert!(refreshed); + assert_eq!(txs.len(), 5); // 4 mined and 1 received + let tx_log = txs[4].clone(); + assert_eq!(tx_log.tx_type, TxLogEntryType::TxReceived); + assert_eq!(tx_log.amount_credited, 5_000_000_000); + assert_eq!(tx_log.amount_debited, 0); + assert_eq!(tx_log.num_inputs, 1); + assert_eq!(tx_log.num_outputs, 1); + let expected_fees_paid = Some(my_fee_contribution(1, 1, 1, 2)?); + assert_eq!(tx_log.fee, expected_fees_paid); + assert_eq!( + wallet_info.amount_currently_spendable, + 4 * 60_000_000_000 + 5_000_000_000 - expected_fees_paid.unwrap().fee() // we expect the balance of 4 mined blocks + 5 Grin - fees paid + ); + Ok(()) + }, + )?; + + // Assert changes in send wallet + wallet::controller::owner_single_use( + send_wallet.clone(), + send_mask, + PathBuf::from(test_dir), + |api, m| { + let (_, wallet_info) = api.retrieve_summary_info(m, true, 1)?; + let (refreshed, txs) = api.retrieve_txs(m, true, None, None, None)?; + assert_eq!(wallet_info.last_confirmed_height, bh); + assert!(refreshed); + assert_eq!(txs.len() as u64, bh - 4 + 1); // send wallet didn't mine 4 blocks and made 1 tx + let tx_log = txs[txs.len() - 5].clone(); // TODO: why -5 and not -4? + assert_eq!(tx_log.tx_type, TxLogEntryType::TxSent); + assert_eq!(tx_log.amount_credited, 0); + assert_eq!(tx_log.amount_debited, 5_000_000_000); + assert_eq!(tx_log.num_inputs, 1); + assert_eq!(tx_log.num_outputs, 1); + assert_eq!(tx_log.fee, Some(my_fee_contribution(1, 1, 1, 2)?)); + Ok(()) + }, + )?; + + // let logging finish + stopper.store(false, Ordering::Relaxed); + thread::sleep(Duration::from_millis(200)); + + Ok(()) +} + +#[test] +fn wallet_contract_srs_tx() -> Result<(), libwallet::Error> { + let test_dir = "test_output/contract_srs_tx"; + setup(test_dir); + contract_srs_tx_impl(test_dir)?; + clean_output_dir(test_dir); + Ok(()) +} diff --git a/controller/tests/contract_srs_mwmixnet.rs b/controller/tests/contract_srs_mwmixnet.rs new file mode 100644 index 000000000..1f8c5cb02 --- /dev/null +++ b/controller/tests/contract_srs_mwmixnet.rs @@ -0,0 +1,172 @@ +// 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. + +//! Test a wallet doing contract SRS flow +// #[macro_use] +extern crate grin_wallet_controller as wallet; +extern crate grin_wallet_impls as impls; +extern crate log; + +use grin_util::{secp::SecretKey, static_secp_instance}; +use grin_wallet_libwallet as libwallet; + +use impls::test_framework::{self}; +use libwallet::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI}; +use libwallet::mwixnet::MixnetReqCreationParams; +use libwallet::{Slate, SlateState}; +use std::sync::atomic::Ordering; +use std::thread; +use std::time::Duration; + +#[macro_use] +mod common; +use common::{clean_output_dir, create_wallets, setup}; +use std::path::PathBuf; + +/// contract SRS flow - just creating an mwixnet tx at the moment +fn contract_srs_mwixnet_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { + // create two wallets and mine 4 blocks in each (we want both to have balance to get a payjoin) + let (wallets, chain, stopper, _bh) = + create_wallets(vec![vec![("default", 4)], vec![("default", 4)]], test_dir).unwrap(); + let send_wallet = wallets[0].0.clone(); + let send_mask = wallets[0].1.as_ref(); + let recv_wallet = wallets[1].0.clone(); + let recv_mask = wallets[1].1.as_ref(); + + let mut slate = Slate::blank(0, true); // this gets overriden below + + wallet::controller::owner_single_use( + send_wallet.clone(), + send_mask, + PathBuf::from(test_dir), + |api, m| { + // Send wallet inititates a standard transaction with --send=5 + let args = &mut ContractNewArgsAPI { + setup_args: ContractSetupArgsAPI { + net_change: Some(-5_000_000_000), + ..Default::default() + }, + ..Default::default() + }; + slate = api.contract_new(m, args)?; + Ok(()) + }, + )?; + assert_eq!(slate.state, SlateState::Standard1); + + wallet::controller::owner_single_use( + recv_wallet.clone(), + recv_mask, + PathBuf::from(test_dir), + |api, m| { + // Receive wallet calls --receive=5 + let args = &mut ContractSetupArgsAPI { + net_change: Some(5_000_000_000), + ..Default::default() + }; + args.proof_args.suppress_proof = true; + slate = api.contract_sign(m, &slate, args)?; + Ok(()) + }, + )?; + assert_eq!(slate.state, SlateState::Standard2); + + // Send wallet finalizes and posts + wallet::controller::owner_single_use( + send_wallet.clone(), + send_mask, + PathBuf::from(test_dir), + |api, m| { + let args = &mut ContractSetupArgsAPI { + ..Default::default() + }; + args.proof_args.suppress_proof = true; + slate = api.contract_sign(m, &slate, args)?; + Ok(()) + }, + )?; + assert_eq!(slate.state, SlateState::Standard3); + + wallet::controller::owner_single_use( + send_wallet.clone(), + send_mask, + PathBuf::from(test_dir), + |api, m| { + api.post_tx(m, &slate, false)?; + Ok(()) + }, + )?; + + let _ = + test_framework::award_blocks_to_wallet(&chain, send_wallet.clone(), send_mask, 3, false); + + // Recipient wallet sends outputs to mwixnet + wallet::controller::owner_single_use( + recv_wallet.clone(), + recv_mask, + PathBuf::from(test_dir), + |api, m| { + let secp_locked = 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(recv_mask, 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(recv_mask, 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(200)); + + Ok(()) +} + +#[test] +fn wallet_contract_srs_mwixnet_tx() -> Result<(), libwallet::Error> { + let test_dir = "test_output/contract_srs_mwixnet_tx"; + setup(test_dir); + contract_srs_mwixnet_tx_impl(test_dir)?; + clean_output_dir(test_dir); + Ok(()) +} diff --git a/controller/tests/file.rs b/controller/tests/file.rs index c6c4e1ac3..e31340743 100644 --- a/controller/tests/file.rs +++ b/controller/tests/file.rs @@ -19,6 +19,7 @@ extern crate grin_wallet_impls as impls; use grin_core as core; use grin_wallet_libwallet as libwallet; +use std::path::PathBuf; use impls::test_framework::{self, LocalWalletClient}; use impls::{PathToSlate, SlateGetter as _, SlatePutter as _}; @@ -75,18 +76,28 @@ fn file_exchange_test_impl(test_dir: &'static str, use_bin: bool) -> Result<(), 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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet2.clone(), + mask2, + PathBuf::from(test_dir), + |api, m| { + api.create_account_path(m, "account1")?; + api.create_account_path(m, "account2")?; + Ok(()) + }, + )?; // Get some mining done { @@ -111,27 +122,32 @@ fn file_exchange_test_impl(test_dir: &'static str, use_bin: bool) -> Result<(), }; // 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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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 { @@ -142,43 +158,63 @@ fn file_exchange_test_impl(test_dir: &'static str, use_bin: bool) -> Result<(), 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::controller::foreign_single_use( + wallet2.clone(), + PathBuf::from(test_dir), + 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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet2.clone(), + mask2, + PathBuf::from(test_dir), + |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 @@ -197,43 +233,63 @@ fn file_exchange_test_impl(test_dir: &'static str, use_bin: bool) -> Result<(), 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(()) - })?; + wallet::controller::owner_single_use( + wallet2.clone(), + mask2, + PathBuf::from(test_dir), + |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( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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(), + PathBuf::from(test_dir), + 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( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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); @@ -251,46 +307,66 @@ fn file_exchange_test_impl(test_dir: &'static str, use_bin: bool) -> Result<(), }; 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::controller::owner_single_use( + wallet2.clone(), + mask2, + PathBuf::from(test_dir), + |api, m| { + address = Some(api.get_slatepack_address(m, 0)?); + Ok(()) + }, + )?; + + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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(), + PathBuf::from(test_dir), + 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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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); diff --git a/controller/tests/invoice.rs b/controller/tests/invoice.rs index 2fe7d648d..7acd8bfe7 100644 --- a/controller/tests/invoice.rs +++ b/controller/tests/invoice.rs @@ -19,6 +19,7 @@ extern crate grin_wallet_impls as impls; use grin_core as core; use grin_wallet_libwallet as libwallet; +use std::path::PathBuf; use impls::test_framework::{self, LocalWalletClient}; use libwallet::{InitTxArgs, IssueInvoiceTxArgs, Slate, SlateState}; @@ -71,11 +72,16 @@ fn invoice_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { 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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |api, m| { + api.create_account_path(m, "mining")?; + api.create_account_path(m, "listener")?; + Ok(()) + }, + )?; // Get some mining done { @@ -87,122 +93,167 @@ fn invoice_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { 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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet2.clone(), + mask2, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::foreign_single_use( + wallet2.clone(), + PathBuf::from(test_dir), + 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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet2.clone(), + mask2, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::foreign_single_use( + wallet1.clone(), + PathBuf::from(test_dir), + 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; @@ -210,82 +261,107 @@ fn invoice_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { // 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(()) - })?; + wallet::controller::owner_single_use( + wallet2.clone(), + mask2, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet2.clone(), + mask2, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet2.clone(), + mask2, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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 diff --git a/controller/tests/late_lock.rs b/controller/tests/late_lock.rs index b80cc8c46..2d1ec290b 100644 --- a/controller/tests/late_lock.rs +++ b/controller/tests/late_lock.rs @@ -20,6 +20,7 @@ extern crate grin_wallet_libwallet as libwallet; use self::libwallet::{InitTxArgs, Slate}; use impls::test_framework::{self, LocalWalletClient}; +use std::path::PathBuf; use std::sync::atomic::Ordering; use std::thread; use std::time::Duration; @@ -68,16 +69,26 @@ fn late_lock_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { }); // add some accounts - wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { - api.create_account_path(m, "mining")?; - Ok(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet2.clone(), + mask2, + PathBuf::from(test_dir), + |api, m| { + api.create_account_path(m, "account1")?; + Ok(()) + }, + )?; // Get some mining done { @@ -94,52 +105,67 @@ fn late_lock_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { 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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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( + wallet2.clone(), + mask2, + PathBuf::from(test_dir), + |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); diff --git a/controller/tests/mwixnet.rs b/controller/tests/mwixnet.rs index 0db537d2c..60e4bd8c0 100644 --- a/controller/tests/mwixnet.rs +++ b/controller/tests/mwixnet.rs @@ -20,6 +20,7 @@ extern crate grin_wallet_impls as impls; use grin_core as core; use grin_util as util; use grin_util::secp::key::SecretKey; +use std::path::PathBuf; use grin_wallet_libwallet as libwallet; use impls::test_framework::{self, LocalWalletClient}; @@ -64,11 +65,16 @@ fn mwixnet_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { 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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |api, m| { + api.create_account_path(m, "mining")?; + api.create_account_path(m, "listener")?; + Ok(()) + }, + )?; // Get some mining done { @@ -80,98 +86,123 @@ fn mwixnet_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { 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)?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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(), + PathBuf::from(test_dir), + 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(()) - })?; - 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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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); diff --git a/controller/tests/no_change.rs b/controller/tests/no_change.rs index cefba915b..aa1a56e4f 100644 --- a/controller/tests/no_change.rs +++ b/controller/tests/no_change.rs @@ -18,6 +18,7 @@ extern crate grin_wallet_controller as wallet; extern crate grin_wallet_impls as impls; use grin_core as core; +use std::path::PathBuf; use grin_wallet_libwallet as libwallet; use impls::test_framework::{self, LocalWalletClient}; @@ -78,122 +79,167 @@ fn no_change_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { // 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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet2.clone(), + mask2, + PathBuf::from(test_dir), + |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::controller::owner_single_use( + wallet2.clone(), + mask2, + PathBuf::from(test_dir), + |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( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::foreign_single_use( + wallet2.clone(), + PathBuf::from(test_dir), + mask2_i.clone(), + |api| { + // Wallet 2 receives the invoice transaction + slate = api.finalize_tx(&slate, false)?; + Ok(()) + }, + )?; + wallet::controller::owner_single_use( + wallet2.clone(), + mask1, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet2.clone(), + mask2, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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); diff --git a/controller/tests/payment_proofs.rs b/controller/tests/payment_proofs.rs index edb416f39..651e48635 100644 --- a/controller/tests/payment_proofs.rs +++ b/controller/tests/payment_proofs.rs @@ -21,6 +21,7 @@ extern crate grin_wallet_util; use grin_wallet_libwallet as libwallet; use impls::test_framework::{self, LocalWalletClient}; use libwallet::{InitTxArgs, Slate}; +use std::path::PathBuf; use std::sync::atomic::Ordering; use std::thread; use std::time::Duration; @@ -77,83 +78,98 @@ fn payment_proofs_test_impl(test_dir: &'static str) -> Result<(), libwallet::Err 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(()) - })?; + wallet::controller::owner_single_use( + wallet2.clone(), + mask2, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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); diff --git a/controller/tests/repost.rs b/controller/tests/repost.rs index 283164cfe..0718364f3 100644 --- a/controller/tests/repost.rs +++ b/controller/tests/repost.rs @@ -19,6 +19,7 @@ extern crate grin_wallet_impls as impls; extern crate grin_wallet_libwallet as libwallet; use grin_core as core; +use std::path::PathBuf; use self::libwallet::{InitTxArgs, Slate}; use impls::test_framework::{self, LocalWalletClient}; @@ -74,18 +75,28 @@ fn file_repost_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> 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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet2.clone(), + mask2, + PathBuf::from(test_dir), + |api, m| { + api.create_account_path(m, "account1")?; + api.create_account_path(m, "account2")?; + Ok(()) + }, + )?; // Get some mining done { @@ -102,26 +113,31 @@ fn file_repost_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> 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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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; @@ -132,12 +148,17 @@ fn file_repost_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> 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::controller::foreign_single_use( + wallet1.clone(), + PathBuf::from(test_dir), + 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 { @@ -146,47 +167,67 @@ fn file_repost_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> } // 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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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 { @@ -201,55 +242,75 @@ fn file_repost_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> 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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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( + wallet2.clone(), + mask2, + PathBuf::from(test_dir), + |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); diff --git a/controller/tests/revert.rs b/controller/tests/revert.rs index ea6c0b2ac..ef5a8879a 100644 --- a/controller/tests/revert.rs +++ b/controller/tests/revert.rs @@ -31,6 +31,7 @@ use grin_wallet_libwallet as libwallet; use grin_wallet_libwallet::api_impl::types::InitTxArgs; use grin_wallet_libwallet::WalletInst; use log::error; +use std::path::PathBuf; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::thread; @@ -41,7 +42,7 @@ type Wallet = Arc< Box< dyn WalletInst< 'static, - DefaultLCProvider<'static, LocalWalletClient, ExtKeychain>, + DefaultLCProvider, LocalWalletClient, ExtKeychain, >, @@ -104,13 +105,13 @@ fn revert( } }); - owner(Some(wallet1.clone()), mask1, None, |api, m| { + owner(wallet1.clone(), mask1, PathBuf::from(test_dir), |api, m| { api.create_account_path(m, "a")?; api.set_active_account(m, "a")?; Ok(()) })?; - owner(Some(wallet2.clone()), mask2, None, |api, m| { + owner(wallet2.clone(), mask2, PathBuf::from(test_dir), |api, m| { api.create_account_path(m, "b")?; api.set_active_account(m, "b")?; Ok(()) @@ -125,7 +126,7 @@ fn revert( award_blocks_to_wallet(&chain, wallet1.clone(), mask1, bh as usize, false)?; // Sanity check contents - owner(Some(wallet1.clone()), mask1, None, |api, m| { + owner(wallet1.clone(), mask1, PathBuf::from(test_dir), |api, m| { let (refreshed, info) = api.retrieve_summary_info(m, true, 1)?; assert!(refreshed); assert_eq!(info.last_confirmed_height, bh); @@ -140,7 +141,7 @@ fn revert( Ok(()) })?; - owner(Some(wallet2.clone()), mask2, None, |api, m| { + owner(wallet2.clone(), mask2, PathBuf::from(test_dir), |api, m| { let (refreshed, info) = api.retrieve_summary_info(m, true, 1)?; assert!(refreshed); assert_eq!(info.last_confirmed_height, bh); @@ -155,7 +156,7 @@ fn revert( // Send some funds let mut tx = None; - owner(Some(wallet1.clone()), mask1, None, |api, m| { + owner(wallet1.clone(), mask1, PathBuf::from(test_dir), |api, m| { // send to send let args = InitTxArgs { src_acct_name: None, @@ -180,7 +181,7 @@ fn revert( let tx = tx.expect("tx from slate"); // Check funds have been received - owner(Some(wallet2.clone()), mask2, None, |api, m| { + owner(wallet2.clone(), mask2, PathBuf::from(test_dir), |api, m| { let (refreshed, info) = api.retrieve_summary_info(m, true, 1)?; assert!(refreshed); assert_eq!(info.last_confirmed_height, bh); @@ -222,7 +223,7 @@ fn revert( let bh = bh + 1; // Check funds have been confirmed - owner(Some(wallet2.clone()), mask2, None, |api, m| { + owner(wallet2.clone(), mask2, PathBuf::from(test_dir), |api, m| { let (refreshed, info) = api.retrieve_summary_info(m, true, 1)?; assert!(refreshed); assert_eq!(info.last_confirmed_height, bh); @@ -257,7 +258,7 @@ fn revert( let bh = bh + 1; // Check funds have been reverted - owner(Some(wallet2.clone()), mask2, None, |api, m| { + owner(wallet2.clone(), mask2, PathBuf::from(test_dir), |api, m| { api.scan(m, None, false)?; let (refreshed, info) = api.retrieve_summary_info(m, true, 1)?; assert!(refreshed); @@ -292,7 +293,7 @@ fn revert_reconfirm_impl(test_dir: &'static str) -> Result<(), libwallet::Error> let bh = bh + 1; // Check funds have been confirmed again - owner(Some(wallet2.clone()), mask2, None, |api, m| { + owner(wallet2.clone(), mask2, PathBuf::from(test_dir), |api, m| { let (refreshed, info) = api.retrieve_summary_info(m, true, 1)?; assert!(refreshed); assert_eq!(info.last_confirmed_height, bh); @@ -320,7 +321,7 @@ fn revert_cancel_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { let mask2 = mask2_i.as_ref(); // Cancelling tx - owner(Some(wallet2.clone()), mask2, None, |api, m| { + owner(wallet2.clone(), mask2, PathBuf::from(test_dir), |api, m| { // Sanity check let (refreshed, info) = api.retrieve_summary_info(m, true, 1)?; assert!(refreshed); diff --git a/controller/tests/self_send.rs b/controller/tests/self_send.rs index 5fc6058de..6cf572ec9 100644 --- a/controller/tests/self_send.rs +++ b/controller/tests/self_send.rs @@ -18,6 +18,7 @@ extern crate grin_wallet_controller as wallet; extern crate grin_wallet_impls as impls; use grin_core as core; +use std::path::PathBuf; use grin_wallet_libwallet as libwallet; use impls::test_framework::{self, LocalWalletClient}; @@ -62,11 +63,16 @@ fn self_send_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { 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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |api, m| { + api.create_account_path(m, "mining")?; + api.create_account_path(m, "listener")?; + Ok(()) + }, + )?; // Get some mining done { @@ -78,58 +84,78 @@ fn self_send_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { 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)?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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(), + PathBuf::from(test_dir), + 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(()) - })?; - 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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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); diff --git a/controller/tests/self_send_cancel.rs b/controller/tests/self_send_cancel.rs new file mode 100644 index 000000000..492ecf5f4 --- /dev/null +++ b/controller/tests/self_send_cancel.rs @@ -0,0 +1,141 @@ +// 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, RetrieveTxQueryArgs}; +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}; +use std::path::PathBuf; + +/// self send impl +fn self_send_cancel_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; + + 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( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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: None, + 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(), + PathBuf::from(test_dir), + mask1_i.clone(), + |api| { + slate = api.receive_tx(&slate, None, None)?; + Ok(()) + }, + )?; + // Now cancel the transaction + api.cancel_tx(mask1, None, Some(slate.id.clone()))?; + bh += 1; + Ok(()) + }, + )?; + + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |api, _m| { + let query = RetrieveTxQueryArgs { + include_outstanding_only: Some(true), + exclude_cancelled: Some(true), + ..Default::default() + }; + let txs = api.retrieve_txs(mask1, true, None, None, Some(query))?; + for tx in txs.1.iter() { + println!("Tx: {:?}", tx); + } + assert!(txs.1.is_empty()); + Ok(()) + }, + )?; + + // let logging finish + stopper.store(false, Ordering::Relaxed); + thread::sleep(Duration::from_millis(1000)); + Ok(()) +} + +#[test] +fn wallet_self_send_cancel() { + let test_dir = "test_output/self_send_cancel"; + setup(test_dir); + if let Err(e) = self_send_cancel_test_impl(test_dir) { + panic!("Libwallet Error: {}", e); + } + clean_output_dir(test_dir); +} diff --git a/controller/tests/slatepack.rs b/controller/tests/slatepack.rs index d65ec5343..9bdd740c9 100644 --- a/controller/tests/slatepack.rs +++ b/controller/tests/slatepack.rs @@ -18,7 +18,9 @@ extern crate grin_wallet_controller as wallet; extern crate grin_wallet_impls as impls; use grin_core as core; +use grin_wallet_config::GlobalWalletConfig; use grin_wallet_libwallet as libwallet; +use std::path::PathBuf; use impls::test_framework::{self, LocalWalletClient}; use impls::{PathToSlatepack, SlatePutter as _}; @@ -27,12 +29,12 @@ use std::thread; use std::time::Duration; use grin_wallet_libwallet::{ - InitTxArgs, IssueInvoiceTxArgs, Slate, Slatepack, SlatepackAddress, Slatepacker, - SlatepackerArgs, + InitTxArgs, InitTxSendArgs, IssueInvoiceTxArgs, Slate, Slatepack, SlatepackAddress, + Slatepacker, SlatepackerArgs, }; -use ed25519_dalek::PublicKey as edDalekPublicKey; -use ed25519_dalek::SecretKey as edDalekSecretKey; +use ed25519_dalek::SigningKey as edDalekSecretKey; +use ed25519_dalek::VerifyingKey as edDalekPublicKey; #[macro_use] mod common; @@ -124,18 +126,28 @@ fn slatepack_exchange_test_impl( 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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet2.clone(), + mask2, + PathBuf::from(test_dir), + |api, m| { + api.create_account_path(m, "account1")?; + api.create_account_path(m, "account2")?; + Ok(()) + }, + )?; // Get some mining done { @@ -149,13 +161,18 @@ fn slatepack_exchange_test_impl( 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(()) - })?; + let mut sec_key = edDalekSecretKey::from_bytes(&[0u8; 32]); + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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), @@ -168,13 +185,18 @@ fn slatepack_exchange_test_impl( 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(()) - })?; + let mut sec_key = edDalekSecretKey::from_bytes(&[0u8; 32]); + wallet::controller::owner_single_use( + wallet2.clone(), + mask2, + PathBuf::from(test_dir), + |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), @@ -197,34 +219,39 @@ fn slatepack_exchange_test_impl( ), }; - 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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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 { @@ -236,54 +263,75 @@ fn slatepack_exchange_test_impl( 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(()) - })?; + wallet::controller::foreign_single_use( + wallet2.clone(), + PathBuf::from(test_dir), + 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 finalizes and posts + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet2.clone(), + mask2, + PathBuf::from(test_dir), + |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 @@ -302,63 +350,83 @@ fn slatepack_exchange_test_impl( 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(()) - })?; + wallet::controller::owner_single_use( + wallet2.clone(), + mask2, + PathBuf::from(test_dir), + |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( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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(), + PathBuf::from(test_dir), + 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( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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); @@ -377,66 +445,86 @@ fn slatepack_exchange_test_impl( 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::controller::owner_single_use( + wallet2.clone(), + mask2, + PathBuf::from(test_dir), + |api, m| { + address = Some(api.get_slatepack_address(m, 0)?); + Ok(()) + }, + )?; + + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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(), + PathBuf::from(test_dir), + 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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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); @@ -481,31 +569,140 @@ fn slatepack_api_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { 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| { + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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(()) +} + +/// Do not create transaction for invalid or wrong network Slatepack address. +fn slatepack_address_validation(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(); + let config_path = PathBuf::from(test_dir).join("grin-wallet.toml"); + GlobalWalletConfig::for_chain(&core::global::ChainTypes::AutomatedTesting, &config_path) + .write_to_file(config_path.to_str().unwrap(), false, None, None) + .unwrap(); + + 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(); + + let proxy_thread = thread::spawn(move || { + if let Err(e) = wallet_proxy.run() { + error!("Wallet Proxy error: {}", e); + } + }); + + let reward = core::consensus::REWARD; + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 6, false); + let mut slate = Slate::blank(2, true); + + wallet::controller::owner_single_use(wallet2.clone(), mask2, config_path.clone(), |api, m| { + slate = api.issue_invoice_tx( + m, + IssueInvoiceTxArgs { + amount: reward, + ..Default::default() + }, + )?; + Ok(()) + })?; + + wallet::controller::owner_single_use(wallet1.clone(), mask1, config_path, |api, m| { let args = InitTxArgs { src_acct_name: Some("mining".to_owned()), - amount: reward * 2, + amount: reward, 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); + + let mut wrong_net_args = args.clone(); + wrong_net_args.send_args = Some(InitTxSendArgs { + dest: "grin1dvge9z4uqgqlpspmljrd7smh3grrw9xu2r9lkz3u67s3emj3ud2sd5gk9p".to_string(), + post_tx: false, + fluff: false, + skip_tor: Some(true), + }); + assert!(api.init_send_tx(m, wrong_net_args.clone()).is_err()); + assert!(api.process_invoice_tx(m, &slate, wrong_net_args).is_err()); + + let mut invalid_args = args.clone(); + invalid_args.send_args = Some(InitTxSendArgs { + dest: "tgrinaddr10qlk22rxjap2ny8qltc2tl996kenxr3hhwuu6hrzs6tdq08yaqgqnlumr7" + .to_string(), + post_tx: false, + fluff: false, + skip_tor: Some(true), + }); + assert!(api.init_send_tx(m, invalid_args.clone()).is_err()); + assert!(api.process_invoice_tx(m, &slate, invalid_args).is_err()); + + let mut valid_args = args; + valid_args.send_args = Some(InitTxSendArgs { + dest: "tgrin1xtxavwfgs48ckf3gk8wwgcndmn0nt4tvkl8a7ltyejjcy2mc6nfs9gm2lp".to_string(), + post_tx: false, + fluff: false, + skip_tor: Some(true), + }); + api.process_invoice_tx(m, &slate, valid_args.clone())?; + api.init_send_tx(m, valid_args)?; Ok(()) })?; - // let logging finish stopper.store(false, Ordering::Relaxed); - thread::sleep(Duration::from_millis(200)); + proxy_thread.join().expect("wallet proxy thread panicked"); Ok(()) } @@ -585,3 +782,13 @@ fn slatepack_api() { } clean_output_dir(test_dir); } + +#[test] +fn slatepack_address() { + let test_dir = "test_output/slatepack_address"; + setup(test_dir); + if let Err(e) = slatepack_address_validation(test_dir) { + panic!("Libwallet Error: {}", e); + } + clean_output_dir(test_dir); +} diff --git a/controller/tests/transaction.rs b/controller/tests/transaction.rs index 33db4c9bf..55734dcfc 100644 --- a/controller/tests/transaction.rs +++ b/controller/tests/transaction.rs @@ -25,6 +25,7 @@ use self::core::global; use self::libwallet::{InitTxArgs, OutputStatus, Slate, SlateState}; use impls::test_framework::{self, LocalWalletClient}; use std::convert::TryInto; +use std::path::PathBuf; use std::sync::atomic::Ordering; use std::thread; use std::time::Duration; @@ -80,320 +81,390 @@ fn basic_transaction_api(test_dir: &'static str) -> Result<(), libwallet::Error> 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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet2.clone(), + mask2, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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( + wallet2.clone(), + mask2, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet2.clone(), + mask2, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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); @@ -447,132 +518,158 @@ fn tx_rollback(test_dir: &'static str) -> Result<(), libwallet::Error> { 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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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); + assert_eq!(output_mappings.len(), 3); + assert_eq!(locked_count, 2); + assert_eq!(unconfirmed_count, 1); - Ok(()) - })?; + 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; + wallet::controller::owner_single_use( + wallet2.clone(), + mask2, + PathBuf::from(test_dir), + |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(()) - })?; + 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::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet2.clone(), + mask2, + PathBuf::from(test_dir), + |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); diff --git a/controller/tests/ttl_cutoff.rs b/controller/tests/ttl_cutoff.rs index ec4340bc9..faa39900c 100644 --- a/controller/tests/ttl_cutoff.rs +++ b/controller/tests/ttl_cutoff.rs @@ -21,6 +21,7 @@ extern crate grin_wallet_util; use grin_wallet_libwallet as libwallet; use impls::test_framework::{self, LocalWalletClient}; use libwallet::{InitTxArgs, Slate, TxLogEntryType}; +use std::path::PathBuf; use std::sync::atomic::Ordering; use std::thread; use std::time::Duration; @@ -78,95 +79,125 @@ fn ttl_cutoff_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> 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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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(); + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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(()) - })?; + assert_eq!(tx.ttl_cutoff_height, Some(12)); + assert_eq!(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(()) - })?; + wallet::controller::owner_single_use( + wallet2.clone(), + mask2, + PathBuf::from(test_dir), + |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_eq!(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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |sender_api, m| { + // note this will increment the block count as part of the transaction "Posting" + let args = InitTxArgs { + src_acct_name: None, + 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(()) - })?; + wallet::controller::owner_single_use( + wallet2.clone(), + mask2, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |_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); diff --git a/controller/tests/tx_list_filter.rs b/controller/tests/tx_list_filter.rs index 7a8dab030..424db244b 100644 --- a/controller/tests/tx_list_filter.rs +++ b/controller/tests/tx_list_filter.rs @@ -23,6 +23,7 @@ use grin_core as core; use grin_keychain as keychain; use grin_util as util; use libwallet::{RetrieveTxQueryArgs, RetrieveTxQuerySortField}; +use std::path::PathBuf; use self::libwallet::{InitTxArgs, Slate}; use impls::test_framework::{self, LocalWalletClient}; @@ -45,7 +46,7 @@ fn test_wallet_tx_filtering( Box< dyn WalletInst< 'static, - DefaultLCProvider<'static, LocalWalletClient, ExtKeychain>, + DefaultLCProvider, LocalWalletClient, ExtKeychain, >, @@ -53,154 +54,160 @@ fn test_wallet_tx_filtering( >, >, mask: Option<&SecretKey>, + test_dir: &'static str, ) -> 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(()) - })?; + wallet::controller::owner_single_use( + wallet.clone(), + mask, + PathBuf::from(test_dir), + |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(()) } @@ -248,15 +255,25 @@ fn build_chain_for_tx_filtering( // 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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |api, _m| { + api.stop_updater()?; + Ok(()) + }, + )?; + + wallet::controller::owner_single_use( + wallet2.clone(), + mask2, + PathBuf::from(test_dir), + |api, _m| { + api.stop_updater()?; + Ok(()) + }, + )?; // few values to keep things shorter let reward = core::consensus::REWARD; @@ -268,17 +285,22 @@ fn build_chain_for_tx_filtering( 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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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 _ = @@ -293,9 +315,9 @@ fn build_chain_for_tx_filtering( let mut slate = Slate::blank(1, false); debug!("Creating TX for {}", amount); wallet::controller::owner_single_use( - Some(wallet1.clone()), + wallet1.clone(), mask1, - None, + PathBuf::from(test_dir), |sender_api, m| { // note this will increment the block count as part of the transaction "Posting" let args = InitTxArgs { @@ -321,26 +343,31 @@ fn build_chain_for_tx_filtering( 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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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)?; + test_wallet_tx_filtering(wallet1, mask1, test_dir)?; // let logging finish stopper.store(false, Ordering::Relaxed); diff --git a/controller/tests/updater_thread.rs b/controller/tests/updater_thread.rs index c137b4e02..ea8273dee 100644 --- a/controller/tests/updater_thread.rs +++ b/controller/tests/updater_thread.rs @@ -23,6 +23,7 @@ extern crate grin_wallet_libwallet as libwallet; // use grin_wallet_util::grin_core as core; use impls::test_framework::{self, LocalWalletClient}; +use std::path::PathBuf; use std::sync::atomic::Ordering; use std::thread; use std::time::Duration; @@ -71,18 +72,28 @@ fn updater_thread_test_impl(test_dir: &'static str) -> Result<(), libwallet::Err }); // 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(()) - })?; + wallet::controller::owner_single_use( + wallet1.clone(), + mask1, + PathBuf::from(test_dir), + |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(()) - })?; + wallet::controller::owner_single_use( + wallet2.clone(), + mask2, + PathBuf::from(test_dir), + |api, m| { + api.create_account_path(m, "account1")?; + api.create_account_path(m, "account2")?; + Ok(()) + }, + )?; // Get some mining done { @@ -93,7 +104,7 @@ fn updater_thread_test_impl(test_dir: &'static str) -> Result<(), libwallet::Err let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, bh as usize, false); - let owner_api = api::Owner::new(wallet1, None); + let owner_api = api::Owner::new(wallet1, None, PathBuf::from(test_dir)); owner_api.start_updater(mask1, Duration::from_secs(5))?; // let updater thread run a bit diff --git a/default.nix b/default.nix new file mode 100644 index 000000000..6e40b0a76 --- /dev/null +++ b/default.nix @@ -0,0 +1,18 @@ +{ pkgs ? import {} }: + + pkgs.mkShell { + nativeBuildInputs = [ pkgs.clang ]; + buildInputs = with pkgs; [ + glibc + rustup + openssl + pkgconfig + llvmPackages.libclang + ncurses + glibcLocales + tor + ]; + shellHook = '' + export LIBCLANG_PATH="${pkgs.llvmPackages.libclang.lib}/lib"; + ''; + } \ No newline at end of file diff --git a/doc/design/design.md b/doc/design/design.md index d1135045c..2da52d2a9 100644 --- a/doc/design/design.md +++ b/doc/design/design.md @@ -55,24 +55,22 @@ the default client only supports single-user - single recipient, an arbitrary nu ### Wallet Traits -In the current code, a Wallet implementation is just a combination of these three traits. The vast majority of functions within libwallet +In the current code, a Wallet implementation is just a combination of these two traits. The vast majority of functions within libwallet and libTX have a signature similar to the following: ```rust pub fn retrieve_outputs( -!·wallet: &mut T, +!·wallet: &mut WalletBackend, !·show_spent: bool, !·tx_id: Option, ) -> Result, Error> where -!·T: WalletBackend, !·C: NodeClient, !·K: Keychain, { ``` -With `T` in this instance being a class that implements the `WalletBackend` trait, which is further parameterized with implementations of -`NodeClient` and `Keychain`. +Provided `WalletBackend` 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 @@ -81,9 +79,4 @@ The `Keychain` trait makes several assumptions about the underlying implementati There are two implementations of `NodeClient` within the code, the main version being the `HTTPNodeClient` found within `wallet/src/client.rs` and the seconds a test client that communicates with an in-process instance of a chain. The NodeClient isolates all network calls, so upgrading wallet communication from the current simple http interaction to a more secure protocol (or allowing for many options) should be a simple -matter of dropping in different `NodeClient` implementations. - -There are also two implementations of `WalletBackend` within the code at the base of the `wallet` crate. `LMDBBackend` found within -`wallet/src/lmdb_wallet.rs` is the main implementation, and is now used by all grin wallet commands. The earlier `FileWallet` still exists -within the code, however it is not invoked, and given there are no real advantages to running it over a DB implementation, development on it -has been dropped in favour of the LMDB implementation. \ No newline at end of file +matter of dropping in different `NodeClient` implementations. \ No newline at end of file diff --git a/doc/samples/v3_api_node/package-lock.json b/doc/samples/v3_api_node/package-lock.json index 4ccbc359b..5d6ddb088 100644 --- a/doc/samples/v3_api_node/package-lock.json +++ b/doc/samples/v3_api_node/package-lock.json @@ -1,115 +1,142 @@ { "name": "node-sample", "version": "0.0.1", - "lockfileVersion": 1, + "lockfileVersion": 3, "requires": true, - "dependencies": { - "@types/connect": { + "packages": { + "": { + "name": "node-sample", + "version": "0.0.1", + "license": "ISC", + "dependencies": { + "jayson": "^3.2.0" + } + }, + "node_modules/@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": { + "dependencies": { "@types/node": "*" } }, - "@types/express-serve-static-core": { + "node_modules/@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": { + "dependencies": { "@types/node": "*", "@types/range-parser": "*" } }, - "@types/lodash": { + "node_modules/@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": { + "node_modules/@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": { + "node_modules/@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": { + "node_modules/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": { + "node_modules/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": { + "node_modules/es6-promisify": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", - "requires": { + "dependencies": { "es6-promise": "^4.0.3" } }, - "eyes": { + "node_modules/eyes": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", - "integrity": "sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=" + "integrity": "sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=", + "engines": { + "node": "> 0.1.90" + } }, - "jayson": { + "node_modules/jayson": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/jayson/-/jayson-3.2.0.tgz", "integrity": "sha512-DZQnwA57GcStw4soSYB2VntWXFfoWvmSarlaWePDYOWhjxT72PBM4atEBomaTaS1uqk3jFC9UO9AyWjlujo3xw==", - "requires": { + "dependencies": { "@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", + "JSONStream": "^1.3.1", "lodash": "^4.17.15", "uuid": "^3.2.1" + }, + "bin": { + "jayson": "bin/jayson.js" } }, - "json-stringify-safe": { + "node_modules/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": { + "node_modules/jsonparse": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", - "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=" + "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", + "engines": [ + "node >= 0.2.0" + ] + }, + "node_modules/JSONStream": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "dependencies": { + "jsonparse": "^1.2.0", + "through": ">=2.2.7 <3" + }, + "bin": { + "JSONStream": "bin.js" + }, + "engines": { + "node": "*" + } }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==" }, - "through": { + "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" }, - "uuid": { + "node_modules/uuid": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "bin": { + "uuid": "bin/uuid" + } } } } diff --git a/doc/samples/v3_api_node/readme.md b/doc/samples/v3_api_node/readme.md index e08ccbfcb..ed8e907d5 100644 --- a/doc/samples/v3_api_node/readme.md +++ b/doc/samples/v3_api_node/readme.md @@ -10,19 +10,21 @@ First run the Owner API: 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: +The Owner API uses HTTP Basic authentication when `api_secret_path` is configured in `grin-wallet.toml`. Before running the sample, load the secret from that file into an environment variable: ```.sh -const client = jayson.client.http('http://localhost:3420/v3/owner'); +export GRIN_OWNER_API_SECRET="$(cat /path/to/.owner_api_secret)" ``` +The sample sends it as the password for the `grin` user. If `api_secret_path` is disabled, the environment variable can be omitted. + +Ensure the client connection settings in `src/index.js` match the Owner API address. + Then (assuming node.js and npm are installed on the system): ```.sh npm install -node src/index.json +node src/index.js ``` Feel free to play around with the sample, modifying it to call whatever functions you'd like to see in operation! diff --git a/doc/samples/v3_api_node/src/index.js b/doc/samples/v3_api_node/src/index.js index 6de1c5bec..a198d5b9f 100644 --- a/doc/samples/v3_api_node/src/index.js +++ b/doc/samples/v3_api_node/src/index.js @@ -8,7 +8,20 @@ const jayson = require('jayson/promise'); const crypto = require('crypto'); -const client = jayson.client.http('http://localhost:3420/v3/owner'); +const ownerApiSecret = process.env.GRIN_OWNER_API_SECRET; +const clientOptions = { + hostname: 'localhost', + port: 3420, + path: '/v3/owner', +}; + +if (ownerApiSecret) { + clientOptions.headers = { + Authorization: `Basic ${Buffer.from(`grin:${ownerApiSecret}`).toString('base64')}`, + }; +} + +const client = jayson.client.http(clientOptions); // Demo implementation of using `aes-256-gcm` with node.js's `crypto` lib. const aes256gcm = (shared_secret) => { @@ -33,8 +46,7 @@ const aes256gcm = (shared_secret) => { 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 decipher.update(text, 'binary', 'utf8') + decipher.final('utf8') }; return { @@ -69,8 +81,7 @@ class JSONRequestEncrypted { 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 + return aesCipher.decrypt(data, nonce2) } } @@ -101,7 +112,7 @@ async function main() { "password": "", }).send(shared_key); - let token = JSON.parse(response).result.Ok; + let token = JSON.parse(response).result; let iterations = 1; @@ -131,4 +142,3 @@ async function main() { main(); - diff --git a/grin b/grin new file mode 160000 index 000000000..bad44b4e9 --- /dev/null +++ b/grin @@ -0,0 +1 @@ +Subproject commit bad44b4e9bd7e37417d80cc469bcba316249bd93 diff --git a/impls/Cargo.toml b/impls/Cargo.toml index d7778519a..da64641cc 100644 --- a/impls/Cargo.toml +++ b/impls/Cargo.toml @@ -18,64 +18,45 @@ serde = "1" serde_derive = "1" serde_json = "1" log = "0.4" -ring = "0.16" -uuid = { version = "0.8", features = ["serde", "v4"] } +ring = "0.17.14" chrono = { version = "0.4.11", features = ["serde"] } lazy_static = "1" -tokio = { version = "0.2", features = ["full"] } -reqwest = { version = "0.10", features = ["rustls-tls", "socks"] } +tokio = { version = "1.52.3", features = ["full"] } +reqwest = { version = "0.13.4", default-features = false, features = ["socks", "native-tls", "system-proxy", "charset", "http2"] } #Socks/Tor/Bridge/Proxy -byteorder = "1" -ed25519-dalek = "1.0.0-pre.4" -x25519-dalek = "0.6" -data-encoding = "2" +ed25519-dalek = "2" regex = "1.3" timer = "0.2" sysinfo = "0.29" base64 = "0.12.0" url = "2.1" +arti-client = { version = "0.44.0", features = ["static", "pt-client", "onion-service-service", "onion-service-client"] } +tor-rtcompat = { version = "0.44.0", features = ["static"] } +fs-mistrust = "0.15.0" +fslock-guard = "0.8" +tor-hsservice = "0.44.0" +tor-hsrproxy = "0.44.0" +tor-keymgr = "0.44.0" +tor-llcrypto = "0.44.0" +tor-hscrypto = "0.44.0" +sha2 = "0.10.8" +hyper = { version = "1.10.1", features = ["http1", "client"] } +hyper-util = { version = "0.1.20", features = ["tokio"] } +http-body-util = "0.1.3" +bytes = "1.11.1" + grin_wallet_util = { path = "../util", version = "5.4.1" } grin_wallet_config = { path = "../config", version = "5.4.1" } grin_wallet_libwallet = { path = "../libwallet", version = "5.4.1" } ##### Grin Imports -# For Release -grin_core = "5.4.1" -grin_keychain = "5.4.1" -grin_chain = "5.4.1" -grin_util = "5.4.1" -grin_api = "5.4.1" -grin_store = "5.4.1" - -# 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"} - -##### +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"} -[dev-dependencies] -"remove_dir_all" = "0.7" \ No newline at end of file +##### \ No newline at end of file diff --git a/impls/src/adapters/mod.rs b/impls/src/adapters/mod.rs index 44fe8bf8b..361009e0d 100644 --- a/impls/src/adapters/mod.rs +++ b/impls/src/adapters/mod.rs @@ -13,12 +13,12 @@ // limitations under the License. mod file; -pub mod http; mod slatepack; +pub mod tor; pub use self::file::PathToSlate; -pub use self::http::HttpSlateSender; pub use self::slatepack::PathToSlatepack; +pub use self::tor::TorSlateSender; use crate::config::WalletConfig; use crate::libwallet::{Error, Slate}; diff --git a/impls/src/adapters/slatepack.rs b/impls/src/adapters/slatepack.rs index 850c3d537..2b04c23bf 100644 --- a/impls/src/adapters/slatepack.rs +++ b/impls/src/adapters/slatepack.rs @@ -104,7 +104,7 @@ mod tests { use grin_core::global; fn clean_output_dir(test_dir: &str) { - let _ = remove_dir_all::remove_dir_all(test_dir); + let _ = fs::remove_dir_all(test_dir); } fn setup(test_dir: &str) { diff --git a/impls/src/adapters/http.rs b/impls/src/adapters/tor.rs similarity index 52% rename from impls/src/adapters/http.rs rename to impls/src/adapters/tor.rs index e96ff5004..8975ad021 100644 --- a/impls/src/adapters/http.rs +++ b/impls/src/adapters/tor.rs @@ -12,128 +12,108 @@ // 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::arti::{start_tor_client, tor_post}; use crate::tor::bridge::TorBridge; +use crate::tor::config as tor_config; +use crate::tor::process::TorProcess; use crate::tor::proxy::TorProxy; use crate::SlateSender; -use grin_wallet_config::types::{TorBridgeConfig, TorProxyConfig}; +use grin_wallet_config::TorConfig; 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::path::PathBuf; 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 { +pub struct TorSlateSender { base_url: String, - use_socks: bool, - socks_proxy_addr: Option, - tor_config_dir: String, - process: Option>, - bridge: TorBridgeConfig, - proxy: TorProxyConfig, + config: TorConfig, + tor_process: Arc>, } -impl HttpSlateSender { +impl TorSlateSender { /// Create, return Err if scheme is not "http" - fn new(base_url: &str) -> Result { + pub fn new(base_url: &str, config: TorConfig) -> Result { if !base_url.starts_with("http") && !base_url.starts_with("https") { - Err(SchemeNotHttp) + Err(Error::GenericError("Scheme must be http".to_string())) } else { - Ok(HttpSlateSender { + let tor = if config.use_integrated.unwrap_or(false) { + start_tor_client(config.clone())?; + None + } else { + let tor_dir = { + let mut path = PathBuf::from(&config.send_config_dir); + path.push("tor"); + path.push("sender"); + path + }; + let p = Self::launch_tor_process(&config, &tor_dir)?; + Some(p) + }; + Ok(TorSlateSender { 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(), + config, + tor_process: Arc::new(tor), }) } } - /// Switch to using socks proxy - pub fn with_socks_proxy( - base_url: &str, - proxy_addr: &str, - tor_config_dir: &str, - tor_bridge: TorBridgeConfig, - tor_proxy: TorProxyConfig, - ) -> Result { - let mut ret = Self::new(base_url)?; - ret.use_socks = true; - //TODO: Unwrap - ret.socks_proxy_addr = Some(SocketAddr::V4(proxy_addr.parse().unwrap())); - ret.tor_config_dir = tor_config_dir.into(); - ret.bridge = tor_bridge; - ret.proxy = tor_proxy; - Ok(ret) - } + /// Launch external Tor process. + fn launch_tor_process(config: &TorConfig, tor_dir: &PathBuf) -> Result { + let mut tor = TorProcess::new(); + let socks_proxy_addr = SocketAddr::V4( + config + .socks_proxy_addr + .parse() + .map_err(|e| Error::TorConfig(format!("{:?}", e)))?, + ); + info!("Starting TOR Process for send at {:?}", socks_proxy_addr); - /// launch TOR process - pub fn launch_tor(&mut self) -> Result<(), Error> { - // set up tor send process if needed - let mut tor = tor_process::TorProcess::new(); - if self.use_socks && self.process.is_none() { - let tor_dir = format!( - "{}{}{}", - &self.tor_config_dir, MAIN_SEPARATOR, TOR_CONFIG_PATH - ); - info!( - "Starting TOR Process for send at {:?}", - self.socks_proxy_addr - ); - - let mut hm_tor_bridge: HashMap = HashMap::new(); - if self.bridge.bridge_line.is_some() { - let bridge_struct = TorBridge::try_from(self.bridge.clone()) - .map_err(|e| Error::TorConfig(format!("{:?}", e)))?; - hm_tor_bridge = bridge_struct - .to_hashmap() - .map_err(|e| Error::TorConfig(format!("{:?}", e)))?; - } - - let mut hm_tor_proxy: HashMap = HashMap::new(); - if self.proxy.transport.is_some() || self.proxy.allowed_port.is_some() { - let proxy = TorProxy::try_from(self.proxy.clone()) - .map_err(|e| Error::TorConfig(format!("{:?}", e)))?; - hm_tor_proxy = proxy - .to_hashmap() - .map_err(|e| Error::TorConfig(format!("{:?}", e)))?; - } + let mut hm_tor_bridge: HashMap = HashMap::new(); + if config.bridge.bridge_line.is_some() { + let bridge_struct = TorBridge::try_from(config.bridge.clone()) + .map_err(|e| Error::TorConfig(format!("{:?}", e)))?; + hm_tor_bridge = bridge_struct + .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)); + let mut hm_tor_proxy: HashMap = HashMap::new(); + if config.proxy.transport.is_some() || config.proxy.allowed_port.is_some() { + let proxy = TorProxy::try_from(config.proxy.clone()) + .map_err(|e| Error::TorConfig(format!("{:?}", e)))?; + hm_tor_proxy = proxy + .to_hashmap() + .map_err(|e| Error::TorConfig(format!("{:?}", e)))?; } - Ok(()) + + tor_config::output_tor_sender_config( + tor_dir.to_str().unwrap(), + socks_proxy_addr.to_string().as_str(), + hm_tor_bridge, + hm_tor_proxy, + ) + .map_err(|e| Error::TorConfig(format!("{:?}", e)))?; + // Start TOR process + let mut path = tor_dir.clone(); + path.push("torrc"); + tor.torrc_path(path.to_str().unwrap()) + .working_dir(tor_dir.to_str().unwrap()) + .timeout(config.bootstrap_timeout().as_secs()) + .completion_percent(100) + .launch() + .map_err(|e| Error::TorProcess(format!("{:?}", e)))?; + Ok(tor) } /// Check version of the listening wallet pub fn check_other_version(&mut self, url: &str) -> Result { - self.launch_tor()?; let req = json!({ "jsonrpc": "2.0", "method": "check_version", @@ -141,7 +121,7 @@ impl HttpSlateSender { "params": [] }); - let res: String = self.post(url, None, req).map_err(|e| { + let res: String = self.post(url, 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") { @@ -154,7 +134,8 @@ impl HttpSlateSender { Error::ClientCallback(report) })?; - let res: Value = serde_json::from_str(&res).unwrap(); + let res: Value = serde_json::from_str(&res) + .map_err(|_| Error::ClientCallback(format!("Can not parse response {:?}", res)))?; trace!("Response: {}", res); if res["error"] != json!(null) { let report = format!( @@ -168,9 +149,11 @@ impl HttpSlateSender { 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(); + serde_json::from_value(resp_value["foreign_api_version"].clone()) + .map_err(|_| Error::ClientCallback(format!("Can not parse response {:?}", res)))?; let supported_slate_versions: Vec = - serde_json::from_value(resp_value["supported_slate_versions"].clone()).unwrap(); + serde_json::from_value(resp_value["supported_slate_versions"].clone()) + .map_err(|_| Error::ClientCallback(format!("Can not parse response {:?}", res)))?; // trivial tests for now, but will be expanded later if foreign_api_version < 2 { @@ -179,6 +162,10 @@ impl HttpSlateSender { return Err(Error::ClientCallback(report)); } + if supported_slate_versions.contains(&"V5".to_owned()) { + return Ok(SlateVersion::V5); + } + if supported_slate_versions.contains(&"V4".to_owned()) { return Ok(SlateVersion::V4); } @@ -188,32 +175,30 @@ impl HttpSlateSender { Err(Error::ClientCallback(report)) } - fn post( - &self, - url: &str, - api_secret: Option, - input: IN, - ) -> Result + fn post(&self, url: &str, input: IN) -> Result where IN: Serialize, { - let client = if !self.use_socks { - Client::new() + let res = if self.tor_process.is_some() { + let socks_proxy_addr = + SocketAddr::V4(self.config.socks_proxy_addr.parse().map_err(|_| { + ClientError::Internal("Socks proxy address is not set".to_string()) + })?); + let timeout = self.config.request_timeout(); + let client = Client::with_proxy(socks_proxy_addr, "socks5h://", timeout) + .map_err(|_| ClientError::Internal("Unable to create http client".into()))?; + let req = client.create_post_request(url, None, &input)?; + let res = client.send_request(req)?; + res } else { - Client::with_proxy( - self.socks_proxy_addr - .ok_or_else(|| ClientError::Internal("No socks proxy address set".into()))?, - "socks5h://", - ) - } - .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)?; + tor_post(&self.config, &input, url) + .map_err(|e| ClientError::RequestError(format!("{:?}", e)))? + }; Ok(res) } } -impl SlateSender for HttpSlateSender { +impl SlateSender for TorSlateSender { fn send_tx(&mut self, slate: &Slate, finalize: bool) -> Result { let trailing = match self.base_url.ends_with('/') { true => "", @@ -221,9 +206,8 @@ impl SlateSender for HttpSlateSender { }; let url_str = format!("{}{}v2/foreign", self.base_url, trailing); - self.launch_tor()?; - let slate_send = match self.check_other_version(&url_str)? { + SlateVersion::V5 => VersionedSlate::into_version(slate.clone(), SlateVersion::V5)?, SlateVersion::V4 => VersionedSlate::into_version(slate.clone(), SlateVersion::V4)?, }; // Note: not using easy-jsonrpc as don't want the dependencies in this crate @@ -250,7 +234,7 @@ impl SlateSender for HttpSlateSender { trace!("Sending receive_tx request: {}", req); - let res: String = self.post(&url_str, None, req).map_err(|e| { + let res: String = self.post(&url_str, req).map_err(|e| { let report = format!( "Sending transaction slate to other wallet (is recipient listening?): {}", e @@ -258,7 +242,8 @@ impl SlateSender for HttpSlateSender { Error::ClientCallback(report) })?; - let res: Value = serde_json::from_str(&res).unwrap(); + let res: Value = serde_json::from_str(&res) + .map_err(|_| Error::ClientCallback(format!("Can not parse response {:?}", res)))?; trace!("Response: {}", res); if res["error"] != json!(null) { let report = format!( @@ -272,22 +257,15 @@ impl SlateSender for HttpSlateSender { 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| { + if let Ok(slate_value) = serde_json::to_string(&slate_value) { + let slate = Slate::deserialize_upgrade(&slate_value).map_err(|e| { error!("Error deserializing response slate: {}", e); Error::SlateDeser })?; - - Ok(slate) - } -} - -#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] -pub struct SchemeNotHttp; - -impl Into for SchemeNotHttp { - fn into(self) -> Error { - let err_str = "url scheme must be http".to_string(); - Error::GenericError(err_str) + Ok(slate) + } else { + error!("Error deserializing response slate: {}", slate_value); + Err(Error::SlateDeser) + } } } diff --git a/impls/src/backends/lmdb.rs b/impls/src/backends/lmdb.rs deleted file mode 100644 index e0df2691d..000000000 --- a/impls/src/backends/lmdb.rs +++ /dev/null @@ -1,777 +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( - keychain: &K, - slate_id: &[u8], -) -> Result<([u8; SECRET_KEY_SIZE], [u8; SECRET_KEY_SIZE]), Error> -where - K: Keychain, -{ - let root_key = keychain.derive_key(0, &K::root_key_id(), SwitchCommitmentType::Regular)?; - - // derive XOR values for storing secret values in DB - // h(root_key|slate_id|"blind") - let mut hasher = Blake2b::new(SECRET_KEY_SIZE); - hasher.update(&root_key.0[..]); - hasher.update(&slate_id[..]); - hasher.update(&b"blind"[..]); - let blind_xor_key = hasher.finalize(); - let mut ret_blind = [0; SECRET_KEY_SIZE]; - ret_blind.copy_from_slice(&blind_xor_key.as_bytes()[0..SECRET_KEY_SIZE]); - - // h(root_key|slate_id|"nonce") - let mut hasher = Blake2b::new(SECRET_KEY_SIZE); - hasher.update(&root_key.0[..]); - hasher.update(&slate_id[..]); - hasher.update(&b"nonce"[..]); - let nonce_xor_key = hasher.finalize(); - let mut ret_nonce = [0; SECRET_KEY_SIZE]; - ret_nonce.copy_from_slice(&nonce_xor_key.as_bytes()[0..SECRET_KEY_SIZE]); - - Ok((ret_blind, ret_nonce)) -} - -pub struct LMDBBackend<'ck, C, K> -where - C: NodeClient + 'ck, - K: Keychain + 'ck, -{ - db: store::Store, - data_file_dir: String, - /// Keychain - pub keychain: Option, - /// Check value for XORed keychain seed - pub master_checksum: Box>, - /// Parent path to use by default for output operations - parent_key_id: Identifier, - /// wallet to node client - w2n_client: C, - ///phantom - _phantom: &'ck PhantomData, -} - -impl<'ck, C, K> LMDBBackend<'ck, C, K> -where - C: NodeClient + 'ck, - K: Keychain + 'ck, -{ - pub fn new(data_file_dir: &str, n_client: C) -> Result { - let db_path = path::Path::new(data_file_dir).join(DB_DIR); - fs::create_dir_all(&db_path).expect("Couldn't create wallet backend directory!"); - - let stored_tx_path = path::Path::new(data_file_dir).join(TX_SAVE_DIR); - fs::create_dir_all(&stored_tx_path) - .expect("Couldn't create wallet backend tx storage directory!"); - - let store = store::Store::new(db_path.to_str().unwrap(), None, Some(DB_DIR), None)?; - - // Make sure default wallet derivation path always exists - // as well as path (so it can be retrieved by batches to know where to store - // completed transactions, for reference - let default_account = AcctPathMapping { - label: "default".to_owned(), - path: LMDBBackend::::default_path(), - }; - let acct_key = to_key( - ACCOUNT_PATH_MAPPING_PREFIX, - &mut default_account.label.as_bytes().to_vec(), - ); - - { - let batch = store.batch()?; - batch.put_ser(&acct_key, &default_account)?; - batch.commit()?; - } - - let res = LMDBBackend { - db: store, - data_file_dir: data_file_dir.to_owned(), - keychain: None, - master_checksum: Box::new(None), - parent_key_id: LMDBBackend::::default_path(), - w2n_client: n_client, - _phantom: &PhantomData, - }; - Ok(res) - } - - fn default_path() -> Identifier { - // return the default parent wallet path, corresponding to the default account - // in the BIP32 spec. Parent is account 0 at level 2, child output identifiers - // are all at level 3 - ExtKeychain::derive_key_id(2, 0, 0, 0, 0) - } - - /// Just test to see if database files exist in the current directory. If - /// so, use a DB backend for all operations - pub fn exists(data_file_dir: &str) -> bool { - let db_path = path::Path::new(data_file_dir).join(DB_DIR); - db_path.exists() - } -} - -impl<'ck, C, K> WalletBackend<'ck, C, K> for LMDBBackend<'ck, C, K> -where - C: NodeClient + 'ck, - K: Keychain + 'ck, -{ - /// Set the keychain, which should already have been opened - fn set_keychain( - &mut self, - mut k: Box, - mask: bool, - use_test_rng: bool, - ) -> Result, Error> { - // store hash of master key, so it can be verified later after unmasking - let root_key = k.derive_key(0, &K::root_key_id(), SwitchCommitmentType::Regular)?; - let mut hasher = Blake2b::new(SECRET_KEY_SIZE); - hasher.update(&root_key.0[..]); - self.master_checksum = Box::new(Some(hasher.finalize())); - - let mask_value = { - match mask { - true => { - // Random value that must be XORed against the stored wallet seed - // before it is used - let mask_value = match use_test_rng { - true => { - let mut test_rng = StepRng::new(1_234_567_890_u64, 1); - secp::key::SecretKey::new(&k.secp(), &mut test_rng) - } - false => secp::key::SecretKey::new(&k.secp(), &mut thread_rng()), - }; - k.mask_master_key(&mask_value)?; - Some(mask_value) - } - false => None, - } - }; - - self.keychain = Some(*k); - Ok(mask_value) - } - - /// Close wallet - fn close(&mut self) -> Result<(), Error> { - self.keychain = None; - Ok(()) - } - - /// Return the keychain being used, cloned with XORed token value - /// for temporary use - fn keychain(&self, mask: Option<&SecretKey>) -> Result { - match self.keychain.as_ref() { - Some(k) => { - let mut k_masked = k.clone(); - if let Some(m) = mask { - k_masked.mask_master_key(m)?; - } - // Check if master seed is what is expected (especially if it's been xored) - let root_key = - k_masked.derive_key(0, &K::root_key_id(), SwitchCommitmentType::Regular)?; - let mut hasher = Blake2b::new(SECRET_KEY_SIZE); - hasher.update(&root_key.0[..]); - if *self.master_checksum != Some(hasher.finalize()) { - error!("Supplied keychain mask is invalid"); - return Err(Error::InvalidKeychainMask); - } - Ok(k_masked) - } - None => Err(Error::KeychainDoesntExist), - } - } - - /// Return the node client being used - fn w2n_client(&mut self) -> &mut C { - &mut self.w2n_client - } - - /// return the version of the commit for caching - fn calc_commit_for_cache( - &mut self, - keychain_mask: Option<&SecretKey>, - amount: u64, - id: &Identifier, - ) -> Result, Error> { - //TODO: Check if this is really necessary, it's the only thing - //preventing removing the need for config in the wallet backend - /*if self.config.no_commit_cache == Some(true) { - Ok(None) - } else {*/ - Ok(Some( - self.keychain(keychain_mask)? - .commit(amount, &id, SwitchCommitmentType::Regular)? - .0 - .to_vec() - .to_hex(), // TODO: proper support for different switch commitment schemes - )) - /*}*/ - } - - /// Set parent path by account name - fn set_parent_key_id_by_name(&mut self, label: &str) -> Result<(), Error> { - let label = label.to_owned(); - let res = self.acct_path_iter().find(|l| l.label == label); - if let Some(a) = res { - self.set_parent_key_id(a.path); - Ok(()) - } else { - Err(Error::UnknownAccountLabel(label)) - } - } - - /// set parent path - fn set_parent_key_id(&mut self, id: Identifier) { - self.parent_key_id = id; - } - - fn parent_key_id(&mut self) -> Identifier { - self.parent_key_id.clone() - } - - fn get(&self, id: &Identifier, mmr_index: &Option) -> Result { - let key = match mmr_index { - Some(i) => to_key_u64(OUTPUT_PREFIX, &mut id.to_bytes().to_vec(), *i), - None => to_key(OUTPUT_PREFIX, &mut id.to_bytes().to_vec()), - }; - option_to_not_found(self.db.get_ser(&key, None), || format!("Key Id: {}", id)) - .map_err(|e| e.into()) - } - - // TODO - fix this awkward conversion between PrefixIterator and our Box - fn iter<'a>(&'a self) -> Box + 'a> { - let protocol_version = self.db.protocol_version(); - let prefix_iter = self.db.iter(&[OUTPUT_PREFIX], move |_, mut v| { - ser::deserialize( - &mut v, - protocol_version, - ser::DeserializationMode::default(), - ) - .map_err(From::from) - }); - let iter = prefix_iter.expect("deserialize").into_iter(); - Box::new(iter) - } - - fn get_tx_log_entry(&self, u: &Uuid) -> Result, Error> { - let key = to_key(TX_LOG_ENTRY_PREFIX, &mut u.as_bytes().to_vec()); - self.db.get_ser(&key, None).map_err(|e| e.into()) - } - - // TODO - fix this awkward conversion between PrefixIterator and our Box - fn tx_log_iter<'a>(&'a self) -> Box + 'a> { - let protocol_version = self.db.protocol_version(); - let prefix_iter = self.db.iter(&[TX_LOG_ENTRY_PREFIX], move |_, mut v| { - ser::deserialize( - &mut v, - protocol_version, - ser::DeserializationMode::default(), - ) - .map_err(From::from) - }); - let iter = prefix_iter.expect("deserialize").into_iter(); - Box::new(iter) - } - - fn get_private_context( - &mut self, - keychain_mask: Option<&SecretKey>, - slate_id: &[u8], - ) -> Result { - let ctx_key = to_key_u64(PRIVATE_TX_CONTEXT_PREFIX, &mut slate_id.to_vec(), 0); - let (blind_xor_key, nonce_xor_key) = - private_ctx_xor_keys(&self.keychain(keychain_mask)?, slate_id)?; - - let mut ctx: Context = option_to_not_found(self.db.get_ser(&ctx_key, None), || { - format!("Slate id: {:x?}", slate_id.to_vec()) - })?; - - for i in 0..SECRET_KEY_SIZE { - ctx.sec_key.0[i] ^= blind_xor_key[i]; - ctx.sec_nonce.0[i] ^= nonce_xor_key[i]; - } - - Ok(ctx) - } - - // TODO - fix this awkward conversion between PrefixIterator and our Box - fn acct_path_iter<'a>(&'a self) -> Box + 'a> { - let protocol_version = self.db.protocol_version(); - let prefix_iter = self - .db - .iter(&[ACCOUNT_PATH_MAPPING_PREFIX], move |_, mut v| { - ser::deserialize( - &mut v, - protocol_version, - ser::DeserializationMode::default(), - ) - .map_err(From::from) - }); - let iter = prefix_iter.expect("deserialize").into_iter(); - Box::new(iter) - } - - fn get_acct_path(&self, label: String) -> Result, Error> { - let acct_key = to_key(ACCOUNT_PATH_MAPPING_PREFIX, &mut label.as_bytes().to_vec()); - self.db.get_ser(&acct_key, None).map_err(|e| e.into()) - } - - fn store_tx(&self, uuid: &str, tx: &Transaction) -> Result<(), Error> { - let filename = format!("{}.grintx", uuid); - let path = path::Path::new(&self.data_file_dir) - .join(TX_SAVE_DIR) - .join(filename); - let path_buf = Path::new(&path).to_path_buf(); - let mut stored_tx = File::create(path_buf)?; - let tx_hex = ser::ser_vec(tx, ser::ProtocolVersion(1)).unwrap().to_hex(); - stored_tx.write_all(&tx_hex.as_bytes())?; - stored_tx.sync_all()?; - Ok(()) - } - - fn get_stored_tx(&self, uuid: &str) -> Result, Error> { - let filename = format!("{}.grintx", uuid); - let path = path::Path::new(&self.data_file_dir) - .join(TX_SAVE_DIR) - .join(filename); - let tx_file = Path::new(&path).to_path_buf(); - let mut tx_f = File::open(tx_file)?; - let mut content = String::new(); - tx_f.read_to_string(&mut content)?; - let tx_bin = util::from_hex(&content).unwrap(); - Ok(Some( - ser::deserialize( - &mut &tx_bin[..], - ser::ProtocolVersion(1), - ser::DeserializationMode::default(), - ) - .unwrap(), - )) - } - - fn batch<'a>( - &'a mut self, - keychain_mask: Option<&SecretKey>, - ) -> Result + 'a>, Error> { - Ok(Box::new(Batch { - _store: self, - db: RefCell::new(Some(self.db.batch()?)), - keychain: Some(self.keychain(keychain_mask)?), - })) - } - - fn batch_no_mask<'a>(&'a mut self) -> Result + 'a>, Error> { - Ok(Box::new(Batch { - _store: self, - db: RefCell::new(Some(self.db.batch()?)), - keychain: None, - })) - } - - fn current_child_index<'a>(&mut self, parent_key_id: &Identifier) -> Result { - let index = { - let batch = self.db.batch()?; - let deriv_key = to_key(DERIV_PREFIX, &mut parent_key_id.to_bytes().to_vec()); - match batch.get_ser(&deriv_key, None)? { - Some(idx) => idx, - None => 0, - } - }; - Ok(index) - } - - fn next_child<'a>(&mut self, keychain_mask: Option<&SecretKey>) -> Result { - let parent_key_id = self.parent_key_id.clone(); - let mut deriv_idx = { - let batch = self.db.batch()?; - let deriv_key = to_key(DERIV_PREFIX, &mut self.parent_key_id.to_bytes().to_vec()); - match batch.get_ser(&deriv_key, None)? { - Some(idx) => idx, - None => 0, - } - }; - let mut return_path = self.parent_key_id.to_path(); - return_path.depth += 1; - return_path.path[return_path.depth as usize - 1] = ChildNumber::from(deriv_idx); - deriv_idx += 1; - let mut batch = self.batch(keychain_mask)?; - batch.save_child_index(&parent_key_id, deriv_idx)?; - batch.commit()?; - Ok(Identifier::from_path(&return_path)) - } - - fn last_confirmed_height<'a>(&mut self) -> Result { - let batch = self.db.batch()?; - let height_key = to_key( - CONFIRMED_HEIGHT_PREFIX, - &mut self.parent_key_id.to_bytes().to_vec(), - ); - let last_confirmed_height = match batch.get_ser(&height_key, None)? { - Some(h) => h, - None => 0, - }; - Ok(last_confirmed_height) - } - - fn last_scanned_block<'a>(&mut self) -> Result { - let batch = self.db.batch()?; - let scanned_block_key = to_key( - LAST_SCANNED_BLOCK, - &mut LAST_SCANNED_KEY.as_bytes().to_vec(), - ); - let last_scanned_block = match batch.get_ser(&scanned_block_key, None)? { - Some(b) => b, - None => ScannedBlockInfo { - height: 0, - hash: "".to_owned(), - start_pmmr_index: 0, - last_pmmr_index: 0, - }, - }; - Ok(last_scanned_block) - } - - fn init_status<'a>(&mut self) -> Result { - let batch = self.db.batch()?; - let init_status_key = to_key( - WALLET_INIT_STATUS, - &mut WALLET_INIT_STATUS_KEY.as_bytes().to_vec(), - ); - let status = match batch.get_ser(&init_status_key, None)? { - Some(s) => s, - None => WalletInitStatus::InitComplete, - }; - Ok(status) - } -} - -/// An atomic batch in which all changes can be committed all at once or -/// discarded on error. -pub struct Batch<'a, C, K> -where - C: NodeClient, - K: Keychain, -{ - _store: &'a LMDBBackend<'a, C, K>, - db: RefCell>>, - /// Keychain - keychain: Option, -} - -#[allow(missing_docs)] -impl<'a, C, K> WalletOutputBatch for Batch<'a, C, K> -where - C: NodeClient, - K: Keychain, -{ - fn keychain(&mut self) -> &mut K { - self.keychain.as_mut().unwrap() - } - - fn save(&mut self, out: OutputData) -> Result<(), Error> { - // Save the output data to the db. - { - let key = match out.mmr_index { - Some(i) => to_key_u64(OUTPUT_PREFIX, &mut out.key_id.to_bytes().to_vec(), i), - None => to_key(OUTPUT_PREFIX, &mut out.key_id.to_bytes().to_vec()), - }; - self.db.borrow().as_ref().unwrap().put_ser(&key, &out)?; - } - - Ok(()) - } - - fn get(&self, id: &Identifier, mmr_index: &Option) -> Result { - let key = match mmr_index { - Some(i) => to_key_u64(OUTPUT_PREFIX, &mut id.to_bytes().to_vec(), *i), - None => to_key(OUTPUT_PREFIX, &mut id.to_bytes().to_vec()), - }; - option_to_not_found( - self.db.borrow().as_ref().unwrap().get_ser(&key, None), - || format!("Key ID: {}", id), - ) - .map_err(|e| e.into()) - } - - // TODO - fix this awkward conversion between PrefixIterator and our Box - fn iter(&self) -> Box> { - let db = self.db.borrow(); - let db = db.as_ref().unwrap(); - let protocol_version = db.protocol_version(); - let prefix_iter = db.iter(&[OUTPUT_PREFIX], move |_, mut v| { - ser::deserialize( - &mut v, - protocol_version, - ser::DeserializationMode::default(), - ) - .map_err(From::from) - }); - let iter = prefix_iter.expect("deserialize").into_iter(); - Box::new(iter) - } - - fn delete(&mut self, id: &Identifier, mmr_index: &Option) -> Result<(), Error> { - // Delete the output data. - { - let key = match mmr_index { - Some(i) => to_key_u64(OUTPUT_PREFIX, &mut id.to_bytes().to_vec(), *i), - None => to_key(OUTPUT_PREFIX, &mut id.to_bytes().to_vec()), - }; - let _ = self.db.borrow().as_ref().unwrap().delete(&key); - } - - Ok(()) - } - - fn next_tx_log_id(&mut self, parent_key_id: &Identifier) -> Result { - let tx_id_key = to_key(TX_LOG_ID_PREFIX, &mut parent_key_id.to_bytes().to_vec()); - let last_tx_log_id = match self - .db - .borrow() - .as_ref() - .unwrap() - .get_ser(&tx_id_key, None)? - { - Some(t) => t, - None => 0, - }; - self.db - .borrow() - .as_ref() - .unwrap() - .put_ser(&tx_id_key, &(last_tx_log_id + 1))?; - Ok(last_tx_log_id) - } - - // TODO - fix this awkward conversion between PrefixIterator and our Box - fn tx_log_iter(&self) -> Box> { - let db = self.db.borrow(); - let db = db.as_ref().unwrap(); - let protocol_version = db.protocol_version(); - let prefix_iter = db.iter(&[TX_LOG_ENTRY_PREFIX], move |_, mut v| { - ser::deserialize( - &mut v, - protocol_version, - ser::DeserializationMode::default(), - ) - .map_err(From::from) - }); - let iter = prefix_iter.expect("deserialize").into_iter(); - Box::new(iter) - } - - fn save_last_confirmed_height( - &mut self, - parent_key_id: &Identifier, - height: u64, - ) -> Result<(), Error> { - let height_key = to_key( - CONFIRMED_HEIGHT_PREFIX, - &mut parent_key_id.to_bytes().to_vec(), - ); - self.db - .borrow() - .as_ref() - .unwrap() - .put_ser(&height_key, &height)?; - Ok(()) - } - - fn save_last_scanned_block(&mut self, block_info: ScannedBlockInfo) -> Result<(), Error> { - let pmmr_index_key = to_key( - LAST_SCANNED_BLOCK, - &mut LAST_SCANNED_KEY.as_bytes().to_vec(), - ); - self.db - .borrow() - .as_ref() - .unwrap() - .put_ser(&pmmr_index_key, &block_info)?; - Ok(()) - } - - fn save_init_status(&mut self, value: WalletInitStatus) -> Result<(), Error> { - let init_status_key = to_key( - WALLET_INIT_STATUS, - &mut WALLET_INIT_STATUS_KEY.as_bytes().to_vec(), - ); - self.db - .borrow() - .as_ref() - .unwrap() - .put_ser(&init_status_key, &value)?; - Ok(()) - } - - fn save_child_index(&mut self, parent_id: &Identifier, child_n: u32) -> Result<(), Error> { - let deriv_key = to_key(DERIV_PREFIX, &mut parent_id.to_bytes().to_vec()); - self.db - .borrow() - .as_ref() - .unwrap() - .put_ser(&deriv_key, &child_n)?; - Ok(()) - } - - fn save_tx_log_entry( - &mut self, - tx_in: TxLogEntry, - parent_id: &Identifier, - ) -> Result<(), Error> { - let tx_log_key = to_key_u64( - TX_LOG_ENTRY_PREFIX, - &mut parent_id.to_bytes().to_vec(), - tx_in.id as u64, - ); - self.db - .borrow() - .as_ref() - .unwrap() - .put_ser(&tx_log_key, &tx_in)?; - Ok(()) - } - - fn delete_tx_log_entry(&mut self, tx_id: u32, parent_id: &Identifier) -> Result<(), Error> { - let tx_log_key = to_key_u64( - TX_LOG_ENTRY_PREFIX, - &mut parent_id.to_bytes().to_vec(), - tx_id as u64, - ); - self.db.borrow().as_ref().unwrap().delete(&tx_log_key)?; - Ok(()) - } - - fn save_acct_path(&mut self, mapping: AcctPathMapping) -> Result<(), Error> { - let acct_key = to_key( - ACCOUNT_PATH_MAPPING_PREFIX, - &mut mapping.label.as_bytes().to_vec(), - ); - self.db - .borrow() - .as_ref() - .unwrap() - .put_ser(&acct_key, &mapping)?; - Ok(()) - } - - // TODO - fix this awkward conversion between PrefixIterator and our Box - fn acct_path_iter(&self) -> Box> { - let db = self.db.borrow(); - let db = db.as_ref().unwrap(); - let protocol_version = db.protocol_version(); - let prefix_iter = db.iter(&[ACCOUNT_PATH_MAPPING_PREFIX], move |_, mut v| { - ser::deserialize( - &mut v, - protocol_version, - ser::DeserializationMode::default(), - ) - .map_err(From::from) - }); - let iter = prefix_iter.expect("deserialize").into_iter(); - Box::new(iter) - } - - fn lock_output(&mut self, out: &mut OutputData) -> Result<(), Error> { - out.lock(); - self.save(out.clone()) - } - - fn save_private_context(&mut self, slate_id: &[u8], ctx: &Context) -> Result<(), Error> { - let ctx_key = to_key_u64(PRIVATE_TX_CONTEXT_PREFIX, &mut slate_id.to_vec(), 0); - let (blind_xor_key, nonce_xor_key) = private_ctx_xor_keys(self.keychain(), slate_id)?; - - let mut s_ctx = ctx.clone(); - for i in 0..SECRET_KEY_SIZE { - s_ctx.sec_key.0[i] ^= blind_xor_key[i]; - s_ctx.sec_nonce.0[i] ^= nonce_xor_key[i]; - } - - self.db - .borrow() - .as_ref() - .unwrap() - .put_ser(&ctx_key, &s_ctx)?; - Ok(()) - } - - fn delete_private_context(&mut self, slate_id: &[u8]) -> Result<(), Error> { - let ctx_key = to_key_u64(PRIVATE_TX_CONTEXT_PREFIX, &mut slate_id.to_vec(), 0); - self.db - .borrow() - .as_ref() - .unwrap() - .delete(&ctx_key) - .map_err(|e| e.into()) - } - - fn commit(&self) -> Result<(), Error> { - let db = self.db.replace(None); - db.unwrap().commit()?; - Ok(()) - } -} diff --git a/impls/src/client_utils/client.rs b/impls/src/client_utils/client.rs index ec0ee0ff9..94b7b37ee 100644 --- a/impls/src/client_utils/client.rs +++ b/impls/src/client_utils/client.rs @@ -21,23 +21,14 @@ use reqwest::{ClientBuilder, Method, Proxy, RequestBuilder}; use serde::{Deserialize, Serialize}; use serde_json; use std::net::SocketAddr; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; use std::time::Duration; use tokio::runtime::{Builder, Handle, Runtime}; // Global Tokio runtime. -// Needs a `Mutex` because `Runtime::block_on` requires mutable access. -// Tokio v0.3 requires immutable self, but we are waiting on upstream -// updates before we can upgrade. -// See: https://github.com/seanmonstar/reqwest/pull/1076 lazy_static! { - pub static ref RUNTIME: Arc> = Arc::new(Mutex::new( - Builder::new() - .threaded_scheduler() - .enable_all() - .build() - .unwrap() - )); + pub static ref RUNTIME: Arc = + Arc::new(Builder::new_multi_thread().enable_all().build().unwrap()); } #[derive(Clone, Eq, thiserror::Error, PartialEq, Debug)] @@ -61,23 +52,26 @@ pub struct Client { impl Client { /// New client - pub fn new() -> Result { - Self::build(None) + pub fn new(request_timeout: Duration) -> Result { + Self::build(None, request_timeout) } - pub fn with_proxy(socks_proxy_addr: SocketAddr, scheme: &'static str) -> Result { - Self::build(Some((socks_proxy_addr, scheme))) + pub fn with_proxy( + socks_proxy_addr: SocketAddr, + scheme: &'static str, + request_timeout: Duration, + ) -> Result { + Self::build(Some((socks_proxy_addr, scheme)), request_timeout) } - fn build(proxy: Option<(SocketAddr, &str)>) -> Result { + fn build(proxy: Option<(SocketAddr, &str)>, request_timeout: Duration) -> Result { let mut headers = HeaderMap::new(); headers.insert(USER_AGENT, HeaderValue::from_static("grin-client")); headers.insert(ACCEPT, HeaderValue::from_static("application/json")); headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); let mut builder = ClientBuilder::new() - .timeout(Duration::from_secs(20)) - .use_rustls_tls() + .timeout(request_timeout) .default_headers(headers); if let Some(p) = proxy { @@ -273,18 +267,11 @@ impl Client { 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() + std::thread::spawn(move || rt.block_on(async { client.send_request_async(req).await })) + .join() .unwrap() - .block_on(self.send_request_async(req)) + } else { + RUNTIME.block_on(self.send_request_async(req)) } } } diff --git a/impls/src/lib.rs b/impls/src/lib.rs index b18ad7c36..9bbedfaaa 100644 --- a/impls/src/lib.rs +++ b/impls/src/lib.rs @@ -28,14 +28,12 @@ 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; @@ -44,10 +42,9 @@ pub mod test_framework; pub mod tor; pub use crate::adapters::{ - HttpSlateSender, PathToSlate, PathToSlatepack, SlateGetter, SlatePutter, SlateReceiver, - SlateSender, + PathToSlate, PathToSlatepack, SlateGetter, SlatePutter, SlateReceiver, SlateSender, + TorSlateSender, }; -pub use crate::backends::{wallet_db_exists, LMDBBackend}; pub use crate::error::Error; pub use crate::lifecycle::DefaultLCProvider; pub use crate::node_clients::HTTPNodeClient; @@ -57,35 +54,31 @@ use crate::keychain::{ExtKeychain, Keychain}; use libwallet::{NodeClient, WalletInst, WalletLCProvider}; /// Main wallet instance -pub struct DefaultWalletImpl<'a, C> +pub struct DefaultWalletImpl where - C: NodeClient + 'a, + C: NodeClient, { - lc_provider: DefaultLCProvider<'a, C, ExtKeychain>, + lc_provider: DefaultLCProvider, } -impl<'a, C> DefaultWalletImpl<'a, C> +impl DefaultWalletImpl where - C: NodeClient + 'a, + C: NodeClient, { pub fn new(node_client: C) -> Result { let lc_provider = DefaultLCProvider::new(node_client); - Ok(DefaultWalletImpl { - lc_provider: lc_provider, - }) + Ok(DefaultWalletImpl { lc_provider }) } } -impl<'a, L, C, K> WalletInst<'a, L, C, K> for DefaultWalletImpl<'a, C> +impl<'a, L, C, K> WalletInst<'a, L, C, K> for DefaultWalletImpl where - DefaultLCProvider<'a, C, ExtKeychain>: WalletLCProvider<'a, C, K>, + DefaultLCProvider: 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> { + fn lc_provider(&mut self) -> Result<&mut dyn WalletLCProvider<'a, C, K>, libwallet::Error> { Ok(&mut self.lc_provider) } } diff --git a/impls/src/lifecycle/default.rs b/impls/src/lifecycle/default.rs index 0b24b6830..7514a3055 100644 --- a/impls/src/lifecycle/default.rs +++ b/impls/src/lifecycle/default.rs @@ -23,13 +23,13 @@ use crate::libwallet::{Error, NodeClient, WalletBackend, WalletInitStatus, Walle use crate::lifecycle::seed::WalletSeed; use crate::util::secp::key::SecretKey; use crate::util::ZeroingString; -use crate::LMDBBackend; use grin_util::logger::LoggingConfig; +use grin_wallet_config::config::reload_global_config; use std::fs; use std::path::PathBuf; use std::path::MAIN_SEPARATOR; -// Helper fuction to format paths according to OS, avoids bugs on Linux +/// Helper function 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); @@ -37,20 +37,20 @@ pub fn fmt_path(path: String) -> String { path } -pub struct DefaultLCProvider<'a, C, K> +pub struct DefaultLCProvider where - C: NodeClient + 'a, - K: Keychain + 'a, + C: NodeClient, + K: Keychain, { data_dir: String, node_client: C, - backend: Option + 'a>>, + backend: Option>, } -impl<'a, C, K> DefaultLCProvider<'a, C, K> +impl DefaultLCProvider where - C: NodeClient + 'a, - K: Keychain + 'a, + C: NodeClient, + K: Keychain, { /// Create new provider pub fn new(node_client: C) -> Self { @@ -62,7 +62,7 @@ where } } -impl<'a, C, K> WalletLCProvider<'a, C, K> for DefaultLCProvider<'a, C, K> +impl<'a, C, K> WalletLCProvider<'a, C, K> for DefaultLCProvider where C: NodeClient + 'a, K: Keychain + 'a, @@ -90,48 +90,11 @@ where logging_config: Option, tor_config: Option, ) -> Result<(), Error> { - let mut default_config = GlobalWalletConfig::for_chain(&chain_type); - let config_file_version = match default_config.members.as_ref() { - Some(m) => m.clone().config_file_version, - None => None, - }; - let logging = match logging_config { - Some(l) => Some(l), - None => match default_config.members.as_ref() { - Some(m) => m.clone().logging, - None => None, - }, - }; - // Check if config was provided, if not load default and set update to "true" - let (wallet, update) = match wallet_config { - Some(w) => (w, false), - None => match default_config.members.as_ref() { - Some(m) => (m.clone().wallet, true), - None => (WalletConfig::default(), true), - }, - }; - let tor = match tor_config { - Some(t) => Some(t), - None => match default_config.members.as_ref() { - Some(m) => m.clone().tor, - None => Some(TorConfig::default()), - }, - }; - default_config = GlobalWalletConfig { - members: Some(GlobalWalletConfigMembers { - config_file_version, - wallet, - tor, - logging, - }), - ..default_config - }; let mut config_file_name = PathBuf::from(self.data_dir.clone()); config_file_name.push(file_name); let mut data_dir_name = PathBuf::from(self.data_dir.clone()); data_dir_name.push(GRIN_WALLET_DIR); - if config_file_name.exists() && data_dir_name.exists() { let msg = format!( "{} already exists in the target directory ({}). Please remove it first", @@ -141,10 +104,35 @@ where return Err(Error::Lifecycle(msg)); } - // If config exists but the datadir return ok if config_file_name.exists() { return Ok(()); } + + let mut default_config = GlobalWalletConfig::for_chain(&chain_type, &config_file_name); + let config_file_version = default_config.members.config_file_version; + let logging = match logging_config.clone() { + Some(l) => Some(l), + None => default_config.members.logging, + }; + // Check if config was provided, if not load default and set update to "true" + let (wallet, update) = match wallet_config.clone() { + Some(w) => (w, false), + None => (default_config.members.wallet, true), + }; + let tor = match tor_config.clone() { + Some(t) => Some(t), + None => default_config.members.tor, + }; + default_config = GlobalWalletConfig { + members: GlobalWalletConfigMembers { + config_file_version, + wallet, + tor, + logging, + }, + ..default_config + }; + // 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()); @@ -172,6 +160,8 @@ where return Err(Error::Lifecycle(msg)); } + reload_global_config(&config_file_name)?; + info!( "File {} configured and created", config_file_name.to_str().unwrap(), @@ -217,8 +207,8 @@ where 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()) { + let mut wallet: WalletBackend = + match WalletBackend::new(&data_dir_name, self.node_client.clone()) { Err(e) => { let msg = format!("Error creating wallet: {}, Data Dir: {}", e, &data_dir_name); error!("{}", msg); @@ -247,8 +237,8 @@ where 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()) { + let mut wallet: WalletBackend = + match WalletBackend::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)); @@ -262,8 +252,8 @@ where .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)); + let mask = wallet.set_keychain(keychain, create_mask, use_test_rng)?; + self.backend = Some(wallet); Ok(mask) } @@ -329,8 +319,7 @@ where 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 - + // 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 @@ -366,7 +355,7 @@ where .to_string(); return Err(Error::Lifecycle(msg)); } - // Removin + // Removing old file info!("Password change confirmed, removing old seed file."); fs::remove_file(backup_name).map_err(|e| Error::IO(e.to_string()))?; @@ -383,13 +372,13 @@ where Ok(()) } - fn wallet_inst(&mut self) -> Result<&mut Box + 'a>, Error> { + fn wallet_inst(&mut self) -> Result<&mut WalletBackend, 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()), + Some(b) => Ok(b), } } } diff --git a/impls/src/lifecycle/seed.rs b/impls/src/lifecycle/seed.rs index 59dcd343d..5dbbd6c41 100644 --- a/impls/src/lifecycle/seed.rs +++ b/impls/src/lifecycle/seed.rs @@ -25,6 +25,7 @@ use ring::aead; use ring::pbkdf2; use crate::keychain::{mnemonic, Keychain}; +use crate::lifecycle::default::fmt_path; use crate::util::{self, ToHex}; use crate::Error; @@ -56,13 +57,6 @@ impl WalletSeed { self.0.to_vec().to_hex() } - // Helper fuction to format paths according to OS, avoids bugs on Linux - pub fn fmt_path(path: String) -> String { - let sep = &MAIN_SEPARATOR.to_string(); - let path = path.replace("/", &sep); - let path = path.replace("\\", &sep); - path - } pub fn to_mnemonic(&self) -> Result { let result = mnemonic::from_entropy(&self.0); match result { @@ -105,7 +99,7 @@ impl WalletSeed { pub fn seed_file_exists(data_file_dir: &str) -> Result { let seed_file_path = &format!( "{}{}{}", - Self::fmt_path(data_file_dir.to_string()), + fmt_path(data_file_dir.to_string()), MAIN_SEPARATOR, SEED_FILE, ); @@ -145,7 +139,7 @@ impl WalletSeed { ) -> Result<(), Error> { let seed_file_path = &format!( "{}{}{}", - Self::fmt_path(data_file_dir.to_string()), + fmt_path(data_file_dir.to_string()), MAIN_SEPARATOR, SEED_FILE, ); @@ -184,11 +178,11 @@ impl WalletSeed { let seed_file_path = &format!( "{}{}{}", - Self::fmt_path(data_file_dir.to_string()), + fmt_path(data_file_dir.to_string()), MAIN_SEPARATOR, SEED_FILE, ); - let data_file_dir = Self::fmt_path(data_file_dir.to_string()); + let data_file_dir = 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 { @@ -219,7 +213,7 @@ impl WalletSeed { let seed_file_path = &format!( "{}{}{}", - Self::fmt_path(data_file_dir.to_string()), + fmt_path(data_file_dir.to_string()), MAIN_SEPARATOR, SEED_FILE, ); @@ -247,7 +241,7 @@ impl WalletSeed { pub fn delete_seed_file(data_file_dir: &str) -> Result<(), Error> { let seed_file_path = &format!( "{}{}{}", - Self::fmt_path(data_file_dir.to_string()), + fmt_path(data_file_dir.to_string()), MAIN_SEPARATOR, SEED_FILE, ); diff --git a/impls/src/node_clients/http.rs b/impls/src/node_clients/http.rs index 6b80b49e6..b7e29c6ad 100644 --- a/impls/src/node_clients/http.rs +++ b/impls/src/node_clients/http.rs @@ -14,7 +14,9 @@ //! Client functions, implementations of the NodeClient trait +use super::resp_types::*; use crate::api::{self, LocatedTxKernel, OutputListing, OutputPrintable}; +use crate::client_utils::json_rpc::*; use crate::client_utils::{Client, RUNTIME}; use crate::core::core::{Transaction, TxKernel}; use crate::libwallet; @@ -26,9 +28,7 @@ use futures::TryStreamExt; use std::collections::HashMap; use std::env; use std::net::SocketAddr; - -use super::resp_types::*; -use crate::client_utils::json_rpc::*; +use std::time::Duration; const ENDPOINT: &str = "/v2/foreign"; @@ -45,8 +45,9 @@ impl HTTPNodeClient { pub fn new( node_url: &str, node_api_secret: Option, + request_timeout: Duration, ) -> Result { - Self::new_proxy(node_url, node_api_secret, None) + Self::new_proxy(node_url, node_api_secret, None, request_timeout) } /// Create a new client with proxy @@ -54,16 +55,17 @@ impl HTTPNodeClient { node_url: &str, node_api_secret: Option, proxy: Option<(SocketAddr, &'static str)>, + request_timeout: Duration, ) -> Result { let client = if let Some((a, s)) = proxy { - Client::with_proxy(a, s) + Client::with_proxy(a, s, request_timeout) } else { - Client::new() + Client::new(request_timeout) }; Ok(HTTPNodeClient { client: client.map_err(|_| libwallet::Error::Node)?, node_url: node_url.to_owned(), - node_api_secret: node_api_secret, + node_api_secret, node_version_info: None, }) } @@ -107,18 +109,25 @@ impl NodeClient for HTTPNodeClient { fn node_url(&self) -> &str { &self.node_url } - fn node_api_secret(&self) -> Option { - self.node_api_secret.clone() - } - fn set_node_url(&mut self, node_url: &str) { self.node_url = node_url.to_owned(); } + fn node_api_secret(&self) -> Option { + self.node_api_secret.clone() + } + fn set_node_api_secret(&mut self, node_api_secret: Option) { self.node_api_secret = node_api_secret; } + /// Posts a transaction to a grin node + fn post_tx(&self, tx: &Transaction, fluff: bool) -> Result<(), libwallet::Error> { + let params = json!([tx, fluff]); + self.send_json_request::("push_transaction", ¶ms)?; + Ok(()) + } + fn get_version_info(&mut self) -> Option { if let Some(v) = self.node_version_info.as_ref() { return Some(v.clone()); @@ -135,30 +144,23 @@ impl NodeClient for HTTPNodeClient { // 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 { + return if err_string.contains("404") { + 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; - } + None + }; } }; self.node_version_info = Some(retval.clone()); Some(retval) } - /// Posts a transaction to a grin node - fn post_tx(&self, tx: &Transaction, fluff: bool) -> Result<(), libwallet::Error> { - let params = json!([tx, fluff]); - self.send_json_request::("push_transaction", ¶ms)?; - Ok(()) - } - /// Return the chain tip from a given node fn get_chain_tip(&self) -> Result<(u64, String), libwallet::Error> { let result = self.send_json_request::("get_tip", &serde_json::Value::Null)?; @@ -222,7 +224,7 @@ impl NodeClient for HTTPNodeClient { .collect(); // going to leave this here even though we're moving - // to the json RPC api to keep the functionality of + // 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; @@ -272,7 +274,7 @@ impl NodeClient for HTTPNodeClient { let rt = RUNTIME.clone(); let res: Result, _> = - std::thread::spawn(move || rt.lock().unwrap().block_on(async move { task.await })) + std::thread::spawn(move || rt.block_on(async move { task.await })) .join() .unwrap(); @@ -420,8 +422,8 @@ mod tests { } } - // Wallet will "push" a transaction to node, serializing the transaction as json. - // We are testing the json structure is what we expect here. + // 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(); diff --git a/impls/src/test_framework/mod.rs b/impls/src/test_framework/mod.rs index 247979356..c90eac56d 100644 --- a/impls/src/test_framework/mod.rs +++ b/impls/src/test_framework/mod.rs @@ -18,7 +18,6 @@ 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::{ @@ -28,6 +27,7 @@ use crate::util::secp::key::SecretKey; use crate::util::secp::pedersen; use crate::util::Mutex; use chrono::Duration; +use grin_keychain::Keychain; use std::sync::Arc; use std::thread; @@ -36,7 +36,7 @@ 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 { +fn get_output_local(chain: &Chain, commit: pedersen::Commitment) -> Option { if chain.get_unspent(commit).unwrap().is_some() { let block_height = chain.get_header_for_output(commit).unwrap().height; let output_pos = chain.get_output_pos(&commit).unwrap_or(0); @@ -48,7 +48,7 @@ fn get_output_local(chain: &chain::Chain, commit: pedersen::Commitment) -> Optio /// Get a kernel from the chain locally fn get_kernel_local( - chain: Arc, + chain: Arc, excess: &pedersen::Commitment, min_height: Option, max_height: Option, @@ -65,7 +65,7 @@ fn get_kernel_local( /// get output listing traversing pmmr from local fn get_outputs_by_pmmr_index_local( - chain: Arc, + chain: Arc, start_index: u64, end_index: Option, max: u64, @@ -86,7 +86,7 @@ fn get_outputs_by_pmmr_index_local( /// get output listing in a given block range fn height_range_to_pmmr_indices_local( - chain: Arc, + chain: Arc, start_index: u64, end_index: Option, ) -> api::OutputListing { @@ -147,13 +147,13 @@ pub fn create_block_for_wallet<'a, L, C, K>( chain: &Chain, prev: core::core::BlockHeader, txs: &[Transaction], - wallet: Arc + 'a>>>, + wallet: Arc>>>, keychain_mask: Option<&SecretKey>, ) -> Result where L: WalletLCProvider<'a, C, K>, C: NodeClient + 'a, - K: keychain::Keychain + 'a, + K: Keychain + 'a, { // build block fees let fee_amt = txs.iter().map(|tx| tx.fee()).sum(); @@ -166,7 +166,7 @@ where 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)? + foreign::build_coinbase(w, keychain_mask, &block_fees, false)? }; let block = create_block_with_reward(chain, prev, txs, coinbase_tx.output, coinbase_tx.kernel); Ok(block) @@ -178,13 +178,13 @@ where pub fn award_block_to_wallet<'a, L, C, K>( chain: &Chain, txs: &[Transaction], - wallet: Arc + 'a>>>, + wallet: Arc>>>, keychain_mask: Option<&SecretKey>, ) -> Result<(), libwallet::Error> where L: WalletLCProvider<'a, C, K>, C: NodeClient + 'a, - K: keychain::Keychain + 'a, + K: Keychain + 'a, { let prev = chain.head_header().unwrap(); let block = create_block_for_wallet(chain, prev, txs, wallet, keychain_mask)?; @@ -200,7 +200,7 @@ pub fn process_block(chain: &Chain, block: core::core::Block) { /// Award a blocks to a wallet directly pub fn award_blocks_to_wallet<'a, L, C, K>( chain: &Chain, - wallet: Arc + 'a>>>, + wallet: Arc>>>, keychain_mask: Option<&SecretKey>, number: usize, pause_between: bool, @@ -208,7 +208,7 @@ pub fn award_blocks_to_wallet<'a, L, C, K>( where L: WalletLCProvider<'a, C, K>, C: NodeClient + 'a, - K: keychain::Keychain + 'a, + K: Keychain + 'a, { for _ in 0..number { award_block_to_wallet(chain, &[], wallet.clone(), keychain_mask)?; @@ -231,7 +231,7 @@ pub fn send_to_dest<'a, L, C, K>( where L: WalletLCProvider<'a, C, K>, C: NodeClient + 'a, - K: keychain::Keychain + 'a, + K: Keychain + 'a, { let slate = { let mut w_lock = wallet.lock(); @@ -245,10 +245,10 @@ where selection_strategy_is_use_all: true, ..Default::default() }; - let slate_i = owner::init_send_tx(&mut **w, keychain_mask, args, test_mode)?; + let slate_i = owner::init_send_tx(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)? + owner::tx_lock_outputs(w, keychain_mask, &slate)?; + owner::finalize_tx(w, keychain_mask, &slate)? }; let client = { let mut w_lock = wallet.lock(); @@ -267,7 +267,7 @@ pub fn wallet_info<'a, L, C, K>( where L: WalletLCProvider<'a, C, K>, C: NodeClient + 'a, - K: keychain::Keychain + 'a, + K: Keychain + 'a, { let (wallet_refreshed, wallet_info) = owner::retrieve_summary_info(wallet, keychain_mask, &None, true, 1)?; diff --git a/impls/src/test_framework/testclient.rs b/impls/src/test_framework/testclient.rs index 05eb08323..dcd2ac1d5 100644 --- a/impls/src/test_framework/testclient.rs +++ b/impls/src/test_framework/testclient.rs @@ -70,7 +70,7 @@ where String, ( Sender, - Arc + 'a>>>, + Arc>>>, Option, ), >, @@ -100,14 +100,15 @@ where genesis_block, pow::verify_size, false, + None, ) .unwrap(); let (tx, rx) = channel(); WalletProxy { chain_dir: chain_dir.to_owned(), chain: Arc::new(c), - tx: tx, - rx: rx, + tx, + rx, wallets: HashMap::new(), running: Arc::new(AtomicBool::new(false)), } @@ -118,7 +119,7 @@ where &mut self, addr: &str, tx: Sender, - wallet: Arc + 'a>>>, + wallet: Arc>>>, keychain_mask: Option, ) { self.wallets @@ -218,8 +219,7 @@ where 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) - { + match foreign::receive_tx(w, (&mask).as_ref(), &Slate::from(slate), None, false) { Err(e) => { return Ok(WalletProxyMessage { sender_id: m.dest, diff --git a/impls/src/tor/arti.rs b/impls/src/tor/arti.rs new file mode 100644 index 000000000..769e2bc09 --- /dev/null +++ b/impls/src/tor/arti.rs @@ -0,0 +1,478 @@ +// Copyright 2026 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::tor::config::exp_sec_key_bytes; +use arti_client::config::pt::TransportConfigBuilder; +use arti_client::config::{BridgeConfigBuilder, TorClientConfigBuilder}; +use arti_client::{TorClient, TorClientConfig}; +use bytes::Bytes; +use ed25519_dalek::hazmat::ExpandedSecretKey; +use ed25519_dalek::Digest; +use fs_mistrust::Mistrust; +use fslock_guard::LockFileGuard; +use grin_util::secp::SecretKey; +use grin_wallet_config::TorConfig; +use grin_wallet_libwallet::Error; +use grin_wallet_util::OnionV3Address; +use http_body_util::{BodyExt, Full}; +use hyper::{Request, Uri}; +use hyper_util::rt::TokioIo; +use lazy_static::lazy_static; +use serde::Serialize; +use sha2::Sha512; +use std::collections::HashMap; +use std::net::SocketAddr; +use std::path::{Path, PathBuf}; +use std::sync::{Arc, LazyLock, Mutex}; +use std::thread::{self, JoinHandle}; +use std::time::Duration; +use tor_hscrypto::pk::{HsIdKey, HsIdKeypair}; +use tor_hsrproxy::config::{ + Encapsulation, ProxyAction, ProxyConfigBuilder, ProxyPattern, ProxyRule, TargetAddr, +}; +use tor_hsrproxy::OnionServiceReverseProxy; +use tor_hsservice::config::OnionServiceConfigBuilder; +use tor_hsservice::{ + HsIdKeypairSpecifier, HsIdPublicKeySpecifier, HsNickname, RunningOnionService, +}; +use tor_keymgr::config::CfgPath; +use tor_keymgr::{ArtiNativeKeystore, KeyMgrBuilder, KeystoreSelector}; +use tor_llcrypto::pk::ed25519::ExpandedKeypair; +use tor_rtcompat::tokio::TokioNativeTlsRuntime; +use tor_rtcompat::{SleepProviderExt, ToplevelBlockOn}; + +struct TorService { + service: Arc, + proxy: Arc, + worker: JoinHandle<()>, + state_lock: PathBuf, +} + +lazy_static! { + /// Arti Tokio runtime. + static ref ARTI_RUNTIME: LazyLock>> = + LazyLock::new(|| Mutex::new(ArtiRuntimeWrapper::create().ok())); + /// Arti client and config. + static ref ARTI_CLIENT_CONFIG: LazyLock>, TorClientConfig)>>> = + LazyLock::new(|| Mutex::new(None)); + /// Running services, where key is onion address. + static ref ARTI_PROXY_SERVICES: LazyLock>> = + LazyLock::new(|| Mutex::new(HashMap::new())); +} + +/// Arti client runtime wrapper. +#[derive(Clone)] +struct ArtiRuntimeWrapper { + runtime: TokioNativeTlsRuntime, +} + +impl ArtiRuntimeWrapper { + fn create() -> Result { + Ok(Self { + runtime: TokioNativeTlsRuntime::create()?, + }) + } +} + +/// Get Tor client runtime. +fn runtime() -> Result { + let mut runtime = ARTI_RUNTIME.lock().unwrap(); + let r = match runtime.as_ref() { + None => runtime.insert(ArtiRuntimeWrapper::create()?), + Some(r) => r, + }; + Ok(r.runtime.clone()) +} + +/// Stop running Tor service by onion address as key. +pub fn stop_tor_service(onion_addr: String) -> Result<(), Error> { + let service = ARTI_PROXY_SERVICES.lock().unwrap().remove(&onion_addr); + let Some(service) = service else { + error!("Service {} to stop was not found", onion_addr); + return Ok(()); + }; + let TorService { + service, + proxy, + worker, + state_lock, + } = service; + + proxy.shutdown(); + worker + .join() + .map_err(|_| Error::TorProcess("Tor proxy thread panicked".into()))?; + drop(service); + drop( + LockFileGuard::lock(state_lock) + .map_err(|e| Error::TorProcess(format!("Can not stop Tor service: {}", e)))?, + ); + Ok(()) +} + +/// Get state and cache data paths. +fn state_cache_paths(config: &TorConfig) -> (PathBuf, PathBuf) { + let mut tor_dir = PathBuf::from(&config.send_config_dir); + tor_dir.push("arti"); + let state_path = tor_dir.join("state"); + let cache_path = tor_dir.join("cache"); + (state_path, cache_path) +} + +/// Start Tor service from provided key. +pub fn start_tor_service(key: SecretKey, addr: &str, config: &TorConfig) -> Result<(), Error> { + info!("Starting integrated Tor listener."); + let use_proxy = config.proxy.transport.is_some() && config.proxy.address.is_some(); + if use_proxy { + info!("Proxy configuration will be ignored."); + } + + let (state_path, cache_path) = state_cache_paths(&config); + let (client, config) = init_client(&state_path, &cache_path, config)?; + + // Add service key to keystore. + let onion_address = + OnionV3Address::from_private(&key.0).map_err(|e| Error::TorConfig(format!("{:?}", e)))?; + let hs = HsNickname::new(onion_address.to_string()) + .map_err(|e| Error::TorConfig(format!("{:?}", e)))?; + let keystore_path = Path::new(&state_path).join("keystore"); + // Arti 0.44 uses this lock to finish stopping a service before it can start again. + let state_lock = state_path + .join("hss") + .join(format!("{}.lock", onion_address)); + let _ = add_service_key(config.fs_mistrust(), &key, &hs, keystore_path)?; + + // Launch Onion service. + let service_config = OnionServiceConfigBuilder::default() + .nickname(hs.clone()) + .build() + .map_err(|e| Error::TorConfig(format!("{:?}", e)))?; + let (service, proxy, worker) = match client.launch_onion_service(service_config) { + Ok(res) => { + if let Some((service, mut request)) = res { + let addr: SocketAddr = addr + .parse() + .map_err(|e| Error::TorProcess(format!("{:?}", e)))?; + let proxy = create_service_proxy(addr)?; + let c = client.clone(); + let p = proxy.clone(); + // Launch service proxy. + let worker = thread::spawn(move || { + c.clone().runtime().block_on(async move { + loop { + match run_service_proxy(p.clone(), &mut request, hs.clone()).await { + Ok(_) => { + info!("Tor proxy stopped"); + break; + } + Err(e) => { + error!("Tor proxy error: {:?}, restarting", e); + tokio::time::sleep(Duration::from_millis(1000)).await; + continue; + } + } + } + }) + }); + (service, proxy, worker) + } else { + return Err(Error::TorProcess("Can not launch onion service".to_owned())); + } + } + Err(e) => return Err(Error::TorProcess(format!("{:?}", e))), + }; + + let mut running_services = ARTI_PROXY_SERVICES.lock().unwrap(); + running_services.insert( + onion_address.to_string(), + TorService { + service, + proxy, + worker, + state_lock, + }, + ); + + Ok(()) +} + +/// Start Tor client to send requests. +pub fn start_tor_client(config: TorConfig) -> Result<(), Error> { + info!("Starting integrated Tor client"); + + let (state_path, cache_path) = state_cache_paths(&config); + let (_, _) = init_client(&state_path, &cache_path, &config)?; + Ok(()) +} + +/// Make POST request. +pub fn tor_post(tor_config: &TorConfig, input: &IN, url: &str) -> Result +where + IN: Serialize, +{ + let json = serde_json::to_string(input) + .map_err(|_| Error::GenericError("Could not serialize data to JSON".to_owned()))?; + let url = url.to_string(); + let url: Uri = url + .parse() + .map_err(|_| Error::GenericError(format!("Bad URL: {}", url)))?; + let host = match url.host() { + None => return Err(Error::GenericError(format!("URL {} has bad host", url))), + Some(h) => h, + } + .to_string(); + let timeout = tor_config.request_timeout(); + let (state_path, cache_path) = state_cache_paths(&tor_config); + let (client, _) = init_client(&state_path, &cache_path, tor_config)?; + let res: Result = thread::spawn(move || { + let c = client.clone(); + client.runtime().block_on(async move { + let res = c + .runtime() + .timeout(timeout, async { + let stream = c + .connect((host, url.port_u16().unwrap_or(80))) + .await + .map_err(|e| Error::TorProcess(format!("{:?}", e)))?; + let (mut request_sender, connection) = + hyper::client::conn::http1::handshake(TokioIo::new(stream)) + .await + .map_err(|e| Error::TorProcess(format!("{:?}", e)))?; + + // Spawn a task to poll the connection and drive the HTTP state. + tokio::spawn(async move { + if let Err(e) = connection.await { + error!("Tor connection error: {}", e); + } + }); + + let resp = request_sender + .send_request( + Request::builder() + .uri(url) + .method("POST") + .body::>(Full::from(json)) + .map_err(|e| Error::TorProcess(format!("{:?}", e)))?, + ) + .await + .map_err(|e| Error::TorProcess(format!("{:?}", e)))?; + + let body_resp = resp + .into_body() + .collect() + .await + .map_err(|e| Error::TorProcess(format!("{:?}", e)))?; + let body = body_resp.to_bytes().into(); + let body_text = String::from_utf8(body) + .map_err(|e| Error::TorProcess(format!("{:?}", e)))?; + Ok(body_text) + }) + .await; + match res { + Err(e) => Err(Error::TorProcess(format!("{:?}", e))), + Ok(body) => Ok(body), + } + }) + }) + .join() + .unwrap_or_else(|e| return Err(Error::TorProcess(format!("{:?}", e))))?; + res +} + +/// Create Tor client. +fn init_client( + state_path: &PathBuf, + cache_path: &PathBuf, + tor_config: &TorConfig, +) -> Result<(Arc>, TorClientConfig), Error> { + let mut builder = TorClientConfigBuilder::from_directories(&state_path, cache_path); + builder.address_filter().allow_onion_addrs(true); + + // Configure bridge. + if let Some(bridge_line) = tor_config.bridge.bridge_line.as_ref() { + let bridge: BridgeConfigBuilder = bridge_line + .parse() + .map_err(|e| Error::TorConfig(format!("{:?}", e)))?; + builder.bridges().bridges().push(bridge.clone()); + match bridge.get_transport() { + None => { + return Err(Error::TorConfig(format!( + "No transport found at {}", + bridge_line + ))) + } + Some(t) => { + // Now configure bridge transport. (Requires the "pt-client" feature) + let bin_path = tor_config + .bridge + .bridge_bin_path + .clone() + .unwrap_or(t.to_owned()); + let mut transport = TransportConfigBuilder::default(); + transport + .protocols(vec![t + .parse() + .map_err(|e| Error::TorConfig(format!("{:?}", e)))?]) + .path(CfgPath::new(bin_path)) + .run_on_startup(true); + builder.bridges().transports().push(transport); + } + } + } + let config = builder + .build() + .map_err(|e| Error::TorConfig(format!("{:?}", e)))?; + + // Return existing client if config was not changed. + let mut cached_client_config = ARTI_CLIENT_CONFIG.lock().unwrap(); + if let Some((client, c)) = cached_client_config.as_ref() { + if c == &config { + debug!("Reusing Arti Tor client from global state."); + return Ok((client.clone(), c.clone())); + } else { + debug!("Tor config changed, rebuild client."); + *cached_client_config = None; + } + } + let res = launch_client(config.clone(), &tor_config); + match res { + Ok(client) => { + cached_client_config.replace((client.clone(), config.clone())); + Ok((client, config)) + } + Err(e) => Err(e), + } +} + +/// Launch tor client from provided configuration. +fn launch_client( + client_config: TorClientConfig, + tor_config: &TorConfig, +) -> Result>, Error> { + let r = runtime()?; + let client = TorClient::with_runtime(r) + .config(client_config) + .create_unbootstrapped() + .map_err(|e| Error::TorProcess(format!("{:?}", e)))?; + let c = client.clone(); + let timeout = tor_config.bootstrap_timeout(); + let res = client.runtime().block_on(async move { + let bootstrap = async || { + return match c.bootstrap().await { + Ok(_) => { + let mut percent = 0.0; + let mut prev_percent = 0.0; + while percent < 1.0 { + percent = c.bootstrap_status().as_frac(); + if percent != prev_percent { + info!("Starting Tor {}%", percent * 100.0); + } + prev_percent = percent; + tokio::time::sleep(Duration::from_millis(1000)).await; + } + info!("Tor client bootstrapped successfully"); + Ok(()) + } + Err(e) => Err(e), + }; + }; + match c.runtime().timeout(timeout, bootstrap()).await { + Ok(r) => match r { + Err(e) => Err(Error::TorProcess(format!("{:?}", e))), + Ok(_) => Ok(c), + }, + Err(e) => Err(Error::TorProcess(format!("{:?}", e))), + } + }); + res +} + +/// Create Onion service proxy. +fn create_service_proxy(addr: SocketAddr) -> Result, Error> { + // Setup proxy to forward request from Tor address to local address. + let proxy_rule = ProxyRule::new( + ProxyPattern::one_port(80).map_err(|e| Error::TorConfig(format!("{}", e)))?, + ProxyAction::Forward(Encapsulation::Simple, TargetAddr::Inet(addr)), + ); + let mut proxy_cfg_builder = ProxyConfigBuilder::default(); + proxy_cfg_builder.set_proxy_ports(vec![proxy_rule]); + let proxy_cfg = proxy_cfg_builder + .build() + .map_err(|e| Error::TorConfig(format!("{}", e)))?; + let proxy = OnionServiceReverseProxy::new(proxy_cfg); + Ok(proxy) +} + +/// Launch service proxy. +async fn run_service_proxy( + proxy: Arc, + request: &mut S, + nickname: HsNickname, +) -> Result<(), Error> +where + S: futures::Stream + Unpin + Send + 'static, +{ + let runtime = runtime()?; + proxy + .handle_requests(runtime, nickname, request) + .await + .map_err(|e| Error::TorProcess(format!("{:?}", e)))?; + Ok(()) +} + +/// Save Onion service key to keystore. +fn add_service_key( + mistrust: &Mistrust, + key: &SecretKey, + hs_nickname: &HsNickname, + path: PathBuf, +) -> Result<(), Error> { + let arti_store = ArtiNativeKeystore::from_path_and_mistrust(path, mistrust) + .map_err(|e| Error::TorProcess(format!("{}", e)))?; + + let key_manager = KeyMgrBuilder::default() + .primary_store(Box::new(arti_store)) + .build() + .map_err(|e| Error::TorProcess(format!("{}", e)))?; + + let expanded_sk = + ExpandedSecretKey::from_bytes(Sha512::default().chain_update(key).finalize().as_ref()); + let sk_bytes = exp_sec_key_bytes(expanded_sk); + match ExpandedKeypair::from_secret_key_bytes(sk_bytes) { + None => { + return Err(Error::TorProcess( + "Hidden service key can not be created".into(), + )) + } + Some(expanded_kp) => { + key_manager + .insert( + HsIdKey::from(expanded_kp.public().clone()), + &HsIdPublicKeySpecifier::new(hs_nickname.clone()), + KeystoreSelector::Primary, + true, + ) + .map_err(|e| Error::TorProcess(format!("{}", e)))?; + key_manager + .insert( + HsIdKeypair::from(expanded_kp), + &HsIdKeypairSpecifier::new(hs_nickname.clone()), + KeystoreSelector::Primary, + true, + ) + .map_err(|e| Error::TorProcess(format!("{}", e)))?; + } + } + Ok(()) +} diff --git a/impls/src/tor/config.rs b/impls/src/tor/config.rs index d5e0374c7..422fae194 100644 --- a/impls/src/tor/config.rs +++ b/impls/src/tor/config.rs @@ -17,9 +17,9 @@ 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 ed25519_dalek::hazmat::ExpandedSecretKey; +use ed25519_dalek::SigningKey as DalekSecretKey; +use ed25519_dalek::VerifyingKey as DalekPublicKey; use std::collections::HashMap; use std::convert::TryFrom; use std::fs::{self, File}; @@ -92,6 +92,14 @@ impl TorRcConfig { } } +/// Convert expanded secret key to byte array (works for both external Tor process and Arti). +pub(crate) fn exp_sec_key_bytes(expanded_sk: ExpandedSecretKey) -> [u8; 64] { + let mut sk_bytes = [0_u8; 64]; + sk_bytes[0..32].copy_from_slice(&expanded_sk.scalar.to_bytes()); + sk_bytes[32..64].copy_from_slice(&expanded_sk.hash_prefix); + sk_bytes +} + pub fn create_onion_service_sec_key_file( os_directory: &str, sec_key: &DalekSecretKey, @@ -101,9 +109,9 @@ pub fn create_onion_service_sec_key_file( // 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)?; + let expanded_skey: ExpandedSecretKey = ExpandedSecretKey::from(sec_key.as_bytes()); + let sk_bytes = exp_sec_key_bytes(expanded_skey); + file.write_all(&sk_bytes).map_err(|_| Error::IO)?; Ok(()) } @@ -138,8 +146,7 @@ pub fn output_onion_service_config( tor_config_directory: &str, sec_key: &SecretKey, ) -> Result { - let d_sec_key = DalekSecretKey::from_bytes(&sec_key.0) - .map_err(|_| Error::ED25519Key("Unable to parse private key".into()))?; + let d_sec_key = DalekSecretKey::from_bytes(&sec_key.0); let address = OnionV3Address::from_private(&sec_key.0)?; let hs_dir_file_path = format!( "{}{}{}{}{}", @@ -291,7 +298,7 @@ mod tests { 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); + let _ = fs::remove_dir_all(test_dir); } pub fn setup(test_dir: &str) { diff --git a/impls/src/tor/mod.rs b/impls/src/tor/mod.rs index 37821c9ab..9eb97eca9 100644 --- a/impls/src/tor/mod.rs +++ b/impls/src/tor/mod.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +pub mod arti; pub mod bridge; pub mod config; pub mod process; diff --git a/impls/src/tor/process.rs b/impls/src/tor/process.rs index f964326c7..4e53f8e5d 100644 --- a/impls/src/tor/process.rs +++ b/impls/src/tor/process.rs @@ -51,13 +51,13 @@ 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 std::{cmp, io}; use sysinfo::{Process, ProcessExt, System, SystemExt}; #[cfg(windows)] @@ -83,7 +83,7 @@ pub struct TorProcess { args: Vec, torrc_path: Option, completion_percent: u8, - timeout: u32, + timeout: u64, working_dir: Option, pub stdout: Option>, pub process: Option, @@ -96,8 +96,8 @@ impl TorProcess { tor_cmd: TOR_EXE_NAME.to_string(), args: vec![], torrc_path: None, - completion_percent: 100 as u8, - timeout: 0 as u32, + completion_percent: 100, + timeout: 0, working_dir: None, stdout: None, process: None, @@ -137,7 +137,7 @@ impl TorProcess { self } - pub fn timeout(&mut self, timeout: u32) -> &mut Self { + pub fn timeout(&mut self, timeout: u64) -> &mut Self { self.timeout = timeout; self } @@ -198,10 +198,11 @@ impl TorProcess { 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(()); - }); + // Keep this below chrono's DateTime range used by timer. + let timeout = chrono::Duration::seconds(cmp::min(self.timeout, u32::MAX as u64) as i64); + let _guard = timer.schedule_with_delay(timeout, 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)) @@ -225,8 +226,8 @@ impl TorProcess { mut stdout: BufReader, completion_perc: u8, ) -> Result, Error> { - let re_bootstrap = Regex::new(r"^\[notice\] Bootstrapped (?P[0-9]+)%(.*): ") - .map_err(Error::Regex)?; + let re_bootstrap = + Regex::new(r"^\[notice] Bootstrapped (?P[0-9]+)%(.*): ").map_err(Error::Regex)?; let timestamp_len = "May 16 02:50:08.792".len(); let mut warnings = Vec::new(); diff --git a/integration/tests/framework.rs b/integration/tests/framework.rs index ffade00c0..3e0b883c5 100644 --- a/integration/tests/framework.rs +++ b/integration/tests/framework.rs @@ -37,7 +37,7 @@ 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) { + if let Err(e) = fs::remove_dir_all(target_dir) { println!("can't remove output from previous test :{}, may be ok", e); } } @@ -281,7 +281,11 @@ impl LocalServerContainer { 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); + let client_n = HTTPNodeClient::new( + &self.wallet_config.check_node_api_http_addr, + None, + std::time::Duration::from_secs(60), + ); if let Err(_e) = r { //panic!("Error initializing wallet seed: {}", e); @@ -354,7 +358,11 @@ impl LocalServerContainer { 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_n = HTTPNodeClient::new( + &config.check_node_api_http_addr, + None, + std::time::Duration::from_secs(60), + ); let mut wallet = LMDBBackend::new(config.clone(), "", client_n) .unwrap_or_else(|e| panic!("Error creating wallet: {:?} Config: {:?}", e, config)); wallet.keychain = Some(keychain); @@ -382,7 +390,11 @@ impl LocalServerContainer { .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_n = HTTPNodeClient::new( + &config.check_node_api_http_addr, + None, + std::time::Duration::from_secs(60), + ); let client_w = HTTPWalletCommAdapter::new(); let max_outputs = 500; diff --git a/integration/tests/simulnet.rs b/integration/tests/simulnet.rs index df407ec5d..f3e9b0930 100644 --- a/integration/tests/simulnet.rs +++ b/integration/tests/simulnet.rs @@ -908,7 +908,11 @@ fn replicate_tx_fluff_failure() { // Create Wallet 1 (Mining Input) and start it listening // Wallet 1 post to another node, just for fun - let client1 = HTTPNodeClient::new("http://127.0.0.1:23003", None); + let client1 = HTTPNodeClient::new( + "http://127.0.0.1:23003", + None, + std::time::Duration::from_secs(60), + ); let client1_w = HTTPWalletCommAdapter::new(); let wallet1 = create_wallet("target/tmp/tx_fluff/wallet1", client1.clone()); let _wallet1_handle = thread::spawn(move || { @@ -917,7 +921,11 @@ fn replicate_tx_fluff_failure() { }); // Create Wallet 2 (Recipient) and launch - let client2 = HTTPNodeClient::new("http://127.0.0.1:23001", None); + let client2 = HTTPNodeClient::new( + "http://127.0.0.1:23001", + None, + std::time::Duration::from_secs(60), + ); let wallet2 = create_wallet("target/tmp/tx_fluff/wallet2", client2.clone()); let _wallet2_handle = thread::spawn(move || { controller::foreign_listener(wallet2, "127.0.0.1:33001", None) diff --git a/libwallet/Cargo.toml b/libwallet/Cargo.toml index cfed06b70..fd8842b47 100644 --- a/libwallet/Cargo.toml +++ b/libwallet/Cargo.toml @@ -7,31 +7,33 @@ license = "Apache-2.0" repository = "https://github.com/mimblewimble/grin-wallet" keywords = [ "crypto", "grin", "mimblewimble" ] exclude = ["**/*.grin", "**/*.grin2"] -#build = "src/build/build.rs" edition = "2021" +[features] +mwixnet-test = [] + [dependencies] blake2-rfc = "0.2" rand = "0.6" serde = "1" serde_derive = "1" +serde_with = { version = "1", features = ["chrono"] } serde_json = "1" log = "0.4" -uuid = { version = "0.8", features = ["serde", "v4"] } +uuid = { version = "1.23.4", 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" +ed25519-dalek = "2" +x25519-dalek = { version = "2", features = ["static_secrets"] } base64 = "0.9" regex = "1.3" sha2 = "0.10.0" bs58 = "0.3" -age = "0.7" -curve25519-dalek = "2.1" -secrecy = "0.6" +age = "0.8" +curve25519-dalek = "5" bech32 = "0.7" byteorder = "1.3" num-bigint = "0.2" @@ -45,29 +47,9 @@ grin_wallet_config = { path = "../config", version = "5.4.1" } ##### Grin Imports -# For Release -grin_core = "5.4.1" -grin_keychain = "5.4.1" -grin_util = "5.4.1" -grin_store = "5.4.1" - -# 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"} +grin_core = { path = "../grin/core"} +grin_keychain = { path = "../grin/keychain"} +grin_util = { path = "../grin/util"} +grin_store = { path = "../grin/store"} -##### +##### diff --git a/libwallet/src/api_impl/foreign.rs b/libwallet/src/api_impl/foreign.rs index 315091cd4..7a9cc63cf 100644 --- a/libwallet/src/api_impl/foreign.rs +++ b/libwallet/src/api_impl/foreign.rs @@ -13,9 +13,17 @@ // limitations under the License. //! Generic implementation of owner API functions + use strum::IntoEnumIterator; +use super::owner::tx_lock_outputs; +use crate::api_impl::owner::contract_new as owner_contract_new; +use crate::api_impl::owner::contract_sign as owner_contract_sign; use crate::api_impl::owner::{check_ttl, post_tx}; +use crate::api_impl::types::update_tx_slate_state; +use crate::backend::WalletBackend; +use crate::contract::proofs::InvoiceProof; +use crate::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI}; use crate::grin_core::core::FeeFields; use crate::grin_keychain::Keychain; use crate::grin_util::secp::key::SecretKey; @@ -23,10 +31,8 @@ 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; +use ed25519_dalek::VerifyingKey as DalekPublicKey; const FOREIGN_API_VERSION: u16 = 2; @@ -39,32 +45,30 @@ pub fn check_version() -> VersionInfo { } /// Build a coinbase transaction -pub fn build_coinbase<'a, T: ?Sized, C, K>( - w: &mut T, +pub fn build_coinbase( + w: &mut WalletBackend, keychain_mask: Option<&SecretKey>, block_fees: &BlockFees, test_mode: bool, ) -> Result where - T: WalletBackend<'a, C, K>, - C: NodeClient + 'a, - K: Keychain + 'a, + C: NodeClient, + K: Keychain, { - updater::build_coinbase(&mut *w, keychain_mask, block_fees, test_mode) + updater::build_coinbase(w, keychain_mask, block_fees, test_mode) } /// Receive a tx as recipient -pub fn receive_tx<'a, T: ?Sized, C, K>( - w: &mut T, +pub fn receive_tx( + w: &mut WalletBackend, keychain_mask: Option<&SecretKey>, slate: &Slate, dest_acct_name: Option<&str>, use_test_rng: bool, ) -> Result where - T: WalletBackend<'a, C, K>, - C: NodeClient + 'a, - K: Keychain + 'a, + C: NodeClient, + K: Keychain, { let mut ret_slate = slate.clone(); check_ttl(w, &ret_slate)?; @@ -80,7 +84,7 @@ where }; // Don't do this multiple times let tx = updater::retrieve_txs( - &mut *w, + w, None, Some(ret_slate.id), None, @@ -99,7 +103,7 @@ where let keychain = w.keychain(keychain_mask)?; let context = tx::add_output_to_slate( - &mut *w, + w, keychain_mask, &mut ret_slate, height, @@ -114,35 +118,38 @@ where 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); + if let Some(saddr) = p.sender_address { + let sig = tx::create_payment_proof_signature( + ret_slate.amount, + &excess, + saddr, + address::address_from_derivation_path(&keychain, &parent_key_id, 0)?, + )?; + + p.promise_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; + update_tx_slate_state(w, keychain_mask, &parent_key_id, &ret_slate)?; Ok(ret_slate) } /// Receive a tx that this wallet has issued -pub fn finalize_tx<'a, T: ?Sized, C, K>( - w: &mut T, +pub fn finalize_tx( + w: &mut WalletBackend, keychain_mask: Option<&SecretKey>, slate: &Slate, post_automatically: bool, ) -> Result where - T: WalletBackend<'a, C, K>, - C: NodeClient + 'a, - K: Keychain + 'a, + C: NodeClient, + K: Keychain, { let mut sl = slate.clone(); let mut context = w.get_private_context(keychain_mask, sl.id.as_bytes())?; @@ -154,10 +161,10 @@ where 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)?; + selection::repopulate_tx(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)?; + tx::complete_tx(w, keychain_mask, &mut sl, &context)?; + tx::update_stored_tx(w, keychain_mask, &context, &mut sl, true)?; { let mut batch = w.batch(keychain_mask)?; batch.delete_private_context(sl.id.as_bytes())?; @@ -165,6 +172,9 @@ where } sl.state = SlateState::Invoice3; sl.amount = 0; + + let parent_key_id = w.parent_key_id(); + update_tx_slate_state(w, keychain_mask, &parent_key_id, &sl)?; } else if sl.state == SlateState::Standard2 { let keychain = w.keychain(keychain_mask)?; let parent_key_id = w.parent_key_id(); @@ -175,7 +185,7 @@ where 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)?; + tx::new_tx_slate(w, context.amount, false, 2, false, args.ttl_blocks)?; let temp_context = selection::build_send_tx( w, &keychain, @@ -211,11 +221,11 @@ where // Add our contribution to the offset sl.adjust_offset(&keychain, &context)?; - selection::repopulate_tx(&mut *w, keychain_mask, &mut sl, &context, true)?; + selection::repopulate_tx(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)?; + tx::complete_tx(w, keychain_mask, &mut sl, &context)?; + tx::verify_slate_payment_proof(w, keychain_mask, &parent_key_id, &context, &sl)?; + tx::update_stored_tx(w, keychain_mask, &context, &sl, false)?; { let mut batch = w.batch(keychain_mask)?; batch.delete_private_context(sl.id.as_bytes())?; @@ -223,6 +233,8 @@ where } sl.state = SlateState::Standard3; sl.amount = 0; + + update_tx_slate_state(w, keychain_mask, &parent_key_id, &sl)?; } else { return Err(Error::SlateState); } @@ -231,3 +243,91 @@ where } Ok(sl) } + +/// Initialize a receive transaction contract +pub fn contract_new( + w: &mut WalletBackend, + keychain_mask: Option<&SecretKey>, + args: &ContractNewArgsAPI, +) -> Result +where + C: NodeClient, + K: Keychain, +{ + let net_change = args.setup_args.net_change.ok_or_else(|| { + Error::GenericError("Contract requires a net change (--send or --receive)".to_string()) + })?; + if net_change <= 0 { + return Err(Error::GenericError( + "Can't create a non-receiving contract from a foreign API.".to_string(), + )); + } + owner_contract_new(w, keychain_mask, args) +} + +/// Sign a receive transaction contract +pub fn contract_sign( + w: &mut WalletBackend, + keychain_mask: Option<&SecretKey>, + args: &ContractSetupArgsAPI, + slate: &Slate, +) -> Result +where + C: NodeClient, + K: Keychain, +{ + let net_change = args.net_change.ok_or_else(|| { + Error::GenericError("Contract requires a net change (--send or --receive)".to_string()) + })?; + if net_change <= 0 { + return Err(Error::GenericError( + "Can't sign a non-receiving contract from a foreign API.".to_string(), + )); + } + owner_contract_sign(w, keychain_mask, args, slate) +} + +/// Verify an invoice payment proof +pub fn verify_payment_proof_invoice( + w: &mut WalletBackend, + recipient_address: &DalekPublicKey, + proof: &InvoiceProof, +) -> Result<(), Error> +where + C: NodeClient, + K: Keychain, +{ + let mut client = w.w2n_client().clone(); + + let wd = match proof.witness_data.clone() { + Some(w) => w, + None => { + return Err(Error::PaymentProof( + "Cannot verify invoice proof with no witness data".to_string(), + )) + } + }; + + let (retrieved_kernel, _) = match client.get_kernel(&wd.kernel_commitment, None, None) { + Err(e) => { + return Err(Error::PaymentProof(format!( + "Error retrieving kernel from chain: {}", + e + ))); + } + Ok(None) => { + return Err(Error::PaymentProof(format!( + "Transaction kernel with excess {:?} not found on chain", + wd.kernel_commitment + ))); + } + Ok(Some((k, _, index))) => (k, index), + }; + + // Now verify with retrieved data + proof.verify_witness( + recipient_address, + &retrieved_kernel.excess_sig, + &retrieved_kernel.msg_to_sign()?, + ) +} diff --git a/libwallet/src/api_impl/owner.rs b/libwallet/src/api_impl/owner.rs index 017bf4e97..a8884670b 100644 --- a/libwallet/src/api_impl/owner.rs +++ b/libwallet/src/api_impl/owner.rs @@ -14,36 +14,41 @@ //! Generic implementation of owner API functions +use std::cmp; use uuid::Uuid; use crate::api_impl::foreign::finalize_tx as foreign_finalize; +use crate::contract::proofs::{InvoiceProof, ProofWitness}; use crate::grin_core::core::hash::Hashed; use crate::grin_core::core::{FeeFields, Output, OutputFeatures, Transaction}; use crate::grin_core::libtx::proof; use crate::grin_keychain::ViewKey; -use crate::grin_util::secp::{key::SecretKey, pedersen::Commitment}; -use crate::grin_util::Mutex; -use crate::grin_util::ToHex; -use crate::util::{OnionV3Address, OnionV3AddressError}; +use crate::grin_util::secp::key::SecretKey; +use crate::grin_util::secp::pedersen::Commitment; +use crate::grin_util::{Mutex, ToHex}; +use crate::util::OnionV3Address; use crate::api_impl::owner_updater::StatusMessage; +use crate::contract::types::{ + ContractNewArgsAPI, ContractRevokeArgsAPI, ContractSetupArgsAPI, ContractView, +}; use crate::grin_keychain::{BlindingFactor, Identifier, Keychain, SwitchCommitmentType}; + use crate::internal::{keys, scan, selection, tx, updater}; use crate::slate::{PaymentInfo, Slate, SlateState}; -use crate::types::{AcctPathMapping, NodeClient, TxLogEntry, WalletBackend, WalletInfo}; -use crate::Error; +use crate::types::{AcctPathMapping, NodeClient, TxLogEntry, WalletInfo}; use crate::{ - address, + address, contract, mwixnet::{create_onion, ComSignature, Hop, MixnetReqCreationParams, SwapReq}, - wallet_lock, BuiltOutput, InitTxArgs, IssueInvoiceTxArgs, NodeHeightResult, + wallet_lock, BuiltOutput, Error, InitTxArgs, IssueInvoiceTxArgs, NodeHeightResult, OutputCommitMapping, PaymentProof, RetrieveTxQueryArgs, ScannedBlockInfo, Slatepack, - SlatepackAddress, Slatepacker, SlatepackerArgs, TxLogEntryType, ViewWallet, WalletInitStatus, - WalletInst, WalletLCProvider, + SlatepackAddress, Slatepacker, SlatepackerArgs, TxLogEntryType, ViewWallet, WalletBackend, + WalletInitStatus, WalletInst, WalletLCProvider, }; - -use ed25519_dalek::PublicKey as DalekPublicKey; -use ed25519_dalek::SecretKey as DalekSecretKey; +use chrono::prelude::DateTime; +use ed25519_dalek::SigningKey as DalekSecretKey; use ed25519_dalek::Verifier; +use ed25519_dalek::VerifyingKey as DalekPublicKey; use x25519_dalek::{PublicKey as xPublicKey, StaticSecret}; use std::convert::{TryFrom, TryInto}; @@ -51,35 +56,32 @@ use std::sync::mpsc::Sender; use std::sync::Arc; /// List of accounts -pub fn accounts<'a, T: ?Sized, C, K>(w: &mut T) -> Result, Error> +pub fn accounts(w: &mut WalletBackend) -> Result, Error> where - T: WalletBackend<'a, C, K>, - C: NodeClient + 'a, - K: Keychain + 'a, + C: NodeClient, + K: Keychain, { - keys::accounts(&mut *w) + keys::accounts(w) } /// new account path -pub fn create_account_path<'a, T: ?Sized, C, K>( - w: &mut T, +pub fn create_account_path( + w: &mut WalletBackend, keychain_mask: Option<&SecretKey>, label: &str, ) -> Result where - T: WalletBackend<'a, C, K>, - C: NodeClient + 'a, - K: Keychain + 'a, + C: NodeClient, + K: Keychain, { - keys::new_acct_path(&mut *w, keychain_mask, label) + keys::new_acct_path(w, keychain_mask, label) } /// set active account -pub fn set_active_account<'a, T: ?Sized, C, K>(w: &mut T, label: &str) -> Result<(), Error> +pub fn set_active_account(w: &mut WalletBackend, label: &str) -> Result<(), Error> where - T: WalletBackend<'a, C, K>, - C: NodeClient + 'a, - K: Keychain + 'a, + C: NodeClient, + K: Keychain, { w.set_parent_key_id_by_name(label) } @@ -136,16 +138,7 @@ where let parent_key_id = w.parent_key_id(); let k = w.keychain(keychain_mask)?; let sec_addr_key = address::address_from_derivation_path(&k, &parent_key_id, index)?; - let d_skey = match DalekSecretKey::from_bytes(&sec_addr_key.0) { - Ok(k) => k, - Err(e) => { - return Err(OnionV3AddressError::InvalidPrivateKey(format!( - "Unable to create secret key: {}", - e - )) - .into()); - } - }; + let d_skey = DalekSecretKey::from_bytes(&sec_addr_key.0); Ok(d_skey) } @@ -195,7 +188,7 @@ where dec_key: None, }); let slatepack = packer.deser_slatepack(slatepack.as_bytes(), true)?; - return packer.get_slate(&slatepack); + packer.get_slate(&slatepack) } else { for index in secret_indices { let dec_key = Some(get_slatepack_secret_key( @@ -217,11 +210,11 @@ where }; return packer.get_slate(&slatepack); } - return Err(Error::SlatepackDecryption( + Err(Error::SlatepackDecryption( "Could not decrypt slatepack with any provided index on the address derivation path" .to_owned(), ) - .into()); + .into()) } } @@ -301,13 +294,7 @@ where Ok(( validated, - updater::retrieve_outputs( - &mut **w, - keychain_mask, - include_spent, - tx_id, - Some(&parent_key_id), - )?, + updater::retrieve_outputs(w, keychain_mask, include_spent, tx_id, Some(&parent_key_id))?, )) } @@ -340,7 +327,7 @@ where wallet_lock!(wallet_inst, w); let parent_key_id = w.parent_key_id(); let txs = updater::retrieve_txs( - &mut **w, + w, tx_id, tx_slate_id, query_args, @@ -377,7 +364,7 @@ where wallet_lock!(wallet_inst, w); let parent_key_id = w.parent_key_id(); - let wallet_info = updater::retrieve_info(&mut **w, &parent_key_id, minimum_confirmations)?; + let wallet_info = updater::retrieve_info(w, &parent_key_id, minimum_confirmations)?; Ok((validated, wallet_info)) } @@ -468,8 +455,8 @@ where } }; Ok(PaymentProof { - amount: amount, - excess: excess, + amount, + excess, recipient_address: SlatepackAddress::new(&proof.receiver_address), recipient_sig: r_sig, sender_address: SlatepackAddress::new(&proof.sender_address), @@ -477,18 +464,169 @@ where }) } +/// Retrieve invoice payment proof +/// TODO: Need to unify with legacy above +pub fn retrieve_payment_proof_invoice<'a, L, C, K>( + wallet_inst: Arc>>>, + keychain_mask: Option<&SecretKey>, + status_send_channel: &Option>, + refresh_from_node: bool, + tx_id: Option, + tx_slate_id: Option, +) -> Result +where + L: WalletLCProvider<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + if tx_id.is_none() && tx_slate_id.is_none() { + return Err(Error::PaymentProofRetrieval( + "Transaction ID or Slate UUID must be specified".to_owned(), + )); + } + if refresh_from_node { + update_wallet_state( + wallet_inst.clone(), + keychain_mask, + status_send_channel, + false, + )? + } else { + false + }; + let txs = retrieve_txs( + wallet_inst.clone(), + keychain_mask, + status_send_channel, + refresh_from_node, + tx_id, + tx_slate_id, + None, + )?; + if txs.1.len() != 1 { + return Err(Error::PaymentProofRetrieval( + "Transaction doesn't exist".to_owned(), + )); + } + // Pull out all needed fields, returning an error if they're not present + let tx = txs.1[0].clone(); + let amount = if tx.amount_credited >= tx.amount_debited { + tx.amount_credited - tx.amount_debited + } else { + // TODO: Invoice proof not expecting fee included here + tx.amount_debited - tx.amount_credited + }; + + let (mut proof, sender_part_sig) = match tx.payment_proof { + Some(p) => { + if p.receiver_public_nonce.is_none() { + return Err(Error::PaymentProofRetrieval( + "Invoice Proof requires stored receiver public nonce".into(), + )); + }; + if p.receiver_public_excess.is_none() { + return Err(Error::PaymentProofRetrieval( + "Invoice Proof requires stored receiver public excess".into(), + )); + }; + if p.timestamp.is_none() { + return Err(Error::PaymentProofRetrieval( + "Invoice Proof requires stored timestamp".into(), + )); + }; + if p.sender_part_sig.is_none() { + return Err(Error::PaymentProofRetrieval( + "Invoice Proof requires stored sender partial signature".into(), + )); + }; + + ( + InvoiceProof { + proof_type: if let Some(t) = p.proof_type { t } else { 1u8 }, + amount, + receiver_public_nonce: p.receiver_public_nonce.unwrap(), + receiver_public_excess: p.receiver_public_excess.unwrap(), + sender_address: p.sender_address, + timestamp: p.timestamp.unwrap().timestamp(), + memo: p.memo, + promise_signature: p.promise_signature, + witness_data: None, + }, + p.sender_part_sig.unwrap(), + ) + } + None => { + return Err(Error::PaymentProofRetrieval( + "Transaction does not contain a payment proof".to_owned(), + )); + } + }; + + // Now to kernel lookup, to fill in the witness data + // Check kernel exists + let mut client = { + wallet_lock!(wallet_inst, w); + w.w2n_client().clone() + }; + + let kernel_excess = match tx.kernel_excess { + Some(k) => k, + None => { + return Err(Error::PaymentProofRetrieval(format!( + "Invoice proof transaction kernel excess missing", + ))) + } + }; + + let (retrieved_kernel, index) = match client.get_kernel(&kernel_excess, None, None) { + Err(e) => { + return Err(Error::PaymentProof(format!( + "Error retrieving kernel from chain: {}", + e + ))); + } + Ok(None) => { + return Err(Error::PaymentProof(format!( + "Transaction kernel with excess {:?} not found on chain", + kernel_excess + ))); + } + Ok(Some((k, _, index))) => (k, index), + }; + + proof.witness_data = Some(ProofWitness { + kernel_index: index, + kernel_commitment: retrieved_kernel.excess, + sender_partial_sig: sender_part_sig, + }); + + Ok(proof) +} + /// Initiate tx as sender -pub fn init_send_tx<'a, T: ?Sized, C, K>( - w: &mut T, +pub fn init_send_tx( + w: &mut WalletBackend, keychain_mask: Option<&SecretKey>, args: InitTxArgs, use_test_rng: bool, ) -> Result where - T: WalletBackend<'a, C, K>, - C: NodeClient + 'a, - K: Keychain + 'a, + C: NodeClient, + K: Keychain, { + let payment_proof_address = if let Some(a) = &args.payment_proof_recipient_address { + if a.valid_network() { + Some(a) + } else { + return Err(Error::PaymentProofRetrieval(format!( + "Wrong network for address {}", + a + ))); + } + } else { + None + }; + let parent_key_id = match &args.src_acct_name { Some(d) => { let pm = w.get_acct_path(d.clone())?; @@ -500,14 +638,7 @@ where None => w.parent_key_id(), }; - let mut slate = tx::new_tx_slate( - &mut *w, - args.amount, - false, - 2, - use_test_rng, - args.ttl_blocks, - )?; + let mut slate = tx::new_tx_slate(w, args.amount, false, 2, use_test_rng, args.ttl_blocks)?; if let Some(v) = args.target_slate_version { slate.version_info.version = v; @@ -517,7 +648,7 @@ where // back if let Some(true) = args.estimate_only { let (total, fee) = tx::estimate_send_tx( - &mut *w, + w, keychain_mask, args.amount, args.amount_includes_fee.unwrap_or(false), @@ -535,7 +666,7 @@ where let height = w.w2n_client().get_chain_tip()?.0; let mut context = if args.late_lock.unwrap_or(false) { tx::create_late_lock_context( - &mut *w, + w, keychain_mask, &mut slate, height, @@ -545,7 +676,7 @@ where )? } else { tx::add_inputs_to_slate( - &mut *w, + w, keychain_mask, &mut slate, height, @@ -565,16 +696,18 @@ where // probably want to allow sender to specify which one let deriv_path = 0u32; - if let Some(a) = args.payment_proof_recipient_address { + if let Some(a) = payment_proof_address { let k = w.keychain(keychain_mask)?; let sec_addr_key = address::address_from_derivation_path(&k, &parent_key_id, deriv_path)?; let sender_address = OnionV3Address::from_private(&sec_addr_key.0)?; slate.payment_proof = Some(PaymentInfo { - sender_address: sender_address.to_ed25519()?, + sender_address: Some(sender_address.to_ed25519()?), receiver_address: a.pub_key, - receiver_signature: None, + promise_signature: None, + timestamp: DateTime::from_timestamp(0, 0).unwrap(), + memo: None, }); context.payment_proof_derivation_index = Some(deriv_path); @@ -594,16 +727,15 @@ where } /// Initiate a transaction as the recipient (invoicing) -pub fn issue_invoice_tx<'a, T: ?Sized, C, K>( - w: &mut T, +pub fn issue_invoice_tx( + w: &mut WalletBackend, keychain_mask: Option<&SecretKey>, args: IssueInvoiceTxArgs, use_test_rng: bool, ) -> Result where - T: WalletBackend<'a, C, K>, - C: NodeClient + 'a, - K: Keychain + 'a, + C: NodeClient, + K: Keychain, { let parent_key_id = match args.dest_acct_name { Some(d) => { @@ -616,10 +748,10 @@ where None => w.parent_key_id(), }; - let mut slate = tx::new_tx_slate(&mut *w, args.amount, true, 2, use_test_rng, None)?; + let mut slate = tx::new_tx_slate(w, args.amount, true, 2, use_test_rng, None)?; let height = w.w2n_client().get_chain_tip()?.0; let context = tx::add_output_to_slate( - &mut *w, + w, keychain_mask, &mut slate, height, @@ -647,17 +779,16 @@ where /// Receive an invoice tx, essentially adding inputs to whatever /// output was specified -pub fn process_invoice_tx<'a, T: ?Sized, C, K>( - w: &mut T, +pub fn process_invoice_tx( + w: &mut WalletBackend, keychain_mask: Option<&SecretKey>, slate: &Slate, args: InitTxArgs, use_test_rng: bool, ) -> Result where - T: WalletBackend<'a, C, K>, - C: NodeClient + 'a, - K: Keychain + 'a, + C: NodeClient, + K: Keychain, { let mut ret_slate = slate.clone(); check_ttl(w, &ret_slate)?; @@ -673,7 +804,7 @@ where }; // Don't do this multiple times let tx = updater::retrieve_txs( - &mut *w, + w, None, Some(ret_slate.id), None, @@ -703,7 +834,7 @@ where let context_res = w.get_private_context(keychain_mask, slate.id.as_bytes()); let mut context = tx::add_inputs_to_slate( - &mut *w, + w, keychain_mask, &mut ret_slate, height, @@ -749,7 +880,7 @@ where } } - selection::repopulate_tx(&mut *w, keychain_mask, &mut ret_slate, &context, false)?; + selection::repopulate_tx(w, keychain_mask, &mut ret_slate, &context, false)?; // Save the aggsig context in our DB for when we // recieve the transaction back @@ -768,15 +899,14 @@ where } /// Lock sender outputs -pub fn tx_lock_outputs<'a, T: ?Sized, C, K>( - w: &mut T, +pub fn tx_lock_outputs( + w: &mut WalletBackend, keychain_mask: Option<&SecretKey>, slate: &Slate, ) -> Result<(), Error> where - T: WalletBackend<'a, C, K>, - C: NodeClient + 'a, - K: Keychain + 'a, + C: NodeClient, + K: Keychain, { let context = w.get_private_context(keychain_mask, slate.id.as_bytes())?; let mut excess_override = None; @@ -785,7 +915,7 @@ where if sl.tx == None { sl.tx = Some(Slate::empty_transaction()); - selection::repopulate_tx(&mut *w, keychain_mask, &mut sl, &context, true)?; + selection::repopulate_tx(w, keychain_mask, &mut sl, &context, true)?; } if slate.participant_data.len() == 1 { @@ -794,26 +924,18 @@ where } let height = w.w2n_client().get_chain_tip()?.0; - selection::lock_tx_context( - &mut *w, - keychain_mask, - &sl, - height, - &context, - excess_override, - ) + selection::lock_tx_context(w, keychain_mask, &sl, height, &context, excess_override) } /// Finalize slate -pub fn finalize_tx<'a, T: ?Sized, C, K>( - w: &mut T, +pub fn finalize_tx( + w: &mut WalletBackend, keychain_mask: Option<&SecretKey>, slate: &Slate, ) -> Result where - T: WalletBackend<'a, C, K>, - C: NodeClient + 'a, - K: Keychain + 'a, + C: NodeClient, + K: Keychain, { foreign_finalize(w, keychain_mask, slate, false) } @@ -843,24 +965,27 @@ where } wallet_lock!(wallet_inst, w); let parent_key_id = w.parent_key_id(); - tx::cancel_tx(&mut **w, keychain_mask, &parent_key_id, tx_id, tx_slate_id) + tx::cancel_tx(w, keychain_mask, &parent_key_id, tx_id, tx_slate_id) } /// get stored tx /// crashes if stored tx has total fees exceeding 2^40 nanogrin -pub fn get_stored_tx<'a, T: ?Sized, C, K>( - w: &T, +pub fn get_stored_tx( + w: &WalletBackend, tx_id: Option, slate_id: Option<&Uuid>, ) -> Result, Error> where - T: WalletBackend<'a, C, K>, - C: NodeClient + 'a, - K: Keychain + 'a, + C: NodeClient, + K: Keychain, { let mut uuid = None; if let Some(i) = tx_id { - let tx = w.tx_log_iter().find(|t| t.id == i); + let tx = w + .tx_log_iter()? + .filter(|tx| tx.is_ok()) + .map(|tx| tx.unwrap()) + .find(|t| t.id == i); if let Some(t) = tx { uuid = t.tx_slate_id; } @@ -895,9 +1020,9 @@ where /// Posts a transaction to the chain /// take a client impl instead of wallet so as not to have to lock the wallet -pub fn post_tx<'a, C>(client: &C, tx: &Transaction, fluff: bool) -> Result<(), Error> +pub fn post_tx(client: &C, tx: &Transaction, fluff: bool) -> Result<(), Error> where - C: NodeClient + 'a, + C: NodeClient, { let res = client.post_tx(tx, fluff); if let Err(e) = res { @@ -929,8 +1054,7 @@ where let is_hex = rewind_hash.chars().all(|c| c.is_ascii_hexdigit()); let rewind_hash = rewind_hash.to_lowercase(); if !(is_hex && rewind_hash.len() == 64) { - let msg = format!("Invalid Rewind Hash"); - return Err(Error::RewindHash(msg)); + return Err(Error::RewindHash("Invalid Rewind Hash".to_string())); } let tip = { @@ -938,10 +1062,7 @@ where w.w2n_client().get_chain_tip()? }; - let start_height = match start_height { - Some(h) => h, - None => 1, - }; + let start_height = start_height.unwrap_or_else(|| 1); let info = scan::scan_rewind_hash( wallet_inst, @@ -974,25 +1095,35 @@ where w.w2n_client().get_chain_tip()? }; - let start_height = match start_height { - Some(h) => h, - None => 1, - }; + let start_height = start_height.unwrap_or_else(|| 1); - let mut info = scan::scan( - wallet_inst.clone(), - keychain_mask, - delete_unconfirmed, - start_height, - tip.0, - status_send_channel, - )?; - info.hash = tip.1; + // Scan every 10k heights to save data between batches in case of interruption. + let mut total_pmmr_range = None; + for h in (start_height..tip.0 + 1).step_by(10001) { + let batch_end_height = cmp::min(tip.0, h + 10000); + let (mut info, range) = scan::scan( + wallet_inst.clone(), + keychain_mask, + delete_unconfirmed, + h, + batch_end_height, + start_height, + tip.0, + total_pmmr_range, + status_send_channel, + )?; + info.hash = if batch_end_height == tip.0 { + tip.1.clone() + } else { + "".to_owned() + }; + total_pmmr_range = Some(range); - wallet_lock!(wallet_inst, w); - let mut batch = w.batch(keychain_mask)?; - batch.save_last_scanned_block(info)?; - batch.commit()?; + wallet_lock!(wallet_inst, w); + let mut batch = w.batch(keychain_mask)?; + batch.save_last_scanned_block(info)?; + batch.commit()?; + } Ok(()) } @@ -1019,10 +1150,12 @@ where }), Err(_) => { let outputs = retrieve_outputs(wallet_inst, keychain_mask, &None, true, false, None)?; - let height = match outputs.1.iter().map(|m| m.output.height).max() { - Some(height) => height, - None => 0, - }; + let height = outputs + .1 + .iter() + .map(|m| m.output.height) + .max() + .unwrap_or_else(|| 0); Ok(NodeHeightResult { height, header_hash: "".to_owned(), @@ -1032,6 +1165,9 @@ where } } +/// Wallet scan window in blocks (48 hours). +pub const REORG_RESCAN_WINDOW: u64 = 24 * 60 * 2; + /// Experimental, wrap the entire definition of how a wallet's state is updated pub fn update_wallet_state<'a, L, C, K>( wallet_inst: Arc>>>, @@ -1079,7 +1215,7 @@ where // Step 2: Update outstanding transactions with no change outputs by kernel let mut txs = { wallet_lock!(wallet_inst, w); - updater::retrieve_txs(&mut **w, None, None, None, Some(&parent_key_id), true)? + updater::retrieve_txs(w, None, None, None, Some(&parent_key_id), true)? }; result = update_txs_via_kernel(wallet_inst.clone(), keychain_mask, &mut txs)?; if !result { @@ -1110,23 +1246,24 @@ where let last_scanned_block = { wallet_lock!(wallet_inst, w); match w.init_status()? { - WalletInitStatus::InitNeedsScanning => ScannedBlockInfo { - height: 0, - hash: "".to_owned(), - start_pmmr_index: 0, - last_pmmr_index: 0, - }, WalletInitStatus::InitNoScanning => ScannedBlockInfo { height: tip.clone().0, hash: tip.clone().1, start_pmmr_index: 0, last_pmmr_index: 0, }, - WalletInitStatus::InitComplete => w.last_scanned_block()?, + _ => w.last_scanned_block()?, } }; - let start_index = last_scanned_block.height.saturating_sub(100); + let start_height = last_scanned_block + .height + .saturating_sub(REORG_RESCAN_WINDOW); + + debug!( + "update_wallet_state: last_scanned_block: {:?}", + last_scanned_block + ); if last_scanned_block.height == 0 { let msg = "This wallet has not been scanned against the current chain. Beginning full scan... (this first scan may take a while, but subsequent scans will be much quicker)".to_string(); @@ -1135,18 +1272,28 @@ where } } - let mut info = scan::scan( - wallet_inst.clone(), - keychain_mask, - false, - start_index, - tip.0, - status_send_channel, - )?; - - info.hash = tip.1; + // Scan every 10k heights to save data between batches in case of interruption. + let mut total_pmmr_range = None; + for h in (start_height..tip.0 + 1).step_by(10001) { + let batch_end_height = cmp::min(tip.0, h + 10000); + let (mut info, range) = scan::scan( + wallet_inst.clone(), + keychain_mask, + false, + h, + batch_end_height, + start_height, + tip.0, + total_pmmr_range, + status_send_channel, + )?; + info.hash = if batch_end_height == tip.0 { + tip.1.clone() + } else { + "".to_owned() + }; + total_pmmr_range = Some(range); - { wallet_lock!(wallet_inst, w); let mut batch = w.batch(keychain_mask)?; batch.save_last_scanned_block(info)?; @@ -1161,7 +1308,7 @@ where if tip.0 >= e { wallet_lock!(wallet_inst, w); let parent_key_id = w.parent_key_id(); - tx::cancel_tx(&mut **w, keychain_mask, &parent_key_id, Some(tx.id), None)?; + tx::cancel_tx(w, keychain_mask, &parent_key_id, Some(tx.id), None)?; } } } @@ -1170,11 +1317,10 @@ where } /// Check TTL -pub fn check_ttl<'a, T: ?Sized, C, K>(w: &mut T, slate: &Slate) -> Result<(), Error> +pub fn check_ttl(w: &mut WalletBackend, slate: &Slate) -> Result<(), Error> where - T: WalletBackend<'a, C, K>, - C: NodeClient + 'a, - K: Keychain + 'a, + C: NodeClient, + K: Keychain, { // Refuse if TTL is expired let last_confirmed_height = w.last_confirmed_height()?; @@ -1240,14 +1386,9 @@ where return Err(Error::PaymentProof("Invalid sender signature".to_owned())); }; - // for now, simple test as to whether one of the addresses belongs to this wallet + // for now, simple test whether one of the addresses belongs to this wallet let sec_key = address::address_from_derivation_path(&keychain, &parent_key_id, 0)?; - let d_skey = match DalekSecretKey::from_bytes(&sec_key.0) { - Ok(k) => k, - Err(e) => { - return Err(Error::ED25519Key(format!("{}", e))); - } - }; + let d_skey = DalekSecretKey::from_bytes(&sec_key.0); let my_address_pubkey: DalekPublicKey = (&d_skey).into(); let sender_mine = my_address_pubkey == sender_pubkey; @@ -1269,7 +1410,7 @@ where { wallet_lock!(wallet_inst, w); let parent_key_id = w.parent_key_id(); - match updater::refresh_outputs(&mut **w, keychain_mask, &parent_key_id, update_all) { + match updater::refresh_outputs(w, keychain_mask, &parent_key_id, update_all) { Ok(_) => Ok(true), Err(e) => { if let Error::InvalidKeychainMask = e { @@ -1336,20 +1477,19 @@ where } /// Builds an output for the wallet's next available key -pub fn build_output<'a, T: ?Sized, C, K>( - w: &mut T, +pub fn build_output( + w: &mut WalletBackend, keychain_mask: Option<&SecretKey>, features: OutputFeatures, amount: u64, ) -> Result where - T: WalletBackend<'a, C, K>, - C: NodeClient + 'a, - K: Keychain + 'a, + C: NodeClient, + K: Keychain, { let k = w.keychain(keychain_mask)?; - let key_id = keys::next_available_key(&mut *w, keychain_mask)?; + let key_id = keys::next_available_key(w, keychain_mask)?; let blind = k.derive_key(amount, &key_id, SwitchCommitmentType::Regular)?; let commit = k.secp().commit(amount, blind.clone())?; @@ -1369,14 +1509,85 @@ where Ok(BuiltOutput { blind: BlindingFactor::from_secret_key(blind), - key_id: key_id, - output: output, + key_id, + output, }) } +// Contract implementation + +/// Initialize transaction contract +pub fn contract_new( + w: &mut WalletBackend, + keychain_mask: Option<&SecretKey>, + args: &ContractNewArgsAPI, +) -> Result +where + C: NodeClient, + K: Keychain, +{ + contract::new(&mut *w, keychain_mask, &args.setup_args, None) +} + +/// View a transaction contract +pub fn contract_view( + w: &mut WalletBackend, + keychain_mask: Option<&SecretKey>, + slate: &Slate, +) -> Result +where + C: NodeClient, + K: Keychain, +{ + let mut slate = slate.clone(); + contract::view(&mut *w, keychain_mask, &mut slate, "") +} + +/// Sign transaction contract +pub fn contract_sign( + w: &mut WalletBackend, + keychain_mask: Option<&SecretKey>, + args: &ContractSetupArgsAPI, + slate: &Slate, +) -> Result +where + C: NodeClient, + K: Keychain, +{ + contract::sign(&mut *w, keychain_mask, slate, &args) +} + +/// Revoke transaction contract +pub fn contract_revoke( + w: &mut WalletBackend, + keychain_mask: Option<&SecretKey>, + args: &ContractRevokeArgsAPI, +) -> Result, Error> +where + C: NodeClient, + K: Keychain, +{ + contract::revoke(&mut *w, keychain_mask, &args) +} + +/// Return the slate participant index matching this wallet's context +pub fn get_slate_index_matching_my_context( + w: &mut WalletBackend, + keychain_mask: Option<&SecretKey>, + slate: &Slate, +) -> Result +where + C: NodeClient, + K: Keychain, +{ + let keychain = w.keychain(keychain_mask)?; + let context = w.get_private_context(keychain_mask, slate.id.as_bytes())?; + slate.find_index_matching_context(&keychain, &context) +} + /// Create MXMixnet request -pub fn create_mwixnet_req<'a, T: ?Sized, C, K>( - w: &mut T, +pub fn create_mwixnet_req( + w: &mut WalletBackend, keychain_mask: Option<&SecretKey>, params: &MixnetReqCreationParams, commitment: &Commitment, @@ -1384,9 +1595,8 @@ pub fn create_mwixnet_req<'a, T: ?Sized, C, K>( use_test_rng: bool, ) -> Result where - T: WalletBackend<'a, C, K>, - C: NodeClient + 'a, - K: Keychain + 'a, + C: NodeClient, + K: Keychain, { let parent_key_id = w.parent_key_id(); let keychain = w.keychain(keychain_mask)?; diff --git a/libwallet/src/api_impl/types.rs b/libwallet/src/api_impl/types.rs index d942cd612..d87a7bd8c 100644 --- a/libwallet/src/api_impl/types.rs +++ b/libwallet/src/api_impl/types.rs @@ -21,10 +21,12 @@ use crate::grin_util::secp::pedersen; use crate::slate_versions::ser as dalek_ser; use crate::slate_versions::SlateVersion; use crate::types::OutputData; -use crate::SlatepackAddress; +use crate::{Error, NodeClient, Slate, SlatepackAddress, WalletBackend}; use chrono::prelude::*; use ed25519_dalek::Signature as DalekSignature; +use grin_keychain::Keychain; +use grin_util::secp::SecretKey; pub use crate::mwixnet::{Hop, MixnetReqCreationParams, SwapReq}; @@ -102,7 +104,7 @@ pub struct InitTxSendArgs { /// Whether to use dandelion when posting. If false, skip the dandelion relay pub fluff: bool, /// If set, skip the Slatepack TOR send attempt - pub skip_tor: bool, + pub skip_tor: Option, } impl Default for InitTxArgs { @@ -202,6 +204,8 @@ pub struct RetrieveTxQueryArgs { pub include_received_only: Option, /// whether to only consider coinbase transactions pub include_coinbase_only: Option, + /// whether to only consider self spend transactions + pub include_self_spend_only: Option, /// whether to only consider reverted transactions pub include_reverted_only: Option, /// lower bound on the total amount (amount_credited - amount_debited), inclusive @@ -239,6 +243,7 @@ impl Default for RetrieveTxQueryArgs { include_sent_only: Some(false), include_received_only: Some(false), include_coinbase_only: Some(false), + include_self_spend_only: Some(false), include_reverted_only: Some(false), min_amount: None, max_amount: None, @@ -344,3 +349,39 @@ pub struct BuiltOutput { /// Output pub output: Output, } + +/// Update transaction slate state. +pub fn update_tx_slate_state( + wallet: &mut WalletBackend, + keychain_mask: Option<&SecretKey>, + parent_key_id: &Identifier, + slate: &Slate, +) -> Result<(), Error> +where + C: NodeClient, + K: Keychain, +{ + let mut bad_records = 0; + let tx = wallet + .tx_log_iter()? + .filter(|tx| { + if tx.is_err() { + bad_records += 1; + } + tx.is_ok() + }) + .map(|tx| tx.unwrap()) + .find(|tx| tx.tx_slate_id == Some(slate.id)); + if let Some(mut tx) = tx { + let mut batch = wallet.batch(keychain_mask)?; + tx.tx_slate_state = Some(slate.state.clone()); + batch.save_tx_log_entry(tx.clone(), parent_key_id)?; + batch.commit()?; + } else { + return Err(Error::Backend(format!( + "Tx log entry with slate id {} not found, there are {} bad tx log records", + slate.id, bad_records + ))); + } + Ok(()) +} diff --git a/libwallet/src/backend.rs b/libwallet/src/backend.rs new file mode 100644 index 000000000..5833563b0 --- /dev/null +++ b/libwallet/src/backend.rs @@ -0,0 +1,771 @@ +// 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 byteorder::{BigEndian, WriteBytesExt}; +use std::fs; +use std::fs::File; +use std::io::{Read, Write}; +use std::path::Path; + +use crate::blake2::blake2b::{Blake2b, Blake2bResult}; + +use crate::{ + AcctPathMapping, Context, Error, NodeClient, OutputData, ScannedBlockInfo, TxLogEntry, + WalletInitStatus, +}; +use grin_core::core::Transaction; +use grin_core::ser; +use grin_keychain::{ChildNumber, ExtKeychain, Identifier, Keychain, SwitchCommitmentType}; +use grin_store::{option_to_not_found, Store}; +use grin_util::secp::constants::SECRET_KEY_SIZE; +use grin_util::secp::SecretKey; +use grin_util::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"; + +const DB_PREFIXES: [u8; 9] = [ + OUTPUT_PREFIX, + DERIV_PREFIX, + CONFIRMED_HEIGHT_PREFIX, + PRIVATE_TX_CONTEXT_PREFIX, + TX_LOG_ENTRY_PREFIX, + TX_LOG_ID_PREFIX, + ACCOUNT_PATH_MAPPING_PREFIX, + LAST_SCANNED_BLOCK, + WALLET_INIT_STATUS, +]; + +/// Helper to derive XOR keys for storing private transaction keys in the DB +/// (blind_xor_key, nonce_xor_key) +fn private_ctx_xor_keys( + keychain: &K, + slate_id: &[u8], +) -> Result<([u8; SECRET_KEY_SIZE], [u8; SECRET_KEY_SIZE]), Error> +where + K: Keychain, +{ + let root_key = keychain.derive_key(0, &K::root_key_id(), SwitchCommitmentType::Regular)?; + + // derive XOR values for storing secret values in DB + // h(root_key|slate_id|"blind") + let mut hasher = Blake2b::new(SECRET_KEY_SIZE); + hasher.update(&root_key.0[..]); + hasher.update(&slate_id[..]); + hasher.update(&b"blind"[..]); + let blind_xor_key = hasher.finalize(); + let mut ret_blind = [0; SECRET_KEY_SIZE]; + ret_blind.copy_from_slice(&blind_xor_key.as_bytes()[0..SECRET_KEY_SIZE]); + + // h(root_key|slate_id|"nonce") + let mut hasher = Blake2b::new(SECRET_KEY_SIZE); + hasher.update(&root_key.0[..]); + hasher.update(&slate_id[..]); + hasher.update(&b"nonce"[..]); + let nonce_xor_key = hasher.finalize(); + let mut ret_nonce = [0; SECRET_KEY_SIZE]; + ret_nonce.copy_from_slice(&nonce_xor_key.as_bytes()[0..SECRET_KEY_SIZE]); + + Ok((ret_blind, ret_nonce)) +} + +/// Wallet backend. All functions here expect that the wallet instance +/// has instantiated itself or stored whatever credentials it needs. +pub struct WalletBackend +where + C: NodeClient, + K: Keychain, +{ + db: Store, + data_file_dir: String, + /// Keychain + pub keychain: Option, + /// Check value for XORed keychain seed + pub master_checksum: Box>, + /// Parent path to use by default for output operations + parent_key_id: Identifier, + /// wallet to node client + w2n_client: C, +} + +impl WalletBackend +where + C: NodeClient, + K: Keychain, +{ + /// Create new wallet backend. + pub fn new(data_file_dir: &str, n_client: C) -> Result { + let db_path = Path::new(data_file_dir).join(DB_DIR); + fs::create_dir_all(&db_path)?; + + let stored_tx_path = Path::new(data_file_dir).join(TX_SAVE_DIR); + fs::create_dir_all(&stored_tx_path)?; + + let store = Store::new( + db_path.to_str().unwrap(), + None, + Some(DB_DIR), + DB_PREFIXES.to_vec(), + None, + 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: WalletBackend::::default_path(), + }; + + { + let mut batch = store.batch()?; + batch.put_ser( + Some(ACCOUNT_PATH_MAPPING_PREFIX), + default_account.label.as_bytes(), + &default_account, + )?; + batch.commit()?; + } + + let res = WalletBackend { + db: store, + data_file_dir: data_file_dir.to_owned(), + keychain: None, + master_checksum: Box::new(None), + parent_key_id: WalletBackend::::default_path(), + w2n_client: n_client, + }; + Ok(res) + } + + /// 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. + pub fn default_path() -> Identifier { + 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::new(data_file_dir).join(DB_DIR); + db_path.exists() + } + + /// Set the keychain, which should already be initialized + /// Optionally return a token value used to XOR the stored + /// key value + pub fn set_keychain( + &mut self, + mut k: K, + mask: bool, + use_test_rng: bool, + ) -> Result, Error> { + // store hash of master key, so it can be verified later after unmasking + let root_key = k.derive_key(0, &K::root_key_id(), SwitchCommitmentType::Regular)?; + let mut hasher = Blake2b::new(SECRET_KEY_SIZE); + hasher.update(&root_key.0[..]); + self.master_checksum = Box::new(Some(hasher.finalize())); + + let mask_value = { + match mask { + true => { + // Random value that must be XORed against the stored wallet seed + // before it is used + let mask_value = match use_test_rng { + true => { + let mut test_rng = StepRng::new(1_234_567_890_u64, 1); + SecretKey::new(&k.secp(), &mut test_rng) + } + false => 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 by removing stored keychain. + pub fn close(&mut self) -> Result<(), Error> { + self.keychain = None; + Ok(()) + } + + /// Return the keychain being used, cloned with XORed token value + /// for temporary use + /// Can optionally take a mask value + pub fn keychain(&self, mask: Option<&SecretKey>) -> Result { + match self.keychain.as_ref() { + Some(k) => { + let mut k_masked = k.clone(); + if let Some(m) = mask { + k_masked.mask_master_key(m)?; + } + // Check if master seed is what is expected (especially if it's been xored) + let root_key = + k_masked.derive_key(0, &K::root_key_id(), SwitchCommitmentType::Regular)?; + let mut hasher = Blake2b::new(SECRET_KEY_SIZE); + hasher.update(&root_key.0[..]); + if *self.master_checksum != Some(hasher.finalize()) { + error!("Supplied keychain mask is invalid"); + return Err(Error::InvalidKeychainMask); + } + Ok(k_masked) + } + None => Err(Error::KeychainDoesntExist), + } + } + + /// Return the client being used to communicate with the node. + pub fn w2n_client(&mut self) -> &mut C { + &mut self.w2n_client + } + + /// Return the version of the commit for caching if allowed. + pub fn calc_commit_for_cache( + &mut self, + keychain_mask: Option<&SecretKey>, + amount: u64, + id: &Identifier, + ) -> Result, Error> { + //TODO: Check if this is really necessary, it's the only thing + //preventing removing the need for config in the wallet backend + /*if self.config.no_commit_cache == Some(true) { + Ok(None) + } else {*/ + Ok(Some( + self.keychain(keychain_mask)? + .commit(amount, &id, SwitchCommitmentType::Regular)? + .0 + .to_vec() + .to_hex(), // TODO: proper support for different switch commitment schemes + )) + /*}*/ + } + + /// Set parent key id by stored account name. + pub 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)) + } + } + + /// The BIP32 path of the parent path to use for all output-related + /// functions, essentially 'accounts' within a wallet. + pub fn set_parent_key_id(&mut self, id: Identifier) { + self.parent_key_id = id; + } + + /// Get the parent path. + pub fn parent_key_id(&mut self) -> Identifier { + self.parent_key_id.clone() + } + + /// Get output data by id. + pub fn get(&self, id: &Identifier, mmr_index: &Option) -> Result { + let key = match mmr_index { + Some(i) => to_key_u64(id.to_bytes(), *i), + None => id.to_bytes().to_vec(), + }; + option_to_not_found(self.db.get_ser(Some(OUTPUT_PREFIX), &key, None), || { + format!("Key Id: {}", id) + }) + .map_err(|e| e.into()) + } + + /// Iterate over all output data stored by the backend. + pub fn iter(&self) -> Result, Error> { + let protocol_version = self.db.protocol_version(); + let prefix_iter = self.db.iter(Some(OUTPUT_PREFIX), move |_, mut v| { + ser::deserialize( + &mut v, + protocol_version, + ser::DeserializationMode::default(), + ) + .map_err(From::from) + }); + let items: Vec = prefix_iter?.collect::, _>>()?; + Ok(items.into_iter()) + } + + /// Get an (Optional) tx log entry by parent key id and log id. Keyed to match + /// save_tx_log_entry (parent_id + numeric log id), unlike the unused uuid form. + pub fn get_tx_log_entry( + &self, + parent_id: Identifier, + log_id: u32, + ) -> Result, Error> { + let tx_log_key = to_key_u64(parent_id.to_bytes(), log_id as u64); + self.db + .get_ser(Some(TX_LOG_ENTRY_PREFIX), &tx_log_key, None) + .map_err(|e| e.into()) + } + + /// Iterate over all tx log data stored by the backend. + pub fn tx_log_iter( + &self, + ) -> Result>, Error> { + let protocol_version = self.db.protocol_version(); + self.db + .iter(Some(TX_LOG_ENTRY_PREFIX), move |_, mut v| { + ser::deserialize( + &mut v, + protocol_version, + ser::DeserializationMode::default(), + ) + .map_err(From::from) + }) + .map_err(From::from) + } + + /// Retrieve the private context associated with a given slate id. + pub fn get_private_context( + &mut self, + keychain_mask: Option<&SecretKey>, + slate_id: &[u8], + ) -> Result { + let ctx_key = to_key_u64(slate_id, 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(Some(PRIVATE_TX_CONTEXT_PREFIX), &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) + } + + /// Iterate over all stored account paths. + pub fn acct_path_iter(&self) -> Result, Error> { + let protocol_version = self.db.protocol_version(); + let prefix_iter = self + .db + .iter(Some(ACCOUNT_PATH_MAPPING_PREFIX), move |_, mut v| { + ser::deserialize( + &mut v, + protocol_version, + ser::DeserializationMode::default(), + ) + .map_err(From::from) + }); + let items: Vec = prefix_iter?.collect::, _>>()?; + Ok(items.into_iter()) + } + + /// Gets an account path for a given label. + pub fn get_acct_path(&self, label: String) -> Result, Error> { + self.db + .get_ser(Some(ACCOUNT_PATH_MAPPING_PREFIX), label.as_bytes(), None) + .map_err(|e| e.into()) + } + + /// Stores a transaction. + pub fn store_tx(&self, uuid: &str, tx: &Transaction) -> Result<(), Error> { + let filename = format!("{}.grintx", uuid); + let 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(()) + } + + /// Retrieves a stored transaction. + //TODO: Store content of .grintx file at TxLogEntry? + pub fn get_stored_tx(&self, uuid: &str) -> Result, Error> { + let filename = format!("{}.grintx", uuid); + let 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 = grin_util::from_hex(&content).unwrap(); + Ok(Some( + ser::deserialize( + &mut &tx_bin[..], + ser::ProtocolVersion(1), + ser::DeserializationMode::default(), + ) + .unwrap(), + )) + } + + /// Create a new write batch to update or remove output data. + pub fn batch( + &mut self, + keychain_mask: Option<&SecretKey>, + ) -> Result, Error> { + Ok(WalletBatch { + db: self.db.batch()?, + keychain: Some(self.keychain(keychain_mask)?), + }) + } + + /// Batch for use when keychain isn't available or required. + pub fn batch_no_mask(&mut self) -> Result, Error> { + Ok(WalletBatch { + db: self.db.batch()?, + keychain: None, + }) + } + + /// Return the current child index. + pub fn current_child_index(&mut self, parent_key_id: &Identifier) -> Result { + let index = { + let batch = self.db.batch()?; + batch + .get_ser(Some(DERIV_PREFIX), &parent_key_id.to_bytes(), None)? + .unwrap_or_else(|| 0) + }; + Ok(index) + } + + /// Next child ID for the given parent key, when we want to create a new output. + /// Used by contracts so outputs derive under the contract's account rather than + /// whatever account happens to be active. + pub fn next_child_for( + &mut self, + parent_key_id: &Identifier, + keychain_mask: Option<&SecretKey>, + ) -> Result { + let mut deriv_idx = { + let batch = self.db.batch()?; + batch + .get_ser(Some(DERIV_PREFIX), &parent_key_id.to_bytes(), None)? + .unwrap_or_else(|| 0) + }; + let mut return_path = 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)) + } + + /// Next child ID when we want to create a new output, based on current parent. + pub fn next_child(&mut self, keychain_mask: Option<&SecretKey>) -> Result { + let parent_key_id = self.parent_key_id.clone(); + self.next_child_for(&parent_key_id, keychain_mask) + } + + /// Last verified height of outputs directly descending from the given parent key. + pub fn last_confirmed_height(&mut self) -> Result { + let batch = self.db.batch()?; + let last_confirmed_height = batch + .get_ser( + Some(CONFIRMED_HEIGHT_PREFIX), + &self.parent_key_id.to_bytes(), + None, + )? + .unwrap_or_else(|| 0); + Ok(last_confirmed_height) + } + + /// Last block scanned during scan or restore. + pub fn last_scanned_block(&mut self) -> Result { + let batch = self.db.batch()?; + let last_scanned_block = batch + .get_ser(Some(LAST_SCANNED_BLOCK), LAST_SCANNED_KEY.as_bytes(), None)? + .unwrap_or_else(|| ScannedBlockInfo { + height: 0, + hash: "".to_owned(), + start_pmmr_index: 0, + last_pmmr_index: 0, + }); + Ok(last_scanned_block) + } + + /// Flag whether the wallet needs a full UTXO scan on next update attempt. + pub fn init_status(&mut self) -> Result { + let batch = self.db.batch()?; + let status = batch + .get_ser( + Some(WALLET_INIT_STATUS), + WALLET_INIT_STATUS_KEY.as_bytes(), + None, + )? + .unwrap_or_else(|| WalletInitStatus::InitComplete); + Ok(status) + } +} + +/// An atomic batch in which all changes can be committed all at once or +/// discarded on error. +pub struct WalletBatch<'a, K> +where + K: Keychain, +{ + db: grin_store::Batch<'a>, + /// Keychain + keychain: Option, +} + +#[allow(missing_docs)] +impl<'a, K> WalletBatch<'a, K> +where + K: Keychain, +{ + /// Return the keychain being used. + pub fn keychain(&mut self) -> &mut K { + self.keychain.as_mut().unwrap() + } + + /// Add or update data about an output to the backend. + pub fn save(&mut self, out: OutputData) -> Result<(), Error> { + let key = match out.mmr_index { + Some(i) => to_key_u64(out.key_id.to_bytes(), i), + None => out.key_id.to_bytes().to_vec(), + }; + self.db.put_ser(Some(OUTPUT_PREFIX), &key, &out)?; + Ok(()) + } + + /// Gets output data by id + pub fn get(&self, id: &Identifier, mmr_index: &Option) -> Result { + let key = match mmr_index { + Some(i) => to_key_u64(id.to_bytes(), *i), + None => id.to_bytes().to_vec(), + }; + option_to_not_found(self.db.get_ser(Some(OUTPUT_PREFIX), &key, None), || { + format!("Key ID: {}", id) + }) + .map_err(|e| e.into()) + } + + /// Iterate over all output data stored by the backend. + pub fn iter(&'a self) -> Result + 'a, Error> { + let protocol_version = self.db.protocol_version(); + let prefix_iter = self.db.iter(Some(OUTPUT_PREFIX), move |_, mut v| { + ser::deserialize( + &mut v, + protocol_version, + ser::DeserializationMode::default(), + ) + .map_err(From::from) + }); + let items: Vec = prefix_iter?.collect::, _>>()?; + Ok(items.into_iter()) + } + + /// Delete data about an output from the backend. + pub fn delete(&mut self, id: &Identifier, mmr_index: &Option) -> Result<(), Error> { + // Delete the output data. + let key = match mmr_index { + Some(i) => to_key_u64(id.to_bytes(), *i), + None => id.to_bytes().to_vec(), + }; + self.db.delete(Some(OUTPUT_PREFIX), &key)?; + Ok(()) + } + + /// Save last stored child index of a given parent. + pub fn save_child_index(&mut self, parent_id: &Identifier, child_n: u32) -> Result<(), Error> { + self.db + .put_ser(Some(DERIV_PREFIX), &parent_id.to_bytes(), &child_n)?; + Ok(()) + } + + /// Save last confirmed height of outputs for a given parent. + pub fn save_last_confirmed_height( + &mut self, + parent_key_id: &Identifier, + height: u64, + ) -> Result<(), Error> { + self.db.put_ser( + Some(CONFIRMED_HEIGHT_PREFIX), + &parent_key_id.to_bytes(), + &height, + )?; + Ok(()) + } + + /// Save the last PMMR index that was scanned via a scan operation. + pub fn save_last_scanned_block(&mut self, block_info: ScannedBlockInfo) -> Result<(), Error> { + self.db.put_ser( + Some(LAST_SCANNED_BLOCK), + LAST_SCANNED_KEY.as_bytes(), + &block_info, + )?; + Ok(()) + } + + /// Save flag indicating whether wallet needs a full UTXO scan. + pub fn save_init_status(&mut self, value: WalletInitStatus) -> Result<(), Error> { + self.db.put_ser( + Some(WALLET_INIT_STATUS), + WALLET_INIT_STATUS_KEY.as_bytes(), + &value, + )?; + Ok(()) + } + + /// Get next transaction log entry for the parent. + pub fn next_tx_log_id(&mut self, parent_key_id: &Identifier) -> Result { + let last_tx_log_id = self + .db + .get_ser(Some(TX_LOG_ID_PREFIX), &parent_key_id.to_bytes(), None)? + .unwrap_or_else(|| 0); + self.db.put_ser( + Some(TX_LOG_ID_PREFIX), + &parent_key_id.to_bytes(), + &(last_tx_log_id + 1), + )?; + Ok(last_tx_log_id) + } + + /// Iterate over transactions data stored by the backend. + pub fn tx_log_iter( + &'a self, + ) -> Result> + 'a, Error> { + let protocol_version = self.db.protocol_version(); + self.db + .iter(Some(TX_LOG_ENTRY_PREFIX), move |_, mut v| { + ser::deserialize( + &mut v, + protocol_version, + ser::DeserializationMode::default(), + ) + .map_err(From::from) + }) + .map_err(From::from) + } + + /// Save a transaction log entry. + pub fn save_tx_log_entry( + &mut self, + tx_in: TxLogEntry, + parent_id: &Identifier, + ) -> Result<(), Error> { + let tx_log_key = to_key_u64(parent_id.to_bytes(), tx_in.id as u64); + self.db + .put_ser(Some(TX_LOG_ENTRY_PREFIX), &tx_log_key, &tx_in)?; + Ok(()) + } + + /// Delete a transaction log entry. + pub fn delete_tx_log_entry(&mut self, tx_id: u32, parent_id: &Identifier) -> Result<(), Error> { + let tx_log_key = to_key_u64(parent_id.to_bytes(), tx_id as u64); + self.db.delete(Some(TX_LOG_ENTRY_PREFIX), &tx_log_key)?; + Ok(()) + } + + /// Save an account label -> path mapping. + pub fn save_acct_path(&mut self, mapping: AcctPathMapping) -> Result<(), Error> { + self.db.put_ser( + Some(ACCOUNT_PATH_MAPPING_PREFIX), + mapping.label.as_bytes(), + &mapping, + )?; + Ok(()) + } + + /// Iterate over account names stored in backend. + pub fn acct_path_iter(&'a self) -> Result + 'a, Error> { + let protocol_version = self.db.protocol_version(); + let prefix_iter = self + .db + .iter(Some(ACCOUNT_PATH_MAPPING_PREFIX), move |_, mut v| { + ser::deserialize( + &mut v, + protocol_version, + ser::DeserializationMode::default(), + ) + .map_err(From::from) + }); + let items: Vec = prefix_iter?.collect::, _>>()?; + Ok(items.into_iter()) + } + + /// Save an output as locked in the backend. + pub fn lock_output(&mut self, out: &mut OutputData) -> Result<(), Error> { + out.lock(); + self.save(out.clone()) + } + + /// Save the private context associated with a slate id. + pub fn save_private_context(&mut self, slate_id: &[u8], ctx: &Context) -> Result<(), Error> { + let ctx_key = to_key_u64(slate_id, 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 + .put_ser(Some(PRIVATE_TX_CONTEXT_PREFIX), &ctx_key, &s_ctx)?; + Ok(()) + } + + /// Delete the private context associated with the slate id. + pub fn delete_private_context(&mut self, slate_id: &[u8]) -> Result<(), Error> { + let ctx_key = to_key_u64(slate_id, 0); + self.db + .delete(Some(PRIVATE_TX_CONTEXT_PREFIX), &ctx_key) + .map_err(|e| e.into()) + } + + /// Write the wallet data to backend file. + pub fn commit(self) -> Result<(), Error> { + self.db.commit()?; + Ok(()) + } +} + +/// Build a db key from a byte vector identifier and numeric identifier +fn to_key_u64>(k: K, val: u64) -> Vec { + let mut res = k.as_ref().to_vec(); + res.write_u64::(val).unwrap(); + res +} diff --git a/libwallet/src/contract/actions/README.md b/libwallet/src/contract/actions/README.md new file mode 100644 index 000000000..a448ed9ad --- /dev/null +++ b/libwallet/src/contract/actions/README.md @@ -0,0 +1,101 @@ +# Contract actions + +### API endpoints + +Four owner API methods, each with a matching `grin-wallet contract` subcommand: `new`, +`sign`, `view` and `revoke`. There is no `setup` endpoint; setup is a step rather than a +call, run by `new` for the initiator and by `sign` for the signer if it hasn't happened +yet. So a two party contract is `new`, `sign`, `sign`, and a self-spend is `new`, `sign`. + +`view` summarises a slate: participants, signatures so far, the net change it suggests or +the one we already agreed to, and whether the transaction has confirmed. `revoke` +double-spends an input of a contract we signed but that hasn't confirmed, and is +idempotent, so an interrupted revoke can be run again. + +### Rust implementation + +Every contract action on a slate is divided in 3 parts: +1. compute the new state +2. save the new state +3. return slate + +Putting this into code, it looks like the following: +```rust +// Compute the new state (both of the Slate and the Context) +let (slate, context) = compute(slate, args); +// Atomically commit the new state +contract_utils::save_step(slate, context, ...); +// Return the newly produced slate +return slate; +``` + +We only allow contribution of custom inputs/outputs when we're doing the setup phase. Once the setup phase is done, +we no longer allow any customization of inputs. This means that the customization can only happen at contract setup phase which is the first time we see the contract. Additionally, if we customize output selection, we immediately pick the inputs/outputs which means it's an early lock. These are however not added to the slate until we reach the 'sign' phase of the contract. Counterparties don't need to see our inputs/outputs before that. This means we always add inputs/outputs only when we have to and never before. + +Ideally we'd also separate side effects out of these functions e.g. computing the current_height +or refreshing the outputs with updater::refresh_outputs(...). The current_height could be +communicated through a &ChainState parameter which would collect these values before the call. +Additionally, we could fetch the existing Context before the call to avoid doing db fetch. +Separating side effects until the 'save_step' part would make these functions much easier to test. + +#### Not supported + + - More than two parties. `num_participants` is validated to 1 (self-spend) or 2. + - Payment proof types other than invoice (type 1). A variant that doesn't depend on your + position in the contract is RFC work. + - `--send` and `--receive` take a human readable amount, as `send` and `invoice` do. + Everything reaching the API, including `--make-outputs`, is nanogrin. + - Writing the signed transaction in the same commit as the wallet state. `store_tx` + writes a file outside LMDB, as it does for every transaction in the wallet; + `contract::utils::save_step` describes the window and how to recover. + +#### TODOs + + - `next_child_for` reads the derivation index and writes it back in two batches, so + concurrent steps can be handed the same child key. `next_child` has the same shape. + - Contract input selection requires a single confirmation. + - `--use-inputs` names the inputs to spend but doesn't lock them, unlike `--add-outputs`. + - Check the commitments we contributed are the ones that come back, so a counterparty + can't return a slate carrying inputs or outputs of ours that we didn't put there. + - Handle the coinbase flows. Coinbase inputs are built into the slate, but nothing else + is exercised. + - For a payjoin, `--use-inputs any` picks an input for us; naming the commitment would be + clearer, for the API as much as the CLI. + - `new` has no `--no-setup`. + - Separate side effects out from the computation, as described above. + - Decide whether `contract_accounts_switch.rs` is a legitimate scenario, or whether + signing under a different account than setup should error. + +#### Tests + + - signing the same slate twice, which `verify_not_signed` guards + - zero-value outputs, and `--make-outputs` beyond what is available + - a receiver with no spendable input, through lack of funds or of confirmations + - contracts spending more than one input + - slate contents at each step, not only the end state + - the foreign API entry points for `new` and `sign` + - fee contribution across one and two parties + +#### save_step + + // TODO: + // - is_signed should be derived from the slate + // - Consider taking ownership of Context here. It should not be used after this is called. + +### Side-effects + +#### Setup + // Side-effects: + // - height = w.w2n_client().get_chain_tip()?.0; + // - maybe_context = w.get_private_context(keychain_mask, sl.id.as_bytes()) + // - create_contract_ctx -> updater::refresh_outputs(wallet, keychain_mask, parent_key_id, false)?; + // - add_outputs -> let current_height = w.w2n_client().get_chain_tip()?.0; + // - add_outputs -> contribute_output -> let key_id = keys::next_available_key(wallet, keychain_mask).unwrap(); + // - TODO: would we need to compute keys::next_available_key for as many outputs as we plan to contribute and pass + // them as a param to keep this without side effects? + +#### Sign + // Side-effects: + // - contract_utils::check_already_signed -> tx_log_iter + // - contract_utils::get_net_change -> context and net_change + // - everything from 'setup' diff --git a/impls/src/backends/mod.rs b/libwallet/src/contract/actions/mod.rs similarity index 66% rename from impls/src/backends/mod.rs rename to libwallet/src/contract/actions/mod.rs index 56b5df6e2..5681e2ad0 100644 --- a/impls/src/backends/mod.rs +++ b/libwallet/src/contract/actions/mod.rs @@ -1,4 +1,4 @@ -// Copyright 2021 The Grin Developers +// 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. @@ -12,6 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -mod lmdb; +//! This module contains contract related actions. -pub use self::lmdb::{wallet_db_exists, LMDBBackend}; +mod new; +mod revoke; +mod setup; +mod sign; +mod view; + +pub use self::new::new; +pub use self::revoke::revoke; +pub use self::setup::setup; +pub use self::sign::sign; +pub use self::view::view; diff --git a/libwallet/src/contract/actions/new.rs b/libwallet/src/contract/actions/new.rs new file mode 100644 index 000000000..897cf2560 --- /dev/null +++ b/libwallet/src/contract/actions/new.rs @@ -0,0 +1,87 @@ +// 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. + +//! Implementation of contract new + +use crate::backend::WalletBackend; +use crate::contract; +use crate::contract::actions::setup; +use crate::contract::types::ContractSetupArgsAPI; +use crate::error::Error; +use crate::grin_keychain::Keychain; +use crate::grin_util::secp::key::SecretKey; +use crate::slate::Slate; +use crate::types::{Context, NodeClient}; +use uuid::Uuid; + +/// Create a new contract with initial setup done by the initiator. `slate_id`, when +/// provided, fixes the slate id (rather than a random one) so the caller can make a +/// retried creation idempotent: get_or_create reuses the existing context for that id. +pub fn new( + w: &mut WalletBackend, + keychain_mask: Option<&SecretKey>, + setup_args: &ContractSetupArgsAPI, + slate_id: Option, +) -> Result +where + C: NodeClient, + K: Keychain, +{ + // Compute state for 'new' + let (slate, mut context) = compute(w, keychain_mask, setup_args, slate_id)?; + + // Atomically commit state + contract::utils::save_step( + w, + keychain_mask, + &slate, + &mut context, + setup_args.add_outputs, + false, + )?; + + Ok(slate) +} + +/// Compute logic for new +pub fn compute( + w: &mut WalletBackend, + keychain_mask: Option<&SecretKey>, + setup_args: &ContractSetupArgsAPI, + slate_id: Option, +) -> Result<(Slate, Context), Error> +where + C: NodeClient, + K: Keychain, +{ + let net_change = setup_args.net_change.ok_or_else(|| { + Error::GenericError("Contract requires a net change (--send or --receive)".to_string()) + })?; + debug!("contract::new => net_change passed: {}", net_change); + + // Initialize a new contract (if net_change is positive, I'm the receiver meaning this is invoice flow) + let num_participants = setup_args.num_participants; + let mut slate = Slate::blank(num_participants, net_change > 0); + // Use a caller-supplied id when given, so a retried creation reuses the same context. + if let Some(id) = slate_id { + slate.id = id; + } + // We set slate.amount to contain the _positive_ net_change for the other party so they can derive expectations. + // unsigned_abs avoids the i64::MIN overflow panic of abs(). + slate.amount = net_change.unsigned_abs(); + debug!("contract::new => slate amount: {}", slate.amount); + + // Perform setup for the slate + setup::compute(w, keychain_mask, &mut slate, setup_args) +} diff --git a/libwallet/src/contract/actions/revoke.rs b/libwallet/src/contract/actions/revoke.rs new file mode 100644 index 000000000..52268a287 --- /dev/null +++ b/libwallet/src/contract/actions/revoke.rs @@ -0,0 +1,181 @@ +// 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. + +//! Implementation of contract revoke +use crate::backend::WalletBackend; +use crate::blake2::blake2b::blake2b; +use crate::contract::types::{ContractRevokeArgsAPI, ContractSetupArgsAPI, OutputSelectionArgs}; +use crate::contract::{new, sign}; +use crate::error::Error; +use crate::grin_keychain::Keychain; +use crate::grin_util::secp::key::SecretKey; +use crate::internal::tx; +use crate::slate::Slate; +use crate::types::{NodeClient, OutputData, OutputStatus, TxLogEntryType}; +use uuid::Uuid; + +/// Deterministic slate id for the self-spend that revokes a given contract slate. +/// Derived from the revoked slate id so a revoke interrupted between creating and +/// signing the self-spend resumes by reusing the same context (get_or_create), rather +/// than orphaning a fresh self-spend on each retry. +fn self_spend_slate_id(revoked_slate_id: Uuid) -> Uuid { + let hash = blake2b(16, b"grin-contract-revoke", revoked_slate_id.as_bytes()); + let mut bytes = [0u8; 16]; + bytes.copy_from_slice(hash.as_bytes()); + Uuid::from_bytes(bytes) +} + +/// Contract revocation is done by double-spending the input +pub fn revoke( + w: &mut WalletBackend, + keychain_mask: Option<&SecretKey>, + args: &ContractRevokeArgsAPI, +) -> Result, Error> +where + C: NodeClient, + K: Keychain, +{ + // Revoke double-spends an input we contributed to tx_id. cancel_tx, the self-spend + // new() and sign() each commit separately, so these steps are NOT one atomic unit. + // revoke() is therefore written to be safely re-invocable: a crash between cancelling + // and finishing the self-spend leaves our inputs Unspent but still tagged with tx_id, + // and a second call resumes from there. cancel_tx must run first because a Locked + // output is ineligible for selection (OutputData::eligible_to_spend), so the self-spend + // cannot be built until the input is unlocked. + // FUTURE: we may want to boost fees if we notice the original tx in the mempool. + let tx_id = args.tx_id; + + // Inputs we contributed to tx_id that are still recoverable. Locked => the original tx + // is still active; Unspent => a previous revoke cancelled it but the self-spend did not + // finish. Once the self-spend completes these reference the self-spend's tx id instead + // and are no longer matched here, which makes a repeat revoke a no-op. + let my_contributed_inputs = w + .batch(keychain_mask)? + .iter()? + .filter(|out| { + out.tx_log_entry == Some(tx_id) + && (out.status == OutputStatus::Locked || out.status == OutputStatus::Unspent) + }) + .collect::>(); + + // Determine the account that owns the inputs so the cancel and the self-spend target it + // rather than whatever account happens to be active. + let parent_key_id = match my_contributed_inputs.first() { + Some(out) => out.root_key_id.clone(), + None => w.parent_key_id(), + }; + + // Cancel the original tx only if it is still in a cancellable state. On a resumed revoke + // it is already a *Cancelled type (and the inputs are Unspent), so we skip straight to + // re-spending them. + let revoked = w.get_tx_log_entry(parent_key_id.clone(), tx_id)?; + let revoked_slate_id = revoked.as_ref().and_then(|e| e.tx_slate_id); + let needs_cancel = match revoked.as_ref() { + Some(e) => matches!( + e.tx_type, + TxLogEntryType::TxSent + | TxLogEntryType::TxReceived + | TxLogEntryType::TxReverted + | TxLogEntryType::TxSelfSpend + ), + None => false, + }; + if needs_cancel { + // 1. Unlock the inputs by cancelling the original tx. + tx::cancel_tx(&mut *w, keychain_mask, &parent_key_id, Some(tx_id), None)?; + // Drop the canceled slate's private context if one still exists (signing already + // deletes it). + if let Some(slate_id) = revoked_slate_id { + if w.get_private_context(keychain_mask, slate_id.as_bytes()) + .is_ok() + { + let mut batch = w.batch(keychain_mask)?; + batch.delete_private_context(slate_id.as_bytes())?; + batch.commit()?; + } + } + } + + // Nothing of ours to double-spend: we contributed no inputs, or a prior revoke already + // re-spent them. + if my_contributed_inputs.is_empty() { + return Ok(None); + } + let input_commit = my_contributed_inputs[0] + .commit + .as_ref() + .ok_or_else(|| Error::GenericError("Locked input has no cached commitment".to_string()))?; + // Account label for the self-spend, so recovered funds return to the inputs' account. + let src_acct_name = w + .acct_path_iter()? + .find(|m| m.path == parent_key_id) + .map(|m| m.label); + // Deterministic self-spend slate id (when we know the revoked slate) so a crash + // between new() and sign() is resumed by reusing the same context rather than + // orphaning a fresh self-spend on the retry. + let self_spend_id = revoked_slate_id.map(self_spend_slate_id); + // 2. Create a 1-1 self-spend transaction using this input + let ct_slate = new( + w, + keychain_mask, + &ContractSetupArgsAPI { + src_acct_name: src_acct_name.clone(), + net_change: Some(0), // self-spend + num_participants: 1, + add_outputs: false, + selection_args: OutputSelectionArgs { + use_inputs: Some(String::from(input_commit)), + ..Default::default() + }, + proof_args: Default::default(), + }, + self_spend_id, + )?; + let finished_slate = sign( + w, + keychain_mask, + &ct_slate, + &ContractSetupArgsAPI { + src_acct_name, + net_change: None, // we already have it in the context as 0 now + num_participants: 1, + add_outputs: false, + selection_args: OutputSelectionArgs { + use_inputs: Some(String::from(input_commit)), + ..Default::default() + }, + proof_args: Default::default(), + }, + )?; + + Ok(Some(finished_slate)) +} + +#[cfg(test)] +mod tests { + use super::self_spend_slate_id; + use uuid::Uuid; + + #[test] + fn self_spend_id_is_deterministic_and_distinct() { + let revoked = Uuid::parse_str("936da01f-9abd-4d9d-80c7-02af85c822a8").unwrap(); + // Same revoked slate -> same self-spend id, so a retried revoke reuses the context. + assert_eq!(self_spend_slate_id(revoked), self_spend_slate_id(revoked)); + // Distinct from the revoked id (the self-spend is a different tx). + assert_ne!(self_spend_slate_id(revoked), revoked); + // Different revoked slates -> different self-spend ids. + let other = Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap(); + assert_ne!(self_spend_slate_id(revoked), self_spend_slate_id(other)); + } +} diff --git a/libwallet/src/contract/actions/setup.rs b/libwallet/src/contract/actions/setup.rs new file mode 100644 index 000000000..d744a1691 --- /dev/null +++ b/libwallet/src/contract/actions/setup.rs @@ -0,0 +1,92 @@ +// 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. + +//! Implementation of contract setup + +use crate::api_impl::owner::check_ttl; +use crate::backend::WalletBackend; +use crate::contract; +use crate::contract::types::ContractSetupArgsAPI; +use crate::error::Error; +use crate::grin_keychain::Keychain; +use crate::grin_util::secp::key::SecretKey; +use crate::slate::Slate; +use crate::types::{Context, NodeClient}; + +/// Perform a contract setup +pub fn setup( + w: &mut WalletBackend, + keychain_mask: Option<&SecretKey>, + slate: &Slate, + setup_args: &ContractSetupArgsAPI, +) -> Result +where + C: NodeClient, + K: Keychain, +{ + // Compute state for 'setup' + let (slate, mut context) = compute(w, keychain_mask, slate, setup_args)?; + + // Atomically commit state + contract::utils::save_step( + w, + keychain_mask, + &slate, + &mut context, + setup_args.add_outputs, + false, + )?; + + Ok(slate) +} + +/// Compute logic for setup +pub fn compute( + w: &mut WalletBackend, + keychain_mask: Option<&SecretKey>, + slate: &Slate, + setup_args: &ContractSetupArgsAPI, +) -> Result<(Slate, Context), Error> +where + C: NodeClient, + K: Keychain, +{ + let mut sl = slate.clone(); + check_ttl(w, &sl)?; + + // Get or create a transaction Context and verify consistency of setup arguments + let mut context = contract::context::get_or_create(w, keychain_mask, &mut sl, setup_args)?; + contract::utils::verify_setup_args_consistency( + &context.setup_args.as_ref().unwrap(), + &setup_args, + )?; + + // Add keys and payment proof to slate (both are idempotent operations) + contract::slate::add_keys(&mut sl, &w.keychain(keychain_mask)?, &mut context)?; + contract::slate::add_payment_proof( + w, + &mut sl, + keychain_mask, + &mut context, + &setup_args.net_change, + &setup_args.proof_args, + )?; // noop for the sender + + // Add inputs/outputs to the Context if needed. No locking is done here. This happens at save_step. + if setup_args.add_outputs { + contract::context::add_outputs(&mut *w, keychain_mask, &mut context)?; + } + + Ok((sl, context)) +} diff --git a/libwallet/src/contract/actions/sign.rs b/libwallet/src/contract/actions/sign.rs new file mode 100644 index 000000000..5d9065450 --- /dev/null +++ b/libwallet/src/contract/actions/sign.rs @@ -0,0 +1,96 @@ +// 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. + +//! Implementation of contract sign + +use crate::backend::WalletBackend; +use crate::contract; +use crate::contract::actions::setup; +use crate::contract::types::ContractSetupArgsAPI; +use crate::error::Error; +use crate::grin_keychain::Keychain; +use crate::grin_util::secp::key::SecretKey; +use crate::slate::Slate; +use crate::types::{Context, NodeClient}; + +/// Sign a contract +pub fn sign( + w: &mut WalletBackend, + keychain_mask: Option<&SecretKey>, + slate: &Slate, + setup_args: &ContractSetupArgsAPI, +) -> Result +where + C: NodeClient, + K: Keychain, +{ + // Compute if we will add outputs at this step. Only a missing context means we + // still need to add them; propagate any other error. + let will_add_outputs = match w.get_private_context(keychain_mask, slate.id.as_bytes()) { + Ok(ctx) => ctx.get_inputs().len() + ctx.get_outputs().len() == 0, + Err(Error::NotFoundErr(_)) => true, + Err(e) => return Err(e), + }; + // Compute state for 'sign' + let (sl, mut context) = compute(w, keychain_mask, slate, setup_args)?; + + // Atomically commit state + contract::utils::save_step(w, keychain_mask, &sl, &mut context, will_add_outputs, true)?; + + Ok(sl) +} + +/// Compute logic for sign +pub fn compute( + w: &mut WalletBackend, + keychain_mask: Option<&SecretKey>, + slate: &Slate, + setup_args: &ContractSetupArgsAPI, +) -> Result<(Slate, Context), Error> +where + C: NodeClient, + K: Keychain, +{ + let mut sl = slate.clone(); + contract::utils::verify_not_signed(w, sl.id)?; + + // Ensure net_change has been provided + let expected_net_change = + contract::utils::get_net_change(w, keychain_mask, &sl.id, setup_args.net_change)?; + + // Define the values that must be provided in the setup phase at the sign step + let mut setup_args = setup_args.clone(); + setup_args.net_change = Some(expected_net_change); + setup_args.num_participants = sl.num_participants; + setup_args.add_outputs = true; // we add outputs to the Context in case we haven't done that yet + + // Ensure Setup phase is done and that inputs/outputs have been added to the Context + let (mut sl, mut context) = setup::compute(w, keychain_mask, &mut sl, &setup_args)?; + // Add outputs to the slate, verify the payment proof and sign the slate + contract::slate::add_outputs(w, keychain_mask, &mut sl, &context)?; + if let Some(ref p) = sl.payment_proof { + contract::slate::verify_payment_proof(&sl, expected_net_change, &p.receiver_address)?; + // noop for the receiver + } + + contract::slate::sign(w, keychain_mask, &mut sl, &mut context)?; + contract::slate::transition_state(&mut sl)?; + + // If we have all the partial signatures, finalize the tx + if contract::slate::can_finalize(&sl) { + contract::slate::finalize(w, keychain_mask, &mut sl)?; + } + + Ok((sl, context)) +} diff --git a/libwallet/src/contract/actions/view.rs b/libwallet/src/contract/actions/view.rs new file mode 100644 index 000000000..72908f743 --- /dev/null +++ b/libwallet/src/contract/actions/view.rs @@ -0,0 +1,97 @@ +// 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. + +//! Implementation of contract view + +use crate::backend::WalletBackend; +use crate::contract::types::ContractView; +use crate::error::Error; +use crate::grin_keychain::Keychain; +use crate::grin_util::secp::key::SecretKey; +use crate::internal::updater; +use crate::slate::{Slate, SlateState}; +use crate::types::NodeClient; + +/// View contract +pub fn view( + w: &mut WalletBackend, + keychain_mask: Option<&SecretKey>, + slate: &mut Slate, + _encrypted_for: &str, +) -> Result +where + C: NodeClient, + K: Keychain, +{ + // NOTE: This should only be run on slates that we received and were signed for us. + // Otherwise, you can't really predict who the party doing the next step should be. + + // Reject a slate we can't interpret. Standard2/3 and Invoice2/3 are valid mid/late + // flow states (they fall through to suggested_net_change = None below), so only the + // Unknown state is rejected here. + if slate.state == SlateState::Unknown { + return Err(Error::GenericError( + "Cannot view a slate with an Unknown state".to_string(), + )); + } + // Mirror the contract setup bound so a tampered slate can't surface a bogus count. + if slate.num_participants < 1 || slate.num_participants > 2 { + return Err(Error::GenericError(format!( + "Unsupported num_participants: {} (expected 1 or 2)", + slate.num_participants + ))); + } + // Checked conversion so an out-of-range amount can't wrap into a bogus net change. + let suggested_net_change: Option = match slate.state { + SlateState::Invoice1 => Some(i64::try_from(slate.amount).map_err(|_| { + Error::GenericError(format!("Slate amount {} exceeds i64", slate.amount)) + })?), + SlateState::Standard1 => Some(-i64::try_from(slate.amount).map_err(|_| { + Error::GenericError(format!("Slate amount {} exceeds i64", slate.amount)) + })?), + _ => None, + }; + // A contract is executed once the transaction it produced has confirmed. That is + // recorded on our own tx log entry for this slate, so no chain lookup is needed; a + // slate we have never signed simply has no entry and is not executed. + let is_executed = updater::retrieve_txs(w, None, Some(slate.id), None, None, false)? + .iter() + .any(|tx| tx.confirmed); + // Count signatures present (a participant is "complete" once it has a partial sig). + let num_sigs = slate + .participant_data + .clone() + .into_iter() + .filter(|v| v.is_complete()) + .count(); + + // If we have a local context for this slate we've agreed on a net change; surface it. + let agreed_net_change = match w.get_private_context(keychain_mask, slate.id.as_bytes()) { + Ok(ctx) => Some(ctx.get_net_change()?), + Err(Error::NotFoundErr(_)) => None, + Err(e) => return Err(e), + }; + + // TODO: Maybe we can know if the slate was meant for us if it was encrypted for us. + // A possible issue is that one can encrypt the same slate for 10 people. + let ct_view = ContractView { + num_participants: slate.num_participants, + suggested_net_change: suggested_net_change, + agreed_net_change, + num_sigs: num_sigs as u8, + is_executed: is_executed, + ..Default::default() + }; + Ok(ct_view) +} diff --git a/libwallet/src/contract/context.rs b/libwallet/src/contract/context.rs new file mode 100644 index 000000000..d838299c2 --- /dev/null +++ b/libwallet/src/contract/context.rs @@ -0,0 +1,231 @@ +// 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. + +//! Contract functions on the Context + +use crate::backend::WalletBackend; +use crate::contract::selection::prepare_outputs; +use crate::contract::types::ContractSetupArgsAPI; +use crate::contract::utils as contract_utils; +use crate::grin_keychain::{Identifier, Keychain}; +use crate::grin_util::secp::key::SecretKey; +use crate::internal::updater; +use crate::slate::Slate; +use crate::types::{Context, NodeClient}; +use crate::{Error, OutputData}; +use grin_core::core::FeeFields; + +/// Get or create transaction Context for the given slate +pub fn get_or_create( + w: &mut WalletBackend, + keychain_mask: Option<&SecretKey>, + slate: &mut Slate, + setup_args: &ContractSetupArgsAPI, +) -> Result +where + C: NodeClient, + K: Keychain, +{ + debug!("contract::context::get_or_create => called"); + let maybe_context = w.get_private_context(keychain_mask, slate.id.as_bytes()); + + let context = match maybe_context { + // Only create a new context when there genuinely isn't one; propagate other errors. + Err(Error::NotFoundErr(_)) => { + // Get data required for creating a context + let height = w.w2n_client().get_chain_tip()?.0; + let parent_key_id = + contract_utils::parent_key_for(w, setup_args.src_acct_name.as_ref())?; + self::create( + w, + keychain_mask, + slate, + height, + // &args, + setup_args, + &parent_key_id, + false, + )? + } + Err(e) => return Err(e), + Ok(ctx) => ctx, + }; + Ok(context) +} + +/// Creates a context for a contract +fn create( + w: &mut WalletBackend, + keychain_mask: Option<&SecretKey>, + slate: &mut Slate, + current_height: u64, + setup_args: &ContractSetupArgsAPI, + parent_key_id: &Identifier, + use_test_rng: bool, +) -> Result +where + C: NodeClient, + K: Keychain, +{ + debug!("Creating a new contract context"); + // Validate num_participants before any fee math (kernel cost is divided by it, so 0 + // would be a divide-by-zero). This is the common setup path for every party (the + // signer's value comes from the slate), so it also guards against a tampered slate. + // Only 1 (self-spend) and 2 (standard) are implemented today; lift this when + // multi-party contracts are supported. + if setup_args.num_participants < 1 || setup_args.num_participants > 2 { + return Err(Error::GenericError(format!( + "Unsupported num_participants: {} (expected 1 or 2)", + setup_args.num_participants + ))); + } + // sender should always refresh outputs + updater::refresh_outputs(w, keychain_mask, parent_key_id, false)?; + + // Fee contribution estimation + let net_change = setup_args.net_change.ok_or_else(|| { + Error::GenericError("Contract requires a net change (--send or --receive)".to_string()) + })?; + // select inputs to estimate fee cost + let (inputs, _, my_fee) = + prepare_outputs(w, &parent_key_id, current_height, &setup_args, None)?; + // The number of outputs we expect is the number of custom outputs plus one change output + debug!( + "My fee contribution estimation: {} for n_inputs: {}, n_outputs: {}, n_kernels: {}, num_participants: {}", + my_fee.fee(), inputs.len(), setup_args.selection_args.num_custom_outputs() + 1, 1, setup_args.num_participants + ); + // Make sure `my_fee < net_change` holds for the receiver. This can't be true for a self-spend, because nobody + // has a net_change > 0 which makes a self-spend ok to be a net negative when fees are included. + if net_change > 0 && my_fee.fee() > net_change.abs() as u64 { + return Err(Error::GenericError(format!( + "My contribution as a receiver would be net negative. my_fee: {}, net_change: {}", + my_fee.fee(), + net_change + ))); + } + // Add my share of fee contribution to the slate fees + slate.fee_fields = FeeFields::new(0, slate.fee_fields.fee() + my_fee.fee())?; + debug!("Slate.fee: {}", slate.fee_fields.fee()); + + // Create a Context for this slate + let keychain = w.keychain(keychain_mask)?; + // is_initiator only affects the deterministic test RNG; contracts always pass true. + let mut context = Context::new(keychain.secp(), &parent_key_id, use_test_rng, true); + // Context.fee will hold _our_ fee contribution and not the total slate fee + context.fee = my_fee.as_opt(); + // Context.amount is not used in contracts, but we set it anyway. + context.amount = slate.amount; + // Store the full setup args on the context; later steps (add_outputs) read the + // SelectionArgs and ttl from here. + context.setup_args = Some(setup_args.clone()); + debug!( + "Setting Context.net_change as: {}", + context.get_net_change()? + ); + + Ok(context) +} + +/// Add outputs to a contract context (including spent outputs which get locked) +pub fn add_outputs( + w: &mut WalletBackend, + keychain_mask: Option<&SecretKey>, + context: &mut Context, +) -> Result<(), Error> +where + C: NodeClient, + K: Keychain, +{ + debug!("contract::utils::add_outputs => called"); + // Do nothing if we have already contributed our outputs. The assumption is that if this was done, + // our output contribution is complete. + if context.output_ids.len() > 0 || context.input_ids.len() > 0 { + debug!("contract::utils::add_outputs => outputs have already been added, returning."); + return Ok(()); + } + let setup_args = context + .setup_args + .as_ref() + .ok_or_else(|| Error::GenericError("Context carries no contract setup args".to_string()))?; + debug!("contract::utils::add_outputs => adding outputs"); + let current_height = w.w2n_client().get_chain_tip()?.0; + let parent_key_id = &context.parent_key_id; + + // Select inputs for which `Σmy_inputs >= Σmy_outputs + my_fee_cost` holds. Uses committed fee if present. + let (inputs, my_output_amounts, my_fee) = prepare_outputs( + &mut *w, + parent_key_id, + current_height, + &setup_args, + context.fee, + )?; + // An old or damaged context, or a future change to the fee schedule, can reach this + // with a stored fee that no longer matches what we just recomputed. Report both + // values instead of terminating the wallet. + let ctx_fee = context + .fee + .ok_or_else(|| Error::GenericError("Context carries no fee".to_string()))?; + if my_fee.fee() != ctx_fee.fee() { + return Err(Error::GenericError(format!( + "Recomputed fee {} does not match the fee stored in the context {}", + my_fee.fee(), + ctx_fee.fee() + ))); + } + // Add selected/created inputs/outputs to the context + add_inputs_to_ctx(context, &inputs)?; + add_outputs_to_ctx(w, keychain_mask, context, my_output_amounts)?; + + Ok(()) +} + +/// Add inputs to Context +fn add_inputs_to_ctx(context: &mut Context, inputs: &Vec) -> Result<(), Error> { + debug!("contract::utils::add_inputs_to_ctx => adding inputs to context"); + for input in inputs { + context.add_input(&input.key_id, &input.mmr_index, input.value); + debug!( + "contract::utils::add_inputs_to_ctx => input id: {}, value:{}", + &input.key_id, input.value + ); + } + + Ok(()) +} + +/// Add outputs to Context +fn add_outputs_to_ctx( + w: &mut WalletBackend, + keychain_mask: Option<&SecretKey>, + context: &mut Context, + amounts: Vec, +) -> Result<(), Error> +where + C: NodeClient, + K: Keychain, +{ + // Derive under the contract context's account, not the active account: a late-lock + // may have a different account active than the one set on the Context. + let parent_key_id = context.parent_key_id.clone(); + for amount in amounts { + let key_id = w.next_child_for(&parent_key_id, keychain_mask)?; + context.add_output(&key_id, &None, amount); + debug!( + "contract::utils::add_output_to_ctx => added output to context. Output id: {}, amount: {}", + key_id.clone(), + amount + ); + } + Ok(()) +} diff --git a/libwallet/src/contract/mod.rs b/libwallet/src/contract/mod.rs new file mode 100644 index 000000000..a7dbaf544 --- /dev/null +++ b/libwallet/src/contract/mod.rs @@ -0,0 +1,28 @@ +// 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. + +//! This module contains contract related actions. + +mod actions; +mod context; +pub mod proofs; +mod selection; +mod slate; +pub mod types; +mod utils; + +pub use self::actions::{new, revoke, setup, sign, view}; + +pub use self::slate::can_finalize; +pub use self::utils::my_fee_contribution; diff --git a/libwallet/src/contract/proofs.rs b/libwallet/src/contract/proofs.rs new file mode 100644 index 000000000..ae810860f --- /dev/null +++ b/libwallet/src/contract/proofs.rs @@ -0,0 +1,495 @@ +// Copyright 2023 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Experimental early payment proof functionality, currently only used +//! with contracts. Can move outside of this module if early proofs are adopted +//! by legacy transactions + +use crate::backend::WalletBackend; +use crate::contract::types::ProofArgs; +use crate::grin_core::libtx::aggsig; +use crate::grin_core::libtx::secp_ser; +use crate::grin_core::ser as grin_ser; +use crate::grin_core::ser::{Readable, Reader, Writeable, Writer}; +use crate::grin_keychain::Keychain; +use crate::grin_util::secp::key::{PublicKey, SecretKey}; +use crate::grin_util::secp::pedersen::Commitment; +use crate::grin_util::secp::Signature; +use crate::grin_util::static_secp_instance; +use crate::slate::{PaymentInfo, PaymentMemo, Slate}; +use crate::slate_versions::ser as dalek_ser; +use crate::types::{Context, NodeClient}; +use crate::{address, Error}; +use byteorder::{BigEndian, ByteOrder}; +use chrono::{DateTime, Utc}; +use ed25519_dalek::Signature as DalekSignature; +use ed25519_dalek::SigningKey as DalekSecretKey; +use ed25519_dalek::VerifyingKey as DalekPublicKey; +use ed25519_dalek::{Signer, Verifier}; +use grin_util::secp::Message; + +/// All elements required to validate a proof within a single struct +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct ProofWitness { + /// Kernel index, supplied so verifiers can look up kernel + /// without an expensive lookup operation + #[serde(with = "secp_ser::string_or_u64")] + pub kernel_index: u64, + /// Kernel commitment, supplied so prover can recompute index + /// if required after a reorg + #[serde( + serialize_with = "secp_ser::as_hex", + deserialize_with = "secp_ser::commitment_from_hex" + )] + pub kernel_commitment: Commitment, + /// sender partial signature, used to recover receiver partial signature + #[serde(with = "secp_ser::sig_serde")] + pub sender_partial_sig: Signature, +} + +/// Payment proof, to be extracted from slates for +/// signing (when wrapped as PaymentProofBin) or json export from stored tx data +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct InvoiceProof { + /// Proof type, 0x00 legacy (though this will use StoredProofInfo above, 1 invoice, 2 Sender nonce) + pub proof_type: u8, + /// amount + #[serde(with = "secp_ser::string_or_u64")] + pub amount: u64, + /// receiver's public nonce from signing + #[serde(with = "secp_ser::pubkey_serde")] + pub receiver_public_nonce: PublicKey, + /// receiver's public excess from signing + #[serde(with = "secp_ser::pubkey_serde")] + pub receiver_public_excess: PublicKey, + /// Sender's address + #[serde(with = "dalek_ser::dalek_pubkey_serde")] + pub sender_address: DalekPublicKey, + /// Timestamp provided by recipient when signing + pub timestamp: i64, + /// Optional payment memo + #[serde(skip_serializing_if = "Option::is_none")] + pub memo: Option, + /// Not serialized in binary format + #[serde(with = "dalek_ser::option_dalek_sig_serde")] + pub promise_signature: Option, + /// Not serialized in binary format, just a convenient place to insert + /// the witness kernel commitment index + #[serde(skip_serializing_if = "Option::is_none")] + pub witness_data: Option, +} + +struct InvoiceProofBin(InvoiceProof); + +impl Writeable for InvoiceProofBin { + fn write(&self, writer: &mut W) -> Result<(), grin_ser::Error> { + writer.write_u8(1)?; + + // Amount field is 7 bytes, throw error if value is greater + let mut amount_bytes = [0; 8]; + BigEndian::write_u64(&mut amount_bytes, self.0.amount); + + if amount_bytes[0] > 0 { + return Err(grin_ser::Error::UnexpectedData { + expected: [0u8].to_vec(), + received: [amount_bytes[0]].to_vec(), + }); + } + writer.write_fixed_bytes(amount_bytes[1..].to_vec())?; + { + let static_secp = static_secp_instance(); + let static_secp = static_secp.lock(); + writer.write_fixed_bytes( + self.0 + .receiver_public_nonce + .serialize_vec(&static_secp, true), + )?; + writer.write_fixed_bytes( + self.0 + .receiver_public_excess + .serialize_vec(&static_secp, true), + )?; + } + writer.write_fixed_bytes(self.0.sender_address.as_bytes())?; + writer.write_i64(self.0.timestamp)?; + match &self.0.memo { + Some(s) => { + writer.write_u8(s.memo_type)?; + writer.write_fixed_bytes(&s.memo.to_vec())?; + } + None => { + writer.write_u8(0)?; + writer.write_fixed_bytes([0u8; 32].to_vec())?; + } + } + Ok(()) + } +} + +/// Currently only exercised by the tests, but this is a normal Readable, so every +/// externally shaped value it reads is mapped to a serialization error rather than +/// unwrapped. +impl Readable for InvoiceProofBin { + fn read(reader: &mut R) -> Result { + // first 8 bytes are proof type + 7 bytes worth of amount + let mut amount = reader.read_u64()?; + // The shift leaves a single byte, so this conversion cannot lose data + let proof_type = ((amount & 0xFF00_0000_0000_0000) >> 56) as u8; + amount &= 0x00FF_FFFF_FFFF_FFFF; + + let receiver_public_nonce; + let receiver_public_excess; + { + let static_secp = static_secp_instance(); + let static_secp = static_secp.lock(); + receiver_public_nonce = + PublicKey::from_slice(&static_secp, &reader.read_fixed_bytes(33)?) + .map_err(|_| grin_ser::Error::CorruptedData)?; + receiver_public_excess = + PublicKey::from_slice(&static_secp, &reader.read_fixed_bytes(33)?) + .map_err(|_| grin_ser::Error::CorruptedData)?; + } + + let sender_address_vec = reader.read_fixed_bytes(32)?; + let sender_address_bytes = <&[u8; 32]>::try_from(sender_address_vec.as_slice()) + .map_err(|_| grin_ser::Error::CorruptedData)?; + let sender_address = DalekPublicKey::from_bytes(sender_address_bytes) + .map_err(|_| grin_ser::Error::CorruptedData)?; + + let timestamp = reader.read_i64()?; + + let memo_type = reader.read_u8()?; + let memo = reader.read_fixed_bytes(32)?; + let memo_bytes = + <[u8; 32]>::try_from(memo.as_slice()).map_err(|_| grin_ser::Error::CorruptedData)?; + + let res = InvoiceProof { + proof_type, + amount, + receiver_public_nonce, + receiver_public_excess, + sender_address, + timestamp, + memo: match memo_type { + 0 => None, + _ => Some(PaymentMemo { + memo_type, + memo: memo_bytes, + }), + }, + promise_signature: None, + witness_data: None, + }; + + Ok(InvoiceProofBin(res)) + } +} + +impl InvoiceProof { + /// Extracts as much data as possible from the slate to create an invoice proof + pub fn from_slate( + slate: &Slate, + participant_index: usize, + sender_address: Option, + ) -> Result { + // Bounds-check the participant index before indexing participant_data, so a + // malformed slate returns an error rather than panicking. + if participant_index >= slate.participant_data.len() { + return Err(Error::GenericError(format!( + "Participant index {} out of range for slate with {} participant(s)", + participant_index, + slate.participant_data.len() + ))); + } + // Sender address is either provided or in slate (or error) + let sender_address = match sender_address { + Some(a) => a, + None => { + if let Some(ref p) = slate.payment_proof { + if let Some(a) = p.sender_address { + a + } else { + return Err(Error::NoSenderAddressProvided); + } + } else { + return Err(Error::NoSenderAddressProvided); + } + } + }; + + let timestamp = match slate.payment_proof.as_ref() { + Some(p) => p.timestamp.timestamp(), + None => 0, + }; + + let memo = match slate.payment_proof.as_ref() { + Some(p) => p.memo.clone(), + None => None, + }; + + let promise_signature = match slate.payment_proof.as_ref() { + Some(p) => p.promise_signature.clone(), + None => None, + }; + + Ok(Self { + proof_type: 1u8, + amount: slate.amount, + receiver_public_nonce: slate.participant_data[participant_index].public_nonce, + receiver_public_excess: slate.participant_data[participant_index].public_blind_excess, + sender_address, + timestamp, + memo, + promise_signature, + witness_data: None, + }) + } + + /// Sign the invoice proof, provided all fields are populated + pub fn sign(&self, sec_key: &SecretKey) -> Result<(DalekSignature, DalekPublicKey), Error> { + let d_skey = DalekSecretKey::from_bytes(&sec_key.0); + let pub_key = d_skey.verifying_key(); + let mut sig_data_bin = Vec::new(); + grin_ser::serialize_default(&mut sig_data_bin, &InvoiceProofBin(self.clone())).map_err( + |e| Error::GenericError(format!("InvoiceProof serialization failed: {}", e)), + )?; + + Ok((d_skey.sign(&sig_data_bin), pub_key)) + } + + /// Verify the signature of the invoice proof + pub fn verify_promise_signature( + &self, + recipient_address: &DalekPublicKey, + ) -> Result<(), Error> { + if self.promise_signature.is_none() { + return Err(Error::PaymentProofValidation( + "Missing promise signature".into(), + )); + } + + // Rebuild message + let mut sig_data_bin = Vec::new(); + grin_ser::serialize_default(&mut sig_data_bin, &InvoiceProofBin(self.clone())).map_err( + |e| Error::GenericError(format!("InvoiceProof serialization failed: {}", e)), + )?; + + if recipient_address + .verify(&sig_data_bin, self.promise_signature.as_ref().unwrap()) + .is_err() + { + return Err(Error::PaymentProof( + "Invalid recipient signature".to_owned(), + )); + }; + Ok(()) + } + + /// Verify signature and proof against a given kernel message (kernel lookup is beyond the scope + /// of this module) + pub fn verify_witness( + &self, + recipient_address: &DalekPublicKey, + excess_sig: &Signature, + msg: &Message, + ) -> Result<(), Error> { + if self.witness_data.is_none() { + return Err(Error::PaymentProofValidation("Missing witness data".into())); + } + + self.verify_promise_signature(recipient_address)?; + + let wd = self.witness_data.as_ref().unwrap().clone(); + { + let static_secp = static_secp_instance(); + let static_secp = static_secp.lock(); + + let receiver_part_sig = + aggsig::subtract_signature(&static_secp, &excess_sig, &wd.sender_partial_sig)?; + + // Retrieve the public nonce sum from the kernel excess signature + let mut pub_nonce_sum_bytes = [3u8; 33]; + pub_nonce_sum_bytes[1..33].copy_from_slice(&excess_sig[0..32]); + let pub_nonce_sum = PublicKey::from_slice(&static_secp, &pub_nonce_sum_bytes)?; + + // Retrieve the public key sum from the kernel excess + let pub_blind_sum = wd.kernel_commitment.to_pubkey(&static_secp)?; + + if let Err(_) = aggsig::verify_partial_sig( + &static_secp, + &receiver_part_sig.0, + &pub_nonce_sum, + &self.receiver_public_excess, + Some(&pub_blind_sum), + &msg, + ) { + // Try other possibility + if let Some(s) = receiver_part_sig.1 { + aggsig::verify_partial_sig( + &static_secp, + &s, + &pub_nonce_sum, + &self.receiver_public_excess, + Some(&pub_blind_sum), + &msg, + )?; + } else { + return Err(Error::PaymentProofValidation( + "Signature subtraction failed".into(), + )); + } + } + } + Ok(()) + } +} + +impl serde::Serialize for InvoiceProofBin { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut vec = vec![]; + grin_ser::serialize(&mut vec, grin_ser::ProtocolVersion(4), self) + .map_err(|err| serde::ser::Error::custom(err.to_string()))?; + serializer.serialize_bytes(&vec) + } +} + +/// Adds all info needed for a payment proof to a slate, complete with signed recipient data +pub fn add_payment_proof( + wallet: &mut WalletBackend, + keychain_mask: Option<&SecretKey>, + slate: &mut Slate, + context: &Context, + proof_args: &ProofArgs, +) -> Result<(), Error> +where + C: NodeClient, + K: Keychain, +{ + // Invoice (type 1) is the only contract proof type today; other types are future + // work tied to the early-payment-proof RFC. + let (invoice_proof, promise_signature, receiver_address) = + generate_invoice_signature(wallet, keychain_mask, slate, context, proof_args)?; + // Carry over the timestamp the promise signature was made over rather than reading + // the clock a second time. The signature binds it, so a tick between the two reads + // would leave a proof that cannot verify. + let timestamp = DateTime::from_timestamp(invoice_proof.timestamp, 0).ok_or_else(|| { + Error::GenericError(format!( + "Invalid proof timestamp: {}", + invoice_proof.timestamp + )) + })?; + + let proof = PaymentInfo { + sender_address: proof_args.sender_address.clone(), + receiver_address, + timestamp, + promise_signature: Some(promise_signature), + memo: invoice_proof.memo, + }; + slate.payment_proof = Some(proof); + Ok(()) +} + +/// Generates a signature for proof type 'Invoice' +fn generate_invoice_signature( + wallet: &mut WalletBackend, + keychain_mask: Option<&SecretKey>, + slate: &mut Slate, + context: &Context, + proof_args: &ProofArgs, +) -> Result<(InvoiceProof, DalekSignature, DalekPublicKey), Error> +where + C: NodeClient, + K: Keychain, +{ + let keychain = wallet.keychain(keychain_mask)?; + let index = slate.find_index_matching_context(&keychain, context)?; + let mut invoice_proof = InvoiceProof::from_slate(&slate, index, proof_args.sender_address)?; + let derivation_index = match context.payment_proof_derivation_index { + Some(i) => i, + None => 0, + }; + // Derive the proof address under the contract's account, not the active one. + let parent_key_id = context.parent_key_id.clone(); + let recp_key = + address::address_from_derivation_path(&keychain, &parent_key_id, derivation_index)?; + + invoice_proof.timestamp = Utc::now().timestamp(); + let (sig, addr) = invoice_proof.sign(&recp_key)?; + Ok((invoice_proof, sig, addr)) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::slate_versions::tests::populate_test_slate; + + #[test] + fn ser_invoice_proof_bin() -> Result<(), Error> { + let mut slate = populate_test_slate()?; + slate.amount |= 0xFF00_0000_0000_0000; + // Bin serialization doesn't include promise sig as it's used to create signature data + slate.payment_proof.as_mut().unwrap().promise_signature = None; + + // Should fail, amount too big + let invoice_proof = InvoiceProof::from_slate(&slate, 1, None)?; + let mut vec = Vec::new(); + assert!(grin_ser::serialize_default(&mut vec, &InvoiceProofBin(invoice_proof)).is_err()); + + // Should be okay now + slate.amount = 1234; + let invoice_proof = InvoiceProof::from_slate(&slate, 1, None)?; + let mut vec = Vec::new(); + grin_ser::serialize_default(&mut vec, &InvoiceProofBin(invoice_proof.clone())) + .expect("Serialization Failed"); + + let proof_deser: InvoiceProofBin = grin_ser::deserialize_default(&mut &vec[..]).unwrap(); + assert_eq!(invoice_proof, proof_deser.0); + Ok(()) + } + + // Every key the reader takes from the encoded form is externally shaped, so a + // corrupted one has to come back as a serialization error rather than a panic. + #[test] + fn deser_invoice_proof_bin_rejects_bad_keys() -> Result<(), Error> { + let mut slate = populate_test_slate()?; + slate.amount = 1234; + slate.payment_proof.as_mut().unwrap().promise_signature = None; + let invoice_proof = InvoiceProof::from_slate(&slate, 1, None)?; + + let mut vec = Vec::new(); + grin_ser::serialize_default(&mut vec, &InvoiceProofBin(invoice_proof)) + .expect("Serialization Failed"); + + // The two secp keys follow the 8 amount bytes, then the ed25519 sender address + for offset in [8, 41, 74] { + let mut corrupted = vec.clone(); + // An all-ones prefix is not a valid point in either encoding + for b in corrupted[offset..offset + 8].iter_mut() { + *b = 0xFF; + } + let res: Result = + grin_ser::deserialize_default(&mut &corrupted[..]); + assert!( + res.is_err(), + "corrupting offset {} should not parse", + offset + ); + } + Ok(()) + } +} diff --git a/libwallet/src/contract/selection.rs b/libwallet/src/contract/selection.rs new file mode 100644 index 000000000..edeab0e78 --- /dev/null +++ b/libwallet/src/contract/selection.rs @@ -0,0 +1,681 @@ +// 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. + +//! Contract coin selection functions + +use crate::backend::WalletBackend; +use crate::contract::types::{ContractSetupArgsAPI, OutputSelectionArgs}; +use crate::contract::utils::my_fee_contribution; +use crate::grin_core::core::amount_to_hr_string; +use crate::grin_keychain::{Identifier, Keychain}; +use crate::types::NodeClient; +use crate::{Error, OutputData}; +use grin_core::core::FeeFields; + +/// Prepares inputs & outputs that satisfy `Σmy_inputs >= Σmy_outputs + my_fee_cost` taking into account selection args +pub fn prepare_outputs( + w: &mut WalletBackend, + parent_key_id: &Identifier, + current_height: u64, + setup_args: &ContractSetupArgsAPI, + committed_fee: Option, +) -> Result<(Vec, Vec, FeeFields), Error> +where + C: NodeClient, + K: Keychain, +{ + // Find available inputs + let mut eligible_inputs = find_eligible(w, parent_key_id, current_height)?; + // Select which inputs to use to satisfy the equation + compute(setup_args, committed_fee, &mut eligible_inputs) +} + +/// Find all inputs eligible to spend +pub fn find_eligible( + w: &mut WalletBackend, + parent_key_id: &Identifier, + current_height: u64, +) -> Result, Error> +where + C: NodeClient, + K: Keychain, +{ + // Find eligible inputs in the wallet + let eligible_inputs = w + .iter()? + .filter(|out| out.root_key_id == *parent_key_id && out.eligible_to_spend(current_height, 1)) + .collect::>(); + Ok(eligible_inputs) +} +// Given a list of inputs, an optional committed fee and setup args, compute which inputs to use, what amount outputs to make and fee cost +pub fn compute( + setup_args: &ContractSetupArgsAPI, + committed_fee: Option, + inputs: &mut Vec, +) -> Result<(Vec, Vec, FeeFields), Error> +where +{ + let (inputs, fee) = select_inputs(setup_args, committed_fee, inputs)?; + let output_amounts = build_output_amount_list( + inputs.clone().iter().map(|out| out.value).sum::(), + fee.fee(), + setup_args, + )?; + Ok((inputs, output_amounts, fee)) +} + +// Given a list of inputs, an optional committed fee and setup args, compute which inputs to use +fn select_inputs( + setup_args: &ContractSetupArgsAPI, + committed_fee: Option, + inputs: &mut Vec, +) -> Result<(Vec, FeeFields), Error> +where +{ + // We use 'lhs' and 'rhs' to denote the amounts on the left/right-hand side of the equation. + // To simulate receive/payment value we: + // - add positive net_change to the 'lhs' for the receiver (to simulate sender's input) + // - add positive net_change to the 'rhs' for the sender (to simulate receiver's output) + // For either party, the following MUST hold for the inputs the function returns: + // Σmy_inputs >= Σmy_outputs + my_fee_cost + // Each party later balances the equation by adding an additional output (change output or receiver output) + let net_change = setup_args.net_change.ok_or_else(|| { + Error::GenericError("Contract requires a net change (--send or --receive)".to_string()) + })?; + let custom_outputs_amount_sum = setup_args.selection_args.sum_output_amounts()?; + // unsigned_abs, as abs() panics on i64::MIN + let pay_amount = if net_change < 0 { + net_change.unsigned_abs() + } else { + 0 + }; + // Add the amount we pay and the custom outputs to rhs of the equation + let rhs = pay_amount + .checked_add(custom_outputs_amount_sum) + .ok_or_else(|| { + Error::GenericError(format!( + "Amount overflow: pay amount {} plus custom outputs {}", + pay_amount, custom_outputs_amount_sum + )) + })?; + let required_inputs = setup_args.selection_args.required_inputs(); + let is_payjoin = setup_args.selection_args.is_payjoin(); + let is_self_spend = setup_args.num_participants == 1; + debug!( + "contract::selection::selecting inputs: num_participants: {}, min_input_amount: {}, is_payjoin: {}", + setup_args.num_participants, rhs, is_payjoin + ); + // We don't try to contribute an input only in the case where we have multiple participants + // where we are on the receiving end and we don't want to do a payjoin + if !is_self_spend && (pay_amount == 0 && !is_payjoin) { + return Ok(( + vec![], + my_fee_contribution( + 0, + setup_args.selection_args.num_custom_outputs() + 1, + 1, + setup_args.num_participants, + )?, + )); + } + // NOTE: that these are inputs that MUST be selected. We should lock the inputs if they're + // required to minimize any potential race conditions. + let must_use_list = required_inputs.unwrap_or(vec![]); + if must_use_list.len() > 0 { + // Sort the inputs first by the ones listed in the use_inputs and then by value + inputs.sort_by_key(|out| { + ( + // We have to negate the boolean to prioritize truthy values because + // false is 0 and hence would be sorted before truthy entries + !(out.commit.is_some() + && must_use_list.contains(&&out.commit.as_ref().unwrap()[..])), + out.value, + ) + }); + } else { + // Sort the inputs only by value + inputs.sort_by_key(|out| out.value); + } + + // NOTE: Since we sort by value increasingly, if we hold any 0-value inputs, they will all be used if we're the sender + // or a single one if we're the receiver doing a payjoin. + // If we are the receiver, we pretend we have a virtual input from the sender (for which we don't pay the fees) so we can easily + // test that lhs >= rhs and see if we will be able to satisfy equation. We simulate this by starting with lhs = net_change. + let mut lhs: u64 = 0; + if net_change > 0 { + lhs = u64::try_from(net_change) + .map_err(|_| Error::GenericError(format!("net change {} exceeds u64", net_change)))?; + } + // We want to count how many inputs we've picked _so far_. This is used to prevent picking + // all 0*H +r*G outputs when we call with min_input_amount=0 and want just a payjoin. + let mut n_inputs = 0; + let mut must_use_list_cnt: u32 = 0; + let my_num_outputs = setup_args.selection_args.num_custom_outputs() + 1; + // If we have already committed to a fee (context.fee) then set this as our "minimum" fee. The reason we have to + // do this is to avoid solving the equation for less than the committed fee. We have to guarantee the inputs we take + // are enough to cover the committed fee. At the end of selection, we check that the fees for the selection were not + // higher than the fee value we committed to. + let mut my_fee = if committed_fee.is_some() { + committed_fee.unwrap() + } else { + // We start with a fee of 1 output and a shared kernel which is minimum for both parties + my_fee_contribution(0, 1, 1, setup_args.num_participants).unwrap() + // FeeFields::zero() + }; + + // NOTE: This always takes at least one input if it is available. We take the inputs we must take and then we take + // inputs until we fulfill Σmy_inputs >= Σmy_outputs + my_fee_cost + let selected_inputs = inputs + .iter() + .take_while(|out| { + // Take the commitment if it is listed as one of those we MUST take + let must_take = + out.commit.is_some() && must_use_list.contains(&&out.commit.as_ref().unwrap()[..]); + // Compute the fee without this input + let fee_without = + my_fee_contribution(n_inputs, my_num_outputs, 1, setup_args.num_participants) + .unwrap(); + // Compute the total fee cost if we took this input + let mut fee_with = + my_fee_contribution(n_inputs + 1, my_num_outputs, 1, setup_args.num_participants) + .unwrap(); + // If the current fee is lower than the committed fee (my_fee) then set it to committed fee + if my_fee.fee() > fee_with.fee() { + fee_with = my_fee; + } + // If we don't have a "must take" input, have contributed an input and have enough to balance the equation, we can stop + let can_finish = lhs >= (rhs + fee_without.fee()) && n_inputs > 0 && !must_take; + if can_finish { + return false; + } + // Take the commitment if `lhs < rhs+fees_with` (or if we have not yet taken an input - payjoins) + let should_take = lhs < (rhs + fee_with.fee()) || n_inputs == 0; + let res = must_take || should_take; + if res { + lhs += out.value; + n_inputs += 1; + // Update the fee cost if we decided to take the input + my_fee = fee_with; + if must_take { + must_use_list_cnt += 1; + } + } + debug!( + "contract::selection::select_inputs => out_value:{}, new my_inputs_sum:{}", + out.value, lhs + ); + res + }) + .cloned() + .collect::>(); + + // Return an error if the fee computed is larger than the committed fee + if committed_fee.is_some() && my_fee.fee() > committed_fee.unwrap().fee() { + let msg = format!( + "Fee computed ({}) is larger than the committed fee ({}); cancel the transaction and retry", + my_fee.fee(), + committed_fee.unwrap().fee() + ); + return Err(Error::Fee(msg).into()); + } + + // Check that the inputs we picked are enough to cover all our output amounts and fees + // asserts that Σmy_inputs >= Σmy_outputs + my_fee_cost + if lhs < rhs + my_fee.fee() { + let total = inputs.iter().fold(0, |acc, x| acc + x.value); + debug!("Not enough funds. Total funds eligible to spend: {}, needed: {}. Fee cost for this transaction: {}", total, rhs+my_fee.fee(), my_fee.fee()); + return Err(Error::NotEnoughFunds { + available: total, + available_disp: amount_to_hr_string(total, false), + needed: rhs + my_fee.fee(), + needed_disp: amount_to_hr_string(rhs + my_fee.fee(), false), + } + .into()); + // return Err(ErrorKind::GenericError(msg.into()).into()); + } + + // Assert that all the use_inputs have been selected + if must_use_list.len() != must_use_list_cnt as usize { + let msg = format!( + "We have not found all the inputs that have been requested. {}, found only: {}", + setup_args.selection_args.use_inputs.as_ref().unwrap(), + must_use_list_cnt + ); + return Err(Error::GenericError(msg.into()).into()); + } + + debug!( + "contract::selection::select_inputs => selected_inputs: {:#?}", + selected_inputs + ); + // We are returning a set of inputs for which `Σmy_inputs >= Σmy_outputs + my_fee_cost` holds + Ok((selected_inputs, my_fee)) +} + +fn build_output_amount_list( + my_input_sum: u64, + my_fee_cost: u64, + setup_args: &ContractSetupArgsAPI, +) -> Result, Error> { + let expected_net_change = setup_args.net_change.ok_or_else(|| { + Error::GenericError("Contract requires a net change (--send or --receive)".to_string()) + })?; + let mut my_output_amounts = setup_args.selection_args.output_amounts()?; + let custom_outputs_sum = my_output_amounts + .iter() + .try_fold(0u64, |acc, v| acc.checked_add(*v)) + .ok_or_else(|| Error::GenericError("Custom output amounts overflow".to_string()))?; + // We balance `Σmy_inputs >= Σmy_outputs + my_fee_cost` by adding an output holding the + // missing amount (change output or receiver output). A receiver that is not doing a + // payjoin contributes no inputs while --make-outputs is still allowed, so my_input_sum + // can be below custom_outputs_sum: the whole equation is evaluated in checked i64 so + // that case is reported rather than wrapping. + let input_sum = i64::try_from(my_input_sum) + .map_err(|_| Error::GenericError(format!("input sum {} exceeds i64", my_input_sum)))?; + let outputs_sum = i64::try_from(custom_outputs_sum).map_err(|_| { + Error::GenericError(format!("output sum {} exceeds i64", custom_outputs_sum)) + })?; + let fee_cost = i64::try_from(my_fee_cost) + .map_err(|_| Error::GenericError(format!("fee cost {} exceeds i64", my_fee_cost)))?; + let my_change_output_amount = input_sum + .checked_sub(outputs_sum) + .and_then(|v| v.checked_add(expected_net_change)) + .and_then(|v| v.checked_sub(fee_cost)) + .ok_or_else(|| { + Error::GenericError(format!( + "contract::selection::build_output_amount_list => amount overflow. Values: my_input_sum: {}, custom_outputs_sum: {}, expected_net_change: {}, my_fee_cost: {}", + input_sum, outputs_sum, expected_net_change, fee_cost + )) + })?; + // A negative change output would mean the selection math is off; reject it explicitly. + if my_change_output_amount < 0 { + return Err(Error::GenericError(format!( + "contract::selection::build_output_amount_list => negative change output. Values: my_input_sum: {}, custom_outputs_sum: {}, expected_net_change: {}, my_fee_cost: {}", + input_sum, outputs_sum, expected_net_change, fee_cost + ))); + } + // Add our change/receiver output (which can be a zero-value output) to the list of outputs + my_output_amounts.push(my_change_output_amount as u64); + debug!( + "contract::selection::build_output_amount_list => inputs sum: {}, my_output_amounts:{:#?}", + my_input_sum, my_output_amounts + ); + Ok(my_output_amounts) +} + +/// Compares the output selection args provided at call with those from Context and checks whether they conflict +pub fn verify_selection_consistency( + ctx_args: &OutputSelectionArgs, + cur_args: &OutputSelectionArgs, +) -> Result<(), Error> { + // We can't define a selection strategy if we've already done the setup phase. We only allow to pass either the + // default or exactly the same strategy we defined when doing the setup phase. + if cur_args != ctx_args && cur_args != &OutputSelectionArgs::default() { + return Err(Error::GenericError(format!( + "Can't define selection args now because we've already done the setup phase. ctx_selection_args:{:#?}, cur_selection_args:{:#?}", + ctx_args, cur_args + ))); + } + // NOTE: The logic above isn't perfect. This is because the user could define arguments that are the default. In this case + // we'd simply silently use the arguments provided in the setup phase. This could be confusing for the user. + Ok(()) +} + +// Tests +#[cfg(test)] +mod tests { + + use super::*; + use crate::grin_keychain::{Identifier, IDENTIFIER_SIZE}; + use crate::OutputStatus; + + fn _create_output_data_for(amounts: Vec) -> Vec { + let mut rv: Vec = vec![]; + for (idx, amount) in amounts.iter().enumerate() { + let identifier = [0u8; IDENTIFIER_SIZE]; + let key_id = Identifier::from_bytes(&identifier); + rv.push(OutputData { + // The identifiers here don't make sense, but they're not needed for testing + root_key_id: key_id.clone(), + key_id: key_id.clone(), + n_child: key_id.clone().to_path().last_path_index(), + mmr_index: None, + commit: Some(format!("{}{}", "abc", idx.to_string())), + value: *amount, + status: OutputStatus::Unspent, + height: 1, + lock_height: 0, + is_coinbase: false, + tx_log_entry: None, + }); + } + rv + } + + // A receiver that is not doing a payjoin contributes no inputs, while --make-outputs + // is still allowed, so the output balance is computed from a zero input sum against a + // non-zero custom output sum. That has to be reported, not wrapped around. + #[test] + fn receiver_no_payjoin_with_custom_outputs() { + let setup_args = ContractSetupArgsAPI { + net_change: Some(5_000_000_000), + selection_args: OutputSelectionArgs { + // no use_inputs, so is_payjoin() is false and no inputs are selected + make_outputs: Some(vec![1_000_000_000]), + ..Default::default() + }, + ..Default::default() + }; + // Receiving more than the custom outputs and the fee leaves a valid balance + let my_fee = my_fee_contribution(0, 2, 1, 2).unwrap(); + let amounts = build_output_amount_list(0, my_fee.fee(), &setup_args).unwrap(); + assert_eq!(amounts.len(), 2); + assert_eq!(amounts[0], 1_000_000_000); + assert_eq!(amounts[1], 5_000_000_000 - 1_000_000_000 - my_fee.fee()); + + // Custom outputs beyond what we receive are rejected rather than underflowing + let setup_args = ContractSetupArgsAPI { + net_change: Some(1_000_000_000), + selection_args: OutputSelectionArgs { + make_outputs: Some(vec![5_000_000_000]), + ..Default::default() + }, + ..Default::default() + }; + assert!(build_output_amount_list(0, my_fee.fee(), &setup_args).is_err()); + } + + #[test] + fn sender_no_inputs() { + // net_change=-1, no inputs, no fee committed => NotEnoughFunds + let setup_args = ContractSetupArgsAPI { + net_change: Some(-1_000_000_000), + ..Default::default() + }; + let expected = Error::NotEnoughFunds { + available: 0, + available_disp: amount_to_hr_string(0, false), + needed: 1_000_000_000 + my_fee_contribution(0, 1, 1, 2).unwrap().fee(), + needed_disp: amount_to_hr_string( + 1_000_000_000 + my_fee_contribution(0, 1, 1, 2).unwrap().fee(), + false, + ), + }; + let result = compute(&setup_args, None, &mut vec![]); + assert_eq!(result.err().unwrap(), expected); + } + + #[test] + fn sender_not_enough_funds_for_fee() { + // net_change=-3, inputs=[2, 1], no fee committed => NotEnoughFunds because we can't pay for fees + let setup_args = ContractSetupArgsAPI { + net_change: Some(-3_000_000_000), + ..Default::default() + }; + let expected = Error::NotEnoughFunds { + available: 3_000_000_000, + available_disp: amount_to_hr_string(3_000_000_000, false), + needed: 3_000_000_000 + my_fee_contribution(2, 1, 1, 2).unwrap().fee(), + needed_disp: amount_to_hr_string( + 3_000_000_000 + my_fee_contribution(2, 1, 1, 2).unwrap().fee(), + false, + ), + }; + let mut inputs = _create_output_data_for(vec![2_000_000_000, 1_000_000_000]); + let result = compute(&setup_args, None, &mut inputs); + assert_eq!(result.err().unwrap(), expected); + } + + #[test] + fn sender_happy_path() { + // net_change=-3, inputs=[3, 2, 1, 2], no fee committed => Ok([1, 2, 2], fees) + let setup_args = ContractSetupArgsAPI { + net_change: Some(-3_000_000_000), + ..Default::default() + }; + let inputs = _create_output_data_for(vec![ + 3_000_000_000, + 2_000_000_000, + 1_000_000_000, + 2_000_000_000, + ]); + // We expect 3 inputs with amounts 1, 2, 2 + let expected_inputs = vec![&inputs[2], &inputs[1], &inputs[3]]; + let expected_fee = my_fee_contribution(3, 1, 1, 2).unwrap(); + let expected_output_amounts = vec![ + // we expect a single output with change holding 5 - 3 - fees + (5_000_000_000 as i64 + (setup_args.net_change.unwrap())) as u64 - expected_fee.fee(), + ]; + let result = compute(&setup_args, None, &mut inputs.clone()).unwrap(); + + let result_ref = ( + result.0.iter().collect::>(), + result.1, + result.2, + ); + assert_eq!( + result_ref, + (expected_inputs, expected_output_amounts, expected_fee) + ); + } + + #[test] + fn sender_exact() { + // net_change=-3, inputs=[3, my_fees(2, 1)], no fee committed => Ok([3, my_fees(2, 1)], fees) + let setup_args = ContractSetupArgsAPI { + net_change: Some(-3_000_000_000), + ..Default::default() + }; + let inputs = _create_output_data_for(vec![ + my_fee_contribution(2, 1, 1, 2).unwrap().fee(), + 3_000_000_000, + ]); + let expected_inputs = inputs.clone(); // we expect both inputs in the same order + let expected_fee = my_fee_contribution(2, 1, 1, 2).unwrap(); + let expected_output_amounts = vec![0]; // we expect a change output of 0-value + let result = compute(&setup_args, None, &mut inputs.clone()).unwrap(); + + assert_eq!( + result, + (expected_inputs, expected_output_amounts, expected_fee) + ); + } + + #[test] + fn receiver_payjoin_exact() { + // net_change=-my_fees(1, 1), inputs=[my_fees(1, 0)], no fee committed => Ok([3, my_fees(2, 1)], fees) + let setup_args = ContractSetupArgsAPI { + // we expect to receive exactly our fee contribution my_fees(1, 1) + net_change: Some(my_fee_contribution(1, 1, 1, 2).unwrap().fee() as i64), + ..Default::default() + }; + let inputs = _create_output_data_for(vec![0, 1_000_000_000]); // we have a 0-value and 1 grin input + let expected_inputs = vec![&inputs[0]]; // we expect to use the 0-value input + let expected_fee = my_fee_contribution(1, 1, 1, 2).unwrap(); + let expected_output_amounts = vec![0]; // we expect a change output of 0-value + let result = compute(&setup_args, None, &mut inputs.clone()).unwrap(); + + let result_ref = ( + result.0.iter().collect::>(), + result.1, + result.2, + ); + assert_eq!( + result_ref, + (expected_inputs, expected_output_amounts, expected_fee) + ); + } + + #[test] + fn receiver_no_payjoin() { + let setup_args = ContractSetupArgsAPI { + // we expect to receive exactly our fee contribution my_fees(1, 1) + net_change: Some(3_000_000_000), + selection_args: OutputSelectionArgs { + use_inputs: None, + ..Default::default() + }, + ..Default::default() + }; + let inputs = _create_output_data_for(vec![1_000_000_000]); + let expected_inputs = vec![]; + let expected_fee = my_fee_contribution(0, 1, 1, 2).unwrap(); + let expected_output_amounts = vec![ + // we expect a single output with change holding 3 - fees + (setup_args.net_change.unwrap() as u64) - expected_fee.fee(), + ]; + let result = compute(&setup_args, None, &mut inputs.clone()).unwrap(); + + assert_eq!( + result, + (expected_inputs, expected_output_amounts, expected_fee) + ); + } + + #[test] + fn sender_use_inputs_ok() { + let setup_args = ContractSetupArgsAPI { + // we expect to receive exactly our fee contribution my_fees(1, 1) + net_change: Some(-2_000_000_000), + selection_args: OutputSelectionArgs { + use_inputs: Some(String::from("abc0,abc2,abc3")), + ..Default::default() + }, + ..Default::default() + }; + let inputs = _create_output_data_for(vec![ + 1_000_000_000, // abc0 + 2_000_000_000, + 3_000_000_000, // abc2 + 4_000_000_000, // abc3 + ]); + let expected_inputs = vec![&inputs[0], &inputs[2], &inputs[3]]; + let expected_fee = my_fee_contribution(3, 1, 1, 2).unwrap(); + let expected_output_amounts = vec![ + // we expect a single output with change holding 3 - fees + (8_000_000_000 as i64 + setup_args.net_change.unwrap()) as u64 - expected_fee.fee(), + ]; + let result = compute(&setup_args, None, &mut inputs.clone()).unwrap(); + + let result_ref = ( + result.0.iter().collect::>(), + result.1, + result.2, + ); + assert_eq!( + result_ref, + (expected_inputs, expected_output_amounts, expected_fee) + ); + } + + #[test] + fn sender_use_inputs_happy_err() { + let setup_args = ContractSetupArgsAPI { + net_change: Some(-2_000_000_000), + selection_args: OutputSelectionArgs { + // there is no abc5 input + use_inputs: Some(String::from("abc0,abc2,abc5")), + ..Default::default() + }, + ..Default::default() + }; + let inputs = _create_output_data_for(vec![ + 1_000_000_000, // abc0 + 2_000_000_000, + 3_000_000_000, // abc2 + 4_000_000_000, + ]); + let msg = format!( + "We have not found all the inputs that have been requested. abc0,abc2,abc5, found only: 2" + ); + let expected_err = Error::GenericError(msg.into()).into(); + let result = compute(&setup_args, None, &mut inputs.clone()); + + assert_eq!(result.err().unwrap(), expected_err); + } + + #[test] + fn sender_make_outputs_ok() { + let setup_args = ContractSetupArgsAPI { + net_change: Some(-2_000_000_000), + selection_args: OutputSelectionArgs { + // there is no abc5 input + make_outputs: Some(vec![1_000_000_000, 3_000_000_000]), + ..Default::default() + }, + ..Default::default() + }; + let inputs = _create_output_data_for(vec![ + 1_000_000_000, + 2_000_000_000, + 3_000_000_000, + 4_000_000_000, + ]); + // we expect all to be used (2+1+3+fees) + let expected_inputs = inputs.clone(); + let expected_fee = my_fee_contribution(4, 3, 1, 2).unwrap(); + let expected_output_amounts = vec![ + 1_000_000_000u64, + 3_000_000_000u64, + // change output + ((10_000_000_000u64 - 4_000_000_000u64) as i64 + setup_args.net_change.unwrap()) as u64 + - expected_fee.fee(), + ]; + let result = compute(&setup_args, None, &mut inputs.clone()).unwrap(); + + assert_eq!( + result, + (expected_inputs, expected_output_amounts, expected_fee) + ); + } + + #[test] + fn selection_args_consistency() { + // Selection args agreed at the setup phase (stored in the Context). + let ctx_args = OutputSelectionArgs { + use_inputs: Some("any".to_string()), + ..Default::default() + }; + // Re-supplying the same args is fine. + assert!(verify_selection_consistency(&ctx_args, &ctx_args).is_ok()); + // Supplying defaults later is fine (we fall back to the setup-phase args). + assert!(verify_selection_consistency(&ctx_args, &OutputSelectionArgs::default()).is_ok()); + // Supplying different, non-default args after setup is rejected. + let changed = OutputSelectionArgs { + use_inputs: Some("0123456789".to_string()), + ..Default::default() + }; + assert!(verify_selection_consistency(&ctx_args, &changed).is_err()); + } + + /* + + Tests to add: + - compute_receiver_invariant - test that receiving_amount - my_fees >= 0 (to prevent going into negative accidentally) + - compute_sender_invariant - test that -send_amount - my_fees < 0 (do you need this one and is it correct?) + - compute_receiver_payjoin_negative_fee - could the receiver receive a negative amount through fees? but the thing + would go through because they made a payjoin so they could pay for the fees? + - compute_receiver_omit_payjoin - we can't contribute an input, but have enough for other fees + - compute_make_outputs_fee_err - fail due to not enough funds for fees + - compute_make_outputs_sum_err - is this even possible? + - compute_zero_value_outputs_sender - sender uses all 0-value outputs when sending + - compute_zero_value_inputs_receiver - receiver uses 0-value inputs in payjoin + - test_fee_committed_err - we have already committed to a certain fee which we no longer satisfy + - think if we should have sender/receiver separate testing + - sender_use_all_features + - coinbase output cases + - validate --make-outputs has all positive u64 numbers + + */ +} diff --git a/libwallet/src/contract/slate.rs b/libwallet/src/contract/slate.rs new file mode 100644 index 000000000..495e42847 --- /dev/null +++ b/libwallet/src/contract/slate.rs @@ -0,0 +1,357 @@ +// 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. + +//! Contract functions on the Slate + +use crate::backend::WalletBackend; +use crate::grin_core::libtx::build; +use crate::grin_core::libtx::proof::ProofBuilder; +use crate::grin_keychain::Keychain; +use crate::grin_util::secp::key::{PublicKey, SecretKey}; +use crate::slate::{Slate, SlateState}; +use crate::types::{Context, NodeClient}; +use crate::Error; + +use super::types::ProofArgs; +use crate::contract::proofs::InvoiceProof; +use ed25519_dalek::VerifyingKey as DalekPublicKey; + +/// Add payment proof data to slate, noop for sender +pub fn add_payment_proof( + w: &mut WalletBackend, + slate: &mut Slate, + keychain_mask: Option<&SecretKey>, + context: &Context, + net_change: &Option, + proof_args: &ProofArgs, +) -> Result<(), Error> +where + C: NodeClient, + K: Keychain, +{ + // FUTURE: move proof handling onto Slate itself so it can be versioned (slate.add_payment_proof_data()). + debug!("contract::slate::add_payment_proof => called"); + // If we're a recipient, generate proof unless explicity told not to + if let Some(ref c) = net_change { + if *c > 0 && !proof_args.suppress_proof && slate.payment_proof.is_none() { + super::proofs::add_payment_proof(w, keychain_mask, slate, &context, proof_args)?; + } + } + + Ok(()) +} + +/// Verify payment proof signature +pub fn verify_payment_proof( + slate: &Slate, + net_change: i64, + recipient_address: &DalekPublicKey, +) -> Result<(), Error> { + // FUTURE: move proof verification onto Slate itself so it can be versioned (slate.verify_payment_proof_sig()). + debug!("contract::slate::verify_payment_proof => called"); + if net_change > 0 && slate.payment_proof.is_some() { + let invoice_proof = InvoiceProof::from_slate(&slate, 1, None)?; + invoice_proof.verify_promise_signature(&recipient_address)?; + } + Ok(()) +} + +/// Adds inputs and outputs to slate +pub fn add_outputs( + w: &mut WalletBackend, + keychain_mask: Option<&SecretKey>, + slate: &mut Slate, + context: &Context, +) -> Result<(), Error> +where + C: NodeClient, + K: Keychain, +{ + add_inputs_to_slate(w, keychain_mask, slate, context)?; + add_outputs_to_slate(w, keychain_mask, slate, context)?; + // Adjust the offset for the added input and outputs + let keychain = &w.keychain(keychain_mask)?; + slate.adjust_offset(keychain, &context)?; + + Ok(()) +} + +/// Contribute inputs to slate +fn add_inputs_to_slate( + w: &mut WalletBackend, + keychain_mask: Option<&SecretKey>, + slate: &mut Slate, + context: &Context, +) -> Result<(), Error> +where + C: NodeClient, + K: Keychain, +{ + debug!("contract::slate::add_inputs_to_slate => adding inputs to slate"); + let keychain = w.keychain(keychain_mask)?; + let batch = w.batch(keychain_mask)?; + for (key_id, mmr_index, _) in context.get_inputs() { + // We have no information if the input is a coinbase or not, so we fetch the data from DB + let coin = batch.get(&key_id, &mmr_index)?; + if coin.is_coinbase { + slate.add_transaction_elements( + &keychain, + &ProofBuilder::new(&keychain), + vec![build::coinbase_input(coin.value, coin.key_id.clone())], + )?; + debug!( + "contract::slate::add_inputs_to_slate => added coinbase input id: {}, value: {}", + coin.key_id.clone(), + coin.value + ); + } else { + slate.add_transaction_elements( + &keychain, + &ProofBuilder::new(&keychain), + vec![build::input(coin.value, coin.key_id.clone())], + )?; + debug!( + "contract::slate::add_inputs_to_slate => added regular input id: {}, value: {}", + coin.key_id.clone(), + coin.value + ); + } + } + + Ok(()) +} + +/// Contribute outputs to slate +fn add_outputs_to_slate( + w: &mut WalletBackend, + keychain_mask: Option<&SecretKey>, + slate: &mut Slate, + context: &Context, +) -> Result<(), Error> +where + C: NodeClient, + K: Keychain, +{ + debug!("contract::slate::add_outputs_to_slate => start"); + let keychain = w.keychain(keychain_mask)?; + // Iterate over outputs in the Context and add the same output to the slate + for (key_id, _, amount) in context.get_outputs() { + slate.add_transaction_elements( + &keychain, + &ProofBuilder::new(&keychain), + vec![build::output(amount, key_id.clone())], + )?; + debug!( + "contract::slate::add_outputs_to_slate => added output to slate. Output id: {}, amount: {}", + key_id.clone(), + amount + ); + } + + Ok(()) +} + +/// Transition the slate state to the next one +pub fn transition_state(slate: &mut Slate) -> Result<(), Error> { + // We don't really use these states right now apart from leaving it to derive expected net_change. + // This suggests these can't be used for manipulation. It doesn't hurt to think a bit more if that's the case. + let new_state = match slate.state { + SlateState::Invoice1 => SlateState::Invoice2, + SlateState::Invoice2 => SlateState::Invoice3, + SlateState::Standard1 => SlateState::Standard2, + SlateState::Standard2 => SlateState::Standard3, + // Unknown, or a slate that has already reached its final state, is not something + // we can advance. We have signed by this point and the wallet state is persisted + // next, so report it rather than reporting a Standard3 that never happened. + ref s => { + return Err(Error::GenericError(format!( + "Cannot advance a contract slate in state {}", + s + ))) + } + }; + slate.state = new_state; + // NOTE: It's possible to never reach the step3. A self-spend has only 2 steps: new -> sign. + Ok(()) +} + +/// Add partial signature to the slate. +// Nonce reuse is prevented by forgetting the signing context after a signed step: +// save_step deletes the private context (which holds sec_key/sec_nonce) once is_signed +// (except the deliberately-retained step2 context used for safe cancel). The context is +// keyed by slate.id, so a given nonce is only ever used for one message. +pub fn sign( + w: &mut WalletBackend, + keychain_mask: Option<&SecretKey>, + slate: &mut Slate, + context: &mut Context, +) -> Result<(), Error> +where + C: NodeClient, + K: Keychain, +{ + debug!("contract::slate::sign => called"); + // The counterparty controls the participant list, so require it to hold exactly the + // participants the slate declares before we sign over it. Setup has already added our + // own entry by this point. + if slate.participant_data.len() != slate.num_participants as usize { + return Err(Error::GenericError(format!( + "Expected {} participant(s) before signing, found {}", + slate.num_participants, + slate.participant_data.len() + ))); + } + let keychain = w.keychain(keychain_mask)?; + slate.fill_round_2(&keychain, &context.sec_key, &context.sec_nonce)?; + debug!( + "contract::sign => signed for slate fees: {}", + slate.fee_fields + ); + debug!("contract::slate::sign => done"); + + Ok(()) +} + +/// We can finalize if all partial sigs are present +pub fn can_finalize(slate: &Slate) -> bool { + let res = slate + .participant_data + .clone() + .into_iter() + .filter(|v| !v.is_complete()) + .count(); + + // We can finalize if the number of partial sigs is the same as the number of participants + res == 0 && slate.participant_data.len() == slate.num_participants as usize +} + +/// Finalize slate +pub fn finalize( + w: &mut WalletBackend, + keychain_mask: Option<&SecretKey>, + slate: &mut Slate, +) -> Result<(), Error> +where + C: NodeClient, + K: Keychain, +{ + debug!("contract::slate::finalize => called"); + // Final transaction can be built by anyone at this stage + trace!("Slate to finalize is: {}", slate); + // At this point, everyone adjusted their offset, so we update the offset on the tx + slate.tx_or_err_mut()?.offset = slate.offset.clone(); + slate.finalize(&w.keychain(keychain_mask)?)?; + + Ok(()) +} + +/// Perform 'setup' step for a contract. This adds our public key and nonce to the slate +/// The operation should be idempotent. +pub fn add_keys(slate: &mut Slate, keychain: &K, context: &mut Context) -> Result<(), Error> +where + K: Keychain, +{ + debug!("contract::slate::add_keys => called"); + let our_pub_key = PublicKey::from_secret_key(keychain.secp(), &context.sec_key)?; + let our_pub_nonce = PublicKey::from_secret_key(keychain.secp(), &context.sec_nonce)?; + // An entry is ours only when both keys match, which is what add_participant_info and + // fill_round_2 use. Our excess paired with a nonce that is not ours cannot have come + // from us, so reject it rather than treating the entry as ours: we would otherwise + // append a further participant and sign nothing. + if slate + .participant_data + .iter() + .any(|p| p.public_blind_excess == our_pub_key && p.public_nonce != our_pub_nonce) + { + return Err(Error::GenericError( + "Slate carries our public excess with a different nonce".to_string(), + )); + } + // Guard against a tampered slate that already carries the full participant set. + // Re-adding our own info is idempotent, so only block when we are not already in it. + let already_ours = slate + .participant_data + .iter() + .any(|p| p.public_blind_excess == our_pub_key && p.public_nonce == our_pub_nonce); + if !already_ours && slate.participant_data.len() >= slate.num_participants as usize { + return Err(Error::GenericError(format!( + "Slate already has the expected {} participant(s)", + slate.num_participants + ))); + } + slate.add_participant_info(keychain, context, None) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::grin_keychain::{ExtKeychain, SwitchCommitmentType}; + use crate::slate::ParticipantData; + + fn keychain_and_context() -> (ExtKeychain, Context) { + let keychain = ExtKeychain::from_random_seed(true).unwrap(); + let parent_key_id = ExtKeychain::derive_key_id(1, 0, 0, 0, 0); + let context = Context::new(keychain.secp(), &parent_key_id, true, true); + (keychain, context) + } + + #[test] + fn add_keys_rejects_our_excess_with_a_foreign_nonce() { + let (keychain, mut context) = keychain_and_context(); + let our_excess = PublicKey::from_secret_key(keychain.secp(), &context.sec_key).unwrap(); + let other_id = ExtKeychain::derive_key_id(1, 1, 0, 0, 0); + let other_key = keychain + .derive_key(0, &other_id, SwitchCommitmentType::Regular) + .unwrap(); + let other = PublicKey::from_secret_key(keychain.secp(), &other_key).unwrap(); + + let mut slate = Slate::blank(2, false); + slate.participant_data.push(ParticipantData { + public_blind_excess: our_excess, + public_nonce: other, + part_sig: None, + }); + assert!(add_keys(&mut slate, &keychain, &mut context).is_err()); + } + + #[test] + fn transition_state_rejects_states_it_cannot_advance() { + for state in [ + SlateState::Unknown, + SlateState::Standard3, + SlateState::Invoice3, + ] { + let mut slate = Slate::blank(2, false); + slate.state = state.clone(); + assert!( + transition_state(&mut slate).is_err(), + "expected {} to be rejected", + state + ); + } + + // The transitions the contract flows actually use are still accepted + for (from, to) in [ + (SlateState::Standard1, SlateState::Standard2), + (SlateState::Standard2, SlateState::Standard3), + (SlateState::Invoice1, SlateState::Invoice2), + (SlateState::Invoice2, SlateState::Invoice3), + ] { + let mut slate = Slate::blank(2, false); + slate.state = from; + transition_state(&mut slate).unwrap(); + assert_eq!(slate.state, to); + } + } +} diff --git a/libwallet/src/contract/types.rs b/libwallet/src/contract/types.rs new file mode 100644 index 000000000..06f6af707 --- /dev/null +++ b/libwallet/src/contract/types.rs @@ -0,0 +1,214 @@ +// 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. + +//! Types related to a contract + +use crate::error::Error; +use crate::slate_versions::ser as dalek_ser; +use ed25519_dalek::VerifyingKey as DalekPublicKey; + +/// Output selection args +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub struct OutputSelectionArgs { + /// Constraint on how many confirmations used inputs must have + pub min_input_confirmation: u64, + /// Which inputs we want to use - default to payjoin if available with Some("any") + pub use_inputs: Option, + /// Change output specification: explicit output amounts in nanogrin, not including + /// fee subtraction. e.g. [3, 1, 4, 0, 0] describes 5 outputs, two of which hold 0 value. + pub make_outputs: Option>, +} + +impl OutputSelectionArgs { + /// We try to make a payjoin if use_inputs has a value (either commitments or Some("any")) + pub fn is_payjoin(&self) -> bool { + self.use_inputs.is_some() + } + /// Return a list of commitments we must use + pub fn required_inputs(&self) -> Option> { + if self.use_inputs.is_some() { + Some( + self.use_inputs.as_ref().unwrap()[..] + .split(",") + .filter(|x| *x != "any") + .collect(), + ) + } else { + None + } + } + /// Returns the output amounts (nanogrin) we have to create. Amounts arrive already + /// parsed by the caller (e.g. the CLI), so this is just an accessor. + pub fn output_amounts(&self) -> Result, Error> { + Ok(self.make_outputs.clone().unwrap_or_default()) + } + /// Returns the sum of our output amounts + pub fn sum_output_amounts(&self) -> Result { + self.output_amounts()? + .iter() + .try_fold(0u64, |acc, v| acc.checked_add(*v)) + .ok_or_else(|| Error::GenericError("output amounts sum overflow".to_string())) + } + /// Returns the number of custom outputs + pub fn num_custom_outputs(&self) -> usize { + self.make_outputs.as_ref().map(|v| v.len()).unwrap_or(0) + } +} + +impl Default for OutputSelectionArgs { + fn default() -> OutputSelectionArgs { + OutputSelectionArgs { + min_input_confirmation: 10, + use_inputs: Some(String::from("any")), + make_outputs: None, + } + } +} + +/// Types of proof that can be generated +/// as per https://github.com/tromp/grin-rfcs/blob/early-payment-proofs/text/0000-early-payment-proofs.md +/// TODO: Update when RFC is merged + +#[derive(Clone, Serialize, Deserialize, Debug)] +pub enum ProofType { + /// Legacy (0x00) + Legacy, + /// Invoice (0x01, Default) + Invoice, + /// Sender Nonce (0x02) + SenderNonce, +} + +/// Proof generation parameters that can be provided during new or sign phases +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct ProofArgs { + /// If net change is positive during this step, whether to suppress the creation of payment proof + pub suppress_proof: bool, + /// Type of proof (Default 'Invoice') + pub proof_type: ProofType, + /// Sender address (required at some stage, may not necessarily be in slate so can be provided explicitly) + #[serde(with = "dalek_ser::option_dalek_pubkey_serde")] + pub sender_address: Option, +} + +impl Default for ProofArgs { + fn default() -> ProofArgs { + ProofArgs { + // Proofs are opt-in (#729): a `false` default made the receiver build an + // invoice promise with no sender address, failing with NoSenderAddressProvided. + suppress_proof: true, + proof_type: ProofType::Legacy, + sender_address: None, + } + } +} + +/// Contract Setup - defines how we pick inputs/outputs and what we expect from a contract. Both +/// 'new' and 'sign' actions perform a setup phase which is why their endpoints take these parameters. +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct ContractSetupArgsAPI { + /// The human readable account name from which to draw outputs + /// for the transaction, overriding whatever the active account is as set via the + /// [`set_active_account`](../grin_wallet_api/owner/struct.Owner.html#method.set_active_account) method. + pub src_acct_name: Option, + /// The net change we will agree on. The amount is in nanogrins (`1 G = 1_000_000_000nG`). + /// The value is positive when we are on the receiving end and negative when we are the sender. + /// It is optional because we could have agreed on it before we reach the sign e.g. when we create new contract + pub net_change: Option, + /// The number of participants in a contract. Used for computing our kernel fee contribution + pub num_participants: u8, + /// Should we perform an early lock of outputs + pub add_outputs: bool, + /// Output selection arguments + pub selection_args: OutputSelectionArgs, + /// Proof arguments + pub proof_args: ProofArgs, +} + +impl Default for ContractSetupArgsAPI { + fn default() -> ContractSetupArgsAPI { + ContractSetupArgsAPI { + src_acct_name: None, + net_change: None, + num_participants: 2, + add_outputs: false, + selection_args: OutputSelectionArgs { + ..Default::default() + }, + proof_args: ProofArgs::default(), + } + } +} + +/// Contract New +#[derive(Clone, Serialize, Deserialize)] +pub struct ContractNewArgsAPI { + /// Setup args - contract new also initiates the setup by default + pub setup_args: ContractSetupArgsAPI, +} + +impl Default for ContractNewArgsAPI { + fn default() -> ContractNewArgsAPI { + ContractNewArgsAPI { + setup_args: ContractSetupArgsAPI { + src_acct_name: None, + net_change: None, + num_participants: 2, + add_outputs: false, + selection_args: OutputSelectionArgs { + ..Default::default() + }, + proof_args: ProofArgs::default(), + }, + } + } +} + +/// ContractView +#[derive(Clone, Serialize, Deserialize)] +pub struct ContractView { + /// Every slatepack has a number of participants + pub num_participants: u8, + /// Suggested value for the party at step2 (only provided if slatepack is at step1) + pub suggested_net_change: Option, + /// Agreed net_change if we've agreed on it (the context must exist for this) + // NOTE: we drop the Context once we've signed. Perhaps we should think about dropping + // only the private keys associated with it to prevent double-signing with the same + // (pubkey, nonce) pair. This way, we'd retain the history on that wallet instance. + // There might also be value in forgetting the whole context. + pub agreed_net_change: Option, + /// Number of singatures on the contract + pub num_sigs: u8, + /// Has the contract been executed on chain + pub is_executed: bool, +} + +impl Default for ContractView { + fn default() -> ContractView { + ContractView { + num_participants: 2, + suggested_net_change: None, + agreed_net_change: None, + num_sigs: 0, + is_executed: false, + } + } +} + +/// Arguments for contract revoke function +#[derive(Clone, Serialize, Deserialize)] +pub struct ContractRevokeArgsAPI { + /// Tx id to cancel + pub tx_id: u32, +} diff --git a/libwallet/src/contract/utils.rs b/libwallet/src/contract/utils.rs new file mode 100644 index 000000000..b5eaf486c --- /dev/null +++ b/libwallet/src/contract/utils.rs @@ -0,0 +1,458 @@ +// 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. + +//! Contract building utility functions + +use crate::backend::WalletBackend; +use crate::contract::selection::verify_selection_consistency; +use crate::contract::types::ContractSetupArgsAPI; +use crate::grin_core::libtx::tx_fee; +use crate::grin_keychain::{Identifier, Keychain}; +use crate::grin_util::secp::key::SecretKey; +use crate::slate::Slate; +use crate::types::{Context, NodeClient, StoredProofInfo, TxLogEntryType}; +use crate::util::OnionV3Address; +use crate::{address, Error, OutputData, OutputStatus, TxLogEntry}; +use grin_core::core::FeeFields; +use uuid::Uuid; + +/// Creates an initial TxLogEntry without input/output or kernel information +pub fn create_tx_log_entry( + slate: &Slate, + net_change: i64, + parent_key_id: Identifier, + log_id: u32, +) -> Result { + let log_type = if slate.num_participants == 1 { + TxLogEntryType::TxSelfSpend + } else { + if net_change > 0 { + TxLogEntryType::TxReceived + } else { + TxLogEntryType::TxSent + } + }; + let mut t = TxLogEntry::new(parent_key_id.clone(), log_type, log_id); + // stored_tx is set in save_step, once we have signed and the transaction is written. + + t.tx_slate_id = Some(slate.id); + if net_change > 0 { + t.amount_credited = net_change as u64; + } else { + t.amount_debited = -net_change as u64; + } + t.ttl_cutoff_height = match slate.ttl_cutoff_height { + 0 => None, + n => Some(n), + }; + + Ok(t) +} + +/// Updates TxLogEntry for a contract with information available in the 'sign' step +pub fn update_tx_log_entry( + wallet: &mut WalletBackend, + keychain_mask: Option<&SecretKey>, + slate: &Slate, + context: &Context, + tx_log_entry: &mut TxLogEntry, +) -> Result<(), Error> +where + C: NodeClient, + K: Keychain, +{ + // This is expected to be called when we are signing the contract and have already contributed inputs & outputs + let keychain = wallet.keychain(keychain_mask)?; + let parent_key_id = context.parent_key_id.clone(); + let current_height = wallet.w2n_client().get_chain_tip()?.0; + // We have already contributed inputs and outputs so we know how much of each we contribute + tx_log_entry.num_outputs = context.output_ids.len(); + tx_log_entry.num_inputs = context.input_ids.len(); + tx_log_entry.fee = context.fee; + // Set kernel information + tx_log_entry.kernel_excess = Some(slate.calc_excess(keychain.secp())?); + tx_log_entry.kernel_lookup_min_height = Some(current_height); + + // If we're sending and there's payment proof info in the slate added by recipient, store as well + if let Some(ref p) = slate.payment_proof { + if tx_log_entry.amount_debited > 0 { + // note we only use a single path for now + let sender_address_path = 0u32; + let sender_key = address::address_from_derivation_path( + &keychain, + &parent_key_id, + sender_address_path, + )?; + let sender_address = OnionV3Address::from_private(&sender_key.0)?; + + // We're looking for the OTHER party here, the recipient. The 'xor 1' pairing + // only holds for a two-party slate, so check that before indexing: a slate + // with a shorter participant list would otherwise panic here. + let sender_index = slate.find_index_matching_context(&keychain, context)?; + if slate.participant_data.len() != 2 { + return Err(Error::GenericError(format!( + "Expected 2 participants for a payment proof, found {}", + slate.participant_data.len() + ))); + } + let recipient_index = sender_index ^ 1; + + tx_log_entry.payment_proof = Some(StoredProofInfo { + receiver_address: p.receiver_address, + receiver_signature: p.promise_signature, + sender_address: sender_address.to_ed25519()?, + sender_address_path, + sender_signature: None, + // Filled as separate steps for now; could be merged into a general case + // once we know which nonces here belong to the recipient. + proof_type: Some(1u8), + receiver_public_nonce: Some(slate.participant_data[recipient_index].public_nonce), + receiver_public_excess: Some( + slate.participant_data[recipient_index].public_blind_excess, + ), + timestamp: Some(p.timestamp), + memo: p.memo.clone(), + promise_signature: p.promise_signature, + sender_part_sig: slate.participant_data[sender_index].part_sig, + }); + } + } + + Ok(()) +} + +/// Get net_change value. This is obtained either from the Context.net_change or the setup_args.net_change +pub fn get_net_change( + w: &mut WalletBackend, + keychain_mask: Option<&SecretKey>, + slate_id: &Uuid, + setup_args_net_change: Option, +) -> Result +where + C: NodeClient, + K: Keychain, +{ + let mut expected_net_change: Option = setup_args_net_change; + match w.get_private_context(keychain_mask, slate_id.as_bytes()) { + Ok(ctx) => { + debug!("contract::sign => context found"); + // We have a context so we must have agreed on a certain net_change value in Context.net_change. + // If we have both Context.net_change and setup_args.net_change, then they must be equal. + let ctx_net_change = ctx.get_net_change()?; + match expected_net_change { + Some(args_net_change) => { + if ctx_net_change != args_net_change { + return Err(Error::GenericError(format!( + "Expected net change mismatch! Context.net_change: {}, setup_args.net_change: {}", + ctx_net_change, args_net_change + ))); + } + } + None => (), + } + expected_net_change = Some(ctx_net_change); + } + Err(Error::NotFoundErr(_)) => { + debug!("contract::utils::get_net_change => context not found") + } + Err(e) => return Err(e), + }; + + // Fail if net_change was not passed to setup_args and was also not present in the context. + // This means it has not been explicitly agreed on and we require the user to pass it. + if expected_net_change.is_none() { + return Err(Error::GenericError( + "You did not agree on the expected net difference.".into(), + ) + .into()); + } + debug!( + "contract::utils::get_net_change => expected_net_change: {}", + expected_net_change.unwrap() + ); + + Ok(expected_net_change.unwrap()) +} + +/// Atomically locks the inputs and saves the changes of Context, TxLogEntry and OutputData. +/// Additionally, the transaction is saved in a file in case we signed it. +pub fn save_step( + w: &mut WalletBackend, + keychain_mask: Option<&SecretKey>, + slate: &Slate, + context: &mut Context, + step_added_outputs: bool, + is_signed: bool, +) -> Result<(), Error> +where + C: NodeClient, + K: Keychain, +{ + debug!( + "contract::utils::save_step => performing atomic update for slate_id: {}", + slate.id + ); + // Phase 1 - precompute the data needed for atomic update + let parent_key_id = &context.parent_key_id; + let current_height = w.w2n_client().get_chain_tip()?.0; + // We are at step2 if we don't have context.log_id and we have signed the slate + let is_step2 = !context.log_id.is_some() && is_signed; + + let mut tx_log_entry = { + if !context.log_id.is_some() { + // We create a new entry with log_id=0 and but replace it with the real id before committing + create_tx_log_entry(slate, context.get_net_change()?, parent_key_id.clone(), 0)? + } else { + w.get_tx_log_entry(parent_key_id.clone(), context.log_id.unwrap())? + .unwrap() + } + }; + + // Update TxLogEntry if we have signed the contract (we have data about the kernel) + if is_signed { + update_tx_log_entry(w, keychain_mask, &slate, &context, &mut tx_log_entry)?; + // Record where the transaction is stored, as internal::selection does for a + // standard send. store_tx below writes it under this name, and 'txs' reads the + // field to report whether the transaction data is held. + tx_log_entry.stored_tx = Some(format!("{}.grintx", slate.id)); + } + // If we added outputs in this step, we have to create OutputData here because 'batch' + // takes the mutable ref and we can no longer call calc_commit_for_cache for output + let added_outputs = if !step_added_outputs { + vec![] + } else { + let mut output_data_xs: Vec = vec![]; + // Create an OutputData entry for every created output + for (key_id, _, amount) in context.get_outputs() { + let commit = w.calc_commit_for_cache(keychain_mask, amount, &key_id)?; + let output_data = OutputData { + root_key_id: parent_key_id.clone(), + key_id: key_id.clone(), + mmr_index: None, + n_child: key_id.to_path().last_path_index(), + commit: commit, + value: amount, + status: OutputStatus::Unconfirmed, + height: current_height, + lock_height: 0, + is_coinbase: false, + tx_log_entry: None, + }; + output_data_xs.push(output_data); + } + output_data_xs + }; + + // Phase 2 - atomically update Context, OutputData and TxLogEntry + let mut batch = w.batch(keychain_mask)?; + + // Update TxLogEntry + if !context.log_id.is_some() { + // If we just created the TxLogEntry, we have to assign it an id + let log_id = batch.next_tx_log_id(&parent_key_id)?; + tx_log_entry.id = log_id; + context.log_id = Some(log_id); + } + batch.save_tx_log_entry(tx_log_entry.clone(), &parent_key_id)?; + // Create OutputData entries and lock inputs if we added outputs at this step + if step_added_outputs { + // Create an OutputData entry for every created output + for mut output_data in added_outputs { + output_data.tx_log_entry = context.log_id; + batch.save(output_data)?; + } + // Lock inputs + for id in context.get_inputs() { + let mut coin = batch.get(&id.0, &id.1)?; + // At this point we already have context.log_id set + coin.tx_log_entry = context.log_id; + batch.lock_output(&mut coin)?; + } + } + + // Update context + if is_signed && !is_step2 { + // NOTE: We MUST forget the context when we sign. Ideally, these two would be atomic or perhaps + // when we call slate::sigadd_partial_signaturen we could swap the secret key with a temporary one just to be safe. + // The reason we don't delete if we are at step2 is because in case we want to do safe cancel, + // we need to know which inputs are in the context to know which input we have to double-spend. + batch.delete_private_context(slate.id.as_bytes())?; + } else { + batch.save_private_context(slate.id.as_bytes(), &context)?; + } + + batch.commit()?; + + // Defense in depth: once we have signed (and are past the step2 context that is kept + // deliberately for safe cancel), the signing context holding sec_key/sec_nonce must be + // gone so the nonce can never be reused. It is deleted in the is_signed && !is_step2 + // branch above; verify the deletion actually took effect. + if is_signed && !is_step2 { + if w.get_private_context(keychain_mask, slate.id.as_bytes()) + .is_ok() + { + return Err(Error::GenericError( + "signing context was not removed after signing".into(), + )); + } + } + + // Store the signed transaction only after the wallet-state batch has committed (and + // the signing context is confirmed gone), so a DB failure can't leave the stored tx + // out of sync with wallet state. Matches the core convention (internal::selection). + // + // store_tx writes a file outside LMDB, so this cannot be part of the batch above and + // a window remains: the write can fail with the tx log entry and the input locks + // already committed. Making it atomic would mean holding stored transactions in the + // database, which is how every transaction in the wallet is kept, not just contracts. + // Until then the way out is to cancel the transaction, which releases the inputs + // without reading the stored tx. Nothing has been broadcast at this point: the error + // returns to the caller before it posts. Covered by + // wallet_contract_self_spend_cancel_missing_stored_tx. + if is_signed { + w.store_tx(&format!("{}", slate.id), slate.tx_or_err()?)?; + } + debug!("contract::utils::save_step => Atomic updated done"); + + Ok(()) +} + +/// Computes fees contribution for a participant +pub fn my_fee_contribution( + n_inputs: usize, + n_outputs: usize, + n_kernels: usize, + num_participants: u8, +) -> Result { + // Add our fee costs for our inputs and a single output + let mut fee = tx_fee(n_inputs, n_outputs, 0); + // Add out fee costs for kernel. We pay 1/num_participants of a kernel cost + let kernel_cost = tx_fee(0, 0, n_kernels); + // Round each participant's kernel share up, so the participants together never underpay; + // the overpay is bounded to under one fee unit per participant. + let my_kernel_cost = (kernel_cost as f64 / (num_participants as f64)).ceil(); + fee += my_kernel_cost as u64; + + // Add my fee contribution to the slate total fee. Uses the standard FeeFields + // encoding; contracts rely on every participant applying the same + // 1/num_participants split so the contributions together cover the kernel cost. + let my_fee_fields = FeeFields::new(0, fee)?; + Ok(my_fee_fields) +} + +/// Returns an error if the slate has already been signed (in our local database). Even if the +/// result is Ok, it's still possible it was signed but we don't have the data about it locally. +pub fn verify_not_signed(w: &mut WalletBackend, slate_id: Uuid) -> Result<(), Error> +where + C: NodeClient, + K: Keychain, +{ + // If we have a transaction log entry for that slatepack that has a kernel value, then + // we have already signed this slate. + let tx = w + .tx_log_iter()? + .flatten() + .find(|t| t.tx_slate_id.is_some() && t.tx_slate_id.unwrap() == slate_id); + let already_signed = tx.is_some() && tx.unwrap().kernel_excess.is_some(); + if already_signed { + debug!("contract::utils::verify_not_signed => The slate has already been signed."); + return Err(Error::GenericError( + format!("Slate with id:{} has already been signed.", slate_id).into(), + ) + .into()); + } + + Ok(()) +} + +/// Compares the setup args provided at call with those in the Context and checks whether they conflict. +/// This is relevant to see if there's any conflict in the arguments provided at step1 with step3. +pub fn verify_setup_args_consistency( + ctx_setup_args: &ContractSetupArgsAPI, + cur_setup_args: &ContractSetupArgsAPI, +) -> Result<(), Error> { + // Compare net_change + if ctx_setup_args.net_change.unwrap() != cur_setup_args.net_change.unwrap() { + return Err(Error::GenericError(format!( + "Inconsistent net change. Ctx net_change:{}, Current net_change: {}", + ctx_setup_args.net_change.unwrap(), + cur_setup_args.net_change.unwrap() + ))); + } + // Compare num_participants + if ctx_setup_args.num_participants != cur_setup_args.num_participants { + return Err(Error::GenericError(format!( + "Inconsistent num_participants. Ctx num_participants:{}, Current num_participants: {}", + ctx_setup_args.num_participants, cur_setup_args.num_participants + ))); + } + // add_outputs is intentionally forced true at the sign step (late lock), so it is not + // part of this consistency check. parent_key_id is taken from the stored Context, so + // later steps always derive under the contract's account regardless of the active one. + + // Compare OutputSelectionArgs + verify_selection_consistency( + &ctx_setup_args.selection_args, + &cur_setup_args.selection_args, + )?; + Ok(()) +} + +/// Get the parent_key_id for a given wallet instance and src_acct_name. +/// Errors on an unknown account name rather than silently falling back to the +/// active account. +pub fn parent_key_for( + w: &mut WalletBackend, + src_acct_name: Option<&String>, +) -> Result +where + C: NodeClient, + K: Keychain, +{ + let parent_key_id = match src_acct_name { + Some(d) => match w.get_acct_path(d.clone())? { + Some(p) => p.path, + None => return Err(Error::UnknownAccountLabel(d.clone())), + }, + None => w.parent_key_id(), + }; + Ok(parent_key_id) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn fee_contribution_kernel_split() { + let (n_inputs, n_outputs, n_kernels) = (1, 1, 1); + let base = tx_fee(n_inputs, n_outputs, 0); + let kernel_cost = tx_fee(0, 0, n_kernels); + + // A single participant (self-spend) pays the whole kernel cost. + let solo = my_fee_contribution(n_inputs, n_outputs, n_kernels, 1) + .unwrap() + .fee(); + assert_eq!(solo, base + kernel_cost); + + // Two participants each pay ceil(kernel_cost / 2) ... + let half = my_fee_contribution(n_inputs, n_outputs, n_kernels, 2) + .unwrap() + .fee(); + let my_share = ((kernel_cost as f64) / 2.0).ceil() as u64; + assert_eq!(half, base + my_share); + // ... and applying the same split, the participants together cover the kernel cost. + assert!(2 * my_share >= kernel_cost); + } +} diff --git a/libwallet/src/error.rs b/libwallet/src/error.rs index d5a9f9cd1..1863d2dee 100644 --- a/libwallet/src/error.rs +++ b/libwallet/src/error.rs @@ -65,6 +65,14 @@ pub enum Error { #[error("Onion V3 Address Error: {0}")] OnionV3Address(#[from] util::OnionV3AddressError), + /// Comsig error + #[error("Comsig error: {0}")] + ComSig(#[from] crate::mwixnet::ComSigError), + + /// mwixnet Onion error + #[error("Onion error: {0}")] + Onion(#[from] crate::mwixnet::OnionError), + /// Callback implementation error conversion #[error("Trait Implementation error")] CallbackImpl(&'static str), @@ -73,6 +81,10 @@ pub enum Error { #[error("Wallet store error: {0}")] Backend(String), + /// Requested item was not found in the wallet store + #[error("Not found: {0}")] + NotFoundErr(String), + /// Callback implementation error conversion #[error("Restore Error")] Restore, @@ -269,10 +281,18 @@ pub enum Error { #[error("Payment Proof parsing error: {0}")] PaymentProofParsing(String), + /// Retrieving Payment Proof + #[error("Unable to verify payment proof: {0}")] + PaymentProofValidation(String), + /// Decoding OnionV3 addresses to payment proof addresses #[error("Proof Address decoding: {0}")] AddressDecoding(String), + /// Payment proof - no sender address provided or found in slate + #[error("Sender address has not been provided")] + NoSenderAddressProvided, + /// Transaction has expired it's TTL #[error("Transaction Expired")] TransactionExpired, @@ -321,14 +341,29 @@ pub enum Error { #[error("Stored Tx error: {0}")] StoredTx(String), + /// Trying to match index to context + #[error("Cannot match transaction context to slate index")] + ContextToIndex, + /// Other #[error("Generic error: {0}")] GenericError(String), } +impl From for Error { + fn from(error: grin_wallet_config::ConfigError) -> Error { + Error::GenericError(format!("{}", error)) + } +} + impl From for Error { fn from(error: grin_store::Error) -> Error { - Error::Backend(format!("{}", error)) + // Preserve "not found" so callers can distinguish a missing item from other + // store errors; everything else collapses to a generic backend error. + match error { + grin_store::Error::NotFoundErr(s) => Error::NotFoundErr(s), + other => Error::Backend(format!("{}", other)), + } } } diff --git a/libwallet/src/internal/keys.rs b/libwallet/src/internal/keys.rs index 9d8f4cb27..21c937a88 100644 --- a/libwallet/src/internal/keys.rs +++ b/libwallet/src/internal/keys.rs @@ -16,32 +16,31 @@ use crate::error::Error; use crate::grin_keychain::{ChildNumber, ExtKeychain, Identifier, Keychain}; use crate::grin_util::secp::key::SecretKey; -use crate::types::{AcctPathMapping, NodeClient, WalletBackend}; +use crate::types::{AcctPathMapping, NodeClient}; +use crate::WalletBackend; /// Get next available key in the wallet for a given parent -pub fn next_available_key<'a, T: ?Sized, C, K>( - wallet: &mut T, +pub fn next_available_key( + wallet: &mut WalletBackend, keychain_mask: Option<&SecretKey>, ) -> Result where - T: WalletBackend<'a, C, K>, - C: NodeClient + 'a, - K: Keychain + 'a, + C: NodeClient, + K: Keychain, { let child = wallet.next_child(keychain_mask)?; Ok(child) } /// Retrieve an existing key from a wallet -pub fn retrieve_existing_key<'a, T: ?Sized, C, K>( - wallet: &T, +pub fn retrieve_existing_key( + wallet: &WalletBackend, key_id: Identifier, mmr_index: Option, ) -> Result<(Identifier, u32), Error> where - T: WalletBackend<'a, C, K>, - C: NodeClient + 'a, - K: Keychain + 'a, + C: NodeClient, + K: Keychain, { let existing = wallet.get(&key_id, &mmr_index)?; let key_id = existing.key_id.clone(); @@ -50,28 +49,26 @@ where } /// Returns a list of account to BIP32 path mappings -pub fn accounts<'a, T: ?Sized, C, K>(wallet: &mut T) -> Result, Error> +pub fn accounts(wallet: &WalletBackend) -> Result, Error> where - T: WalletBackend<'a, C, K>, - C: NodeClient + 'a, - K: Keychain + 'a, + C: NodeClient, + K: Keychain, { - Ok(wallet.acct_path_iter().collect()) + Ok(wallet.acct_path_iter()?.collect()) } -/// Adds an new parent account path with a given label -pub fn new_acct_path<'a, T: ?Sized, C, K>( - wallet: &mut T, +/// Adds a new parent account path with a given label +pub fn new_acct_path( + wallet: &mut WalletBackend, keychain_mask: Option<&SecretKey>, label: &str, ) -> Result where - T: WalletBackend<'a, C, K>, - C: NodeClient + 'a, - K: Keychain + 'a, + C: NodeClient, + K: Keychain, { let label = label.to_owned(); - if wallet.acct_path_iter().any(|l| l.label == label) { + if wallet.acct_path_iter()?.any(|l| l.label == label) { return Err(Error::AccountLabelAlreadyExists(label)); } @@ -79,7 +76,7 @@ where // so find the highest of those, then increment (to conform with external/internal // derivation chains in BIP32 spec) - let highest_entry = wallet.acct_path_iter().max_by(|a, b| { + let highest_entry = wallet.acct_path_iter()?.max_by(|a, b| { ::from(a.path.to_path().path[0]).cmp(&::from(b.path.to_path().path[0])) }); @@ -94,7 +91,7 @@ where }; let save_path = AcctPathMapping { - label: label, + label, path: return_id.clone(), }; @@ -105,20 +102,19 @@ where } /// Adds/sets a particular account path with a given label -pub fn set_acct_path<'a, T: ?Sized, C, K>( - wallet: &mut T, +pub fn set_acct_path( + wallet: &mut WalletBackend, keychain_mask: Option<&SecretKey>, label: &str, path: &Identifier, ) -> Result<(), Error> where - T: WalletBackend<'a, C, K>, - C: NodeClient + 'a, - K: Keychain + 'a, + C: NodeClient, + K: Keychain, { let label = label.to_owned(); let save_path = AcctPathMapping { - label: label, + label, path: path.clone(), }; diff --git a/libwallet/src/internal/scan.rs b/libwallet/src/internal/scan.rs index 129e99caa..85f0e9aa9 100644 --- a/libwallet/src/internal/scan.rs +++ b/libwallet/src/internal/scan.rs @@ -66,14 +66,14 @@ struct RestoredTxStats { pub num_outputs: usize, } -fn identify_utxo_outputs<'a, K>( +fn identify_utxo_outputs( keychain: &K, outputs: Vec<(pedersen::Commitment, pedersen::RangeProof, bool, u64, u64)>, status_send_channel: &Option>, percentage_complete: u8, ) -> Result, Error> where - K: Keychain + 'a, + K: Keychain, { let mut wallet_outputs: Vec = Vec::new(); @@ -137,7 +137,7 @@ where n_child: key_id.to_path().last_path_index(), value: amount, height: *height, - lock_height: lock_height, + lock_height, is_coinbase: *is_coinbase, mmr_index: *mmr_index, }); @@ -145,7 +145,7 @@ where Ok(wallet_outputs) } -fn collect_chain_outputs_rewind_hash<'a, C>( +fn collect_chain_outputs_rewind_hash( client: C, rewind_hash: String, start_index: u64, @@ -153,13 +153,13 @@ fn collect_chain_outputs_rewind_hash<'a, C>( status_send_channel: &Option>, ) -> Result where - C: NodeClient + 'a, + C: NodeClient, { let batch_size = 1000; let start_index_stat = start_index; let mut start_index = start_index; let mut vw = ViewWallet { - rewind_hash: rewind_hash, + rewind_hash, output_result: vec![], total_balance: 0, last_pmmr_index: 0, @@ -198,7 +198,7 @@ where continue; } - let info = info.unwrap(); + let info = info?; vw.total_balance += info.value; let lock_height = if *is_coinbase { *height + global::coinbase_maturity() @@ -212,7 +212,7 @@ where height: *height, mmr_index: *mmr_index, is_coinbase: *is_coinbase, - lock_height: lock_height, + lock_height, }; vw.output_result.push(output_info); @@ -229,26 +229,30 @@ where fn collect_chain_outputs<'a, C, K>( keychain: &K, client: C, + batch_start_index: u64, + batch_end_index: u64, start_index: u64, - end_index: Option, + end_index: u64, status_send_channel: &Option>, -) -> Result<(Vec, u64), Error> +) -> Result<(Vec, u64, u8), Error> where C: NodeClient + 'a, K: Keychain + 'a, { let batch_size = 1000; let start_index_stat = start_index; - let mut start_index = start_index; + let mut start_index = batch_start_index; let mut result_vec: Vec = vec![]; let last_retrieved_return_index; + + let mut perc_complete; loop { let (highest_index, last_retrieved_index, outputs) = - client.get_outputs_by_pmmr_index(start_index, end_index, batch_size)?; + client.get_outputs_by_pmmr_index(start_index, Some(batch_end_index), batch_size)?; - let range = highest_index as f64 - start_index_stat as f64; + let range = end_index as f64 - start_index_stat as f64; let progress = last_retrieved_index as f64 - start_index_stat as f64; - let perc_complete = cmp::min(((progress / range) * 100.0) as u8, 99); + perc_complete = cmp::min(((progress / range) * 100.0) as u8, 99); let msg = format!( "Checking {} outputs, up to index {}. (Highest index: {})", @@ -264,19 +268,23 @@ where keychain, outputs.clone(), status_send_channel, - perc_complete as u8, + perc_complete, )?); + debug!( + "collect_chain_outputs: start_index {}, last_pmmr_index {}", + start_index, last_retrieved_index + ); + if highest_index <= last_retrieved_index { last_retrieved_return_index = last_retrieved_index; break; } start_index = last_retrieved_index + 1; } - Ok((result_vec, last_retrieved_return_index)) + Ok((result_vec, last_retrieved_return_index, perc_complete)) } -/// fn restore_missing_output<'a, L, C, K>( wallet_inst: Arc>>>, keychain_mask: Option<&SecretKey>, @@ -375,7 +383,7 @@ where wallet_lock!(wallet_inst, w); let updated_tx_entry = if output.tx_log_entry.is_some() { let entries = updater::retrieve_txs( - &mut **w, + w, output.tx_log_entry, None, None, @@ -451,17 +459,30 @@ where Ok(chain_outs) } -/// Check / repair wallet contents by scanning against chain -/// assume wallet contents have been freshly updated with contents -/// of latest block +/// Scan chain outputs and repair the wallet state where needed. +/// +/// This is the low-level worker used by the owner API. Callers should normally +/// use the owner scan/update methods instead of calling this directly, unless +/// they need to drive a scan in batches and save progress between those batches. +/// +/// `batch_start_height` and `batch_end_height` describe the part of the chain +/// scanned by this call. `start_height` and `end_height` describe the full scan +/// range, so progress and PMMR bounds still reflect the whole scan. +/// +/// The returned `ScannedBlockInfo` is the progress marker to persist after the +/// batch. The returned PMMR range can be passed into the next batch to avoid +/// looking up the full range again. pub fn scan<'a, L, C, K>( wallet_inst: Arc>>>, keychain_mask: Option<&SecretKey>, delete_unconfirmed: bool, + batch_start_height: u64, + batch_end_height: u64, start_height: u64, end_height: u64, + total_pmmr_range: Option<(u64, u64)>, status_send_channel: &Option>, -) -> Result +) -> Result<(ScannedBlockInfo, (u64, u64)), Error> where L: WalletLCProvider<'a, C, K>, C: NodeClient + 'a, @@ -469,36 +490,52 @@ where { // First, get a definitive list of outputs we own from the chain if let Some(ref s) = status_send_channel { - let _ = s.send(StatusMessage::Scanning("Starting UTXO scan".to_owned(), 0)); + if start_height == batch_start_height { + let _ = s.send(StatusMessage::Scanning("Starting UTXO scan".to_owned(), 0)); + } } let (client, keychain) = { wallet_lock!(wallet_inst, w); (w.w2n_client().clone(), w.keychain(keychain_mask)?) }; - // Retrieve the actual PMMR index range we're looking for - let pmmr_range = client.height_range_to_pmmr_indices(start_height, Some(end_height))?; + // Retrieve total PMMR index range we're looking for + let total_pmmr_range = if let Some(total_range) = total_pmmr_range { + total_range + } else { + client.height_range_to_pmmr_indices(start_height, Some(end_height))? + }; - let (chain_outs, last_index) = collect_chain_outputs( + // Retrieve current batch PMMR index range + let pmmr_range = + client.height_range_to_pmmr_indices(batch_start_height, Some(batch_end_height))?; + + debug!( + "scan: from: {}, to: {}", + batch_start_height, batch_end_height + ); + + let (chain_outs, last_index, perc_complete) = collect_chain_outputs( &keychain, client, pmmr_range.0, - Some(pmmr_range.1), + pmmr_range.1, + total_pmmr_range.0, + total_pmmr_range.1, status_send_channel, )?; let msg = format!( "Identified {} wallet_outputs as belonging to this wallet", chain_outs.len(), ); - if let Some(ref s) = status_send_channel { - let _ = s.send(StatusMessage::Scanning(msg, 99)); + let _ = s.send(StatusMessage::Scanning(msg, perc_complete)); } // Now, get all outputs owned by this wallet (regardless of account) let wallet_outputs = { wallet_lock!(wallet_inst, w); - updater::retrieve_outputs(&mut **w, keychain_mask, true, None, None)? + updater::retrieve_outputs(w, keychain_mask, true, None, None)? }; let mut missing_outs = vec![]; @@ -530,7 +567,7 @@ where o.value, o.key_id, m.1.commit, ); if let Some(ref s) = status_send_channel { - let _ = s.send(StatusMessage::Scanning(msg, 99)); + let _ = s.send(StatusMessage::Scanning(msg, perc_complete)); } o.status = OutputStatus::Unspent; // any transactions associated with this should be cancelled @@ -551,7 +588,7 @@ where m.value, m.key_id, m.commit, m.mmr_index ); if let Some(ref s) = status_send_channel { - let _ = s.send(StatusMessage::Scanning(msg, 99)); + let _ = s.send(StatusMessage::Scanning(msg, perc_complete)); } restore_missing_output( wallet_inst.clone(), @@ -572,7 +609,7 @@ where o.value, o.key_id, m.1.commit, ); if let Some(ref s) = status_send_channel { - let _ = s.send(StatusMessage::Scanning(msg, 99)); + let _ = s.send(StatusMessage::Scanning(msg, perc_complete)); } o.status = OutputStatus::Unspent; cancel_tx_log_entry(wallet_inst.clone(), keychain_mask, &o)?; @@ -595,7 +632,7 @@ where o.value, o.key_id, m.commit, ); if let Some(ref s) = status_send_channel { - let _ = s.send(StatusMessage::Scanning(msg, 99)); + let _ = s.send(StatusMessage::Scanning(msg, perc_complete)); } cancel_tx_log_entry(wallet_inst.clone(), keychain_mask, &o)?; wallet_lock!(wallet_inst, w); @@ -608,7 +645,7 @@ where // restore labels, account paths and child derivation indices wallet_lock!(wallet_inst, w); let label_base = "account"; - let accounts: Vec = w.acct_path_iter().map(|m| m.path).collect(); + let accounts: Vec = w.acct_path_iter()?.map(|m| m.path).collect(); let mut acct_index = accounts.len(); for (path, max_child_index) in found_parents.iter() { // Only restore paths that don't exist @@ -616,9 +653,9 @@ where let label = format!("{}_{}", label_base, acct_index); let msg = format!("Setting account {} at path {}", label, path); if let Some(ref s) = status_send_channel { - let _ = s.send(StatusMessage::Scanning(msg, 99)); + let _ = s.send(StatusMessage::Scanning(msg, perc_complete)); } - keys::set_acct_path(&mut **w, keychain_mask, &label, path)?; + keys::set_acct_path(w, keychain_mask, &label, path)?; acct_index += 1; } let current_child_index = w.current_child_index(&path)?; @@ -631,15 +668,20 @@ where } if let Some(ref s) = status_send_channel { - let _ = s.send(StatusMessage::ScanningComplete( - "Scanning Complete".to_owned(), - )); + if end_height == batch_end_height { + let _ = s.send(StatusMessage::ScanningComplete( + "Scanning Complete".to_owned(), + )); + } } - Ok(ScannedBlockInfo { - height: end_height, - hash: "".to_owned(), - start_pmmr_index: pmmr_range.0, - last_pmmr_index: last_index, - }) + Ok(( + ScannedBlockInfo { + height: batch_end_height, + hash: "".to_owned(), + start_pmmr_index: pmmr_range.0, + last_pmmr_index: last_index, + }, + total_pmmr_range, + )) } diff --git a/libwallet/src/internal/selection.rs b/libwallet/src/internal/selection.rs index 5bc45005f..75d298d61 100644 --- a/libwallet/src/internal/selection.rs +++ b/libwallet/src/internal/selection.rs @@ -14,7 +14,6 @@ //! Selection of inputs for building transactions -use crate::address; use crate::error::Error; use crate::grin_core::core::amount_to_hr_string; use crate::grin_core::libtx::{ @@ -29,6 +28,7 @@ use crate::internal::keys; use crate::slate::Slate; use crate::types::*; use crate::util::OnionV3Address; +use crate::{address, WalletBackend}; use std::collections::HashMap; use std::convert::TryInto; @@ -37,8 +37,8 @@ use std::convert::TryInto; /// and saves the private wallet identifiers of our selected outputs /// into our transaction context -pub fn build_send_tx<'a, T: ?Sized, C, K>( - wallet: &mut T, +pub fn build_send_tx( + wallet: &mut WalletBackend, keychain: &K, keychain_mask: Option<&SecretKey>, slate: &mut Slate, @@ -54,9 +54,8 @@ pub fn build_send_tx<'a, T: ?Sized, C, K>( amount_includes_fee: bool, ) -> Result where - T: WalletBackend<'a, C, K>, - C: NodeClient + 'a, - K: Keychain + 'a, + C: NodeClient, + K: Keychain, { let (elems, inputs, change_amounts_derivations, fee) = select_send_tx( wallet, @@ -73,7 +72,7 @@ where )?; if amount_includes_fee { slate.amount = slate.amount.checked_sub(fee).ok_or(Error::GenericError( - format!("Transaction amount is too small to include fee").into(), + "Transaction amount is too small to include fee".to_string(), ))?; }; @@ -119,8 +118,8 @@ where /// Locks all corresponding outputs in the context, creates /// change outputs and tx log entry -pub fn lock_tx_context<'a, T: ?Sized, C, K>( - wallet: &mut T, +pub fn lock_tx_context( + wallet: &mut WalletBackend, keychain_mask: Option<&SecretKey>, slate: &Slate, current_height: u64, @@ -128,9 +127,8 @@ pub fn lock_tx_context<'a, T: ?Sized, C, K>( excess_override: Option, ) -> Result<(), Error> where - T: WalletBackend<'a, C, K>, - C: NodeClient + 'a, - K: Keychain + 'a, + C: NodeClient, + K: Keychain, { let mut output_commits: HashMap, u64)> = HashMap::new(); // Store cached commits before locking wallet @@ -159,6 +157,7 @@ where let log_id = batch.next_tx_log_id(&parent_key_id)?; let mut t = TxLogEntry::new(parent_key_id.clone(), TxLogEntryType::TxSent, log_id); t.tx_slate_id = Some(slate_id); + t.tx_slate_state = Some(slate.state.clone()); let filename = format!("{}.grintx", slate_id); t.stored_tx = Some(filename); t.fee = context.fee; @@ -178,7 +177,7 @@ where let mut amount_debited = 0; t.num_inputs = lock_inputs.len(); for id in lock_inputs { - let mut coin = batch.get(&id.0, &id.1).unwrap(); + let mut coin = batch.get(&id.0, &id.1)?; coin.tx_log_entry = Some(log_id); amount_debited += coin.value; batch.lock_output(&mut coin)?; @@ -203,12 +202,23 @@ where sender_address_path, )?; let sender_address = OnionV3Address::from_private(&sender_key.0)?; + t.payment_proof = Some(StoredProofInfo { receiver_address: p.receiver_address, - receiver_signature: p.receiver_signature, + receiver_signature: p.promise_signature, sender_address: sender_address.to_ed25519()?, sender_address_path, sender_signature: None, + // TODO: Will fill these as separate steps for now, check whether this + // can be merged in a general case (which means knowing which nonces here belong to + // the recipient) + proof_type: None, + receiver_public_nonce: None, + receiver_public_excess: None, + timestamp: None, + memo: None, + promise_signature: None, + sender_part_sig: None, }); }; @@ -221,11 +231,11 @@ where root_key_id: parent_key_id.clone(), key_id: id.clone(), n_child: id.to_path().last_path_index(), - commit: commit, + commit, mmr_index: None, value: change_amount, status: OutputStatus::Unconfirmed, - height: height, + height, lock_height: 0, is_coinbase: false, tx_log_entry: Some(log_id), @@ -245,8 +255,8 @@ where /// Creates a new output in the wallet for the recipient, /// returning the key of the fresh output /// Also creates a new transaction containing the output -pub fn build_recipient_output<'a, T: ?Sized, C, K>( - wallet: &mut T, +pub fn build_recipient_output( + wallet: &mut WalletBackend, keychain_mask: Option<&SecretKey>, slate: &mut Slate, current_height: u64, @@ -255,9 +265,8 @@ pub fn build_recipient_output<'a, T: ?Sized, C, K>( is_initiator: bool, ) -> Result<(Identifier, Context, TxLogEntry), Error> where - T: WalletBackend<'a, C, K>, - C: NodeClient + 'a, - K: Keychain + 'a, + C: NodeClient, + K: Keychain, { // Create a potential output for this transaction let key_id = keys::next_available_key(wallet, keychain_mask).unwrap(); @@ -284,6 +293,7 @@ where let log_id = batch.next_tx_log_id(&parent_key_id)?; let mut t = TxLogEntry::new(parent_key_id.clone(), TxLogEntryType::TxReceived, log_id); t.tx_slate_id = Some(slate_id); + t.tx_slate_state = Some(slate.state.clone()); t.amount_credited = amount; t.num_outputs = 1; t.ttl_cutoff_height = match slate.ttl_cutoff_height { @@ -300,10 +310,10 @@ where key_id: key_id_inner.clone(), mmr_index: None, n_child: key_id_inner.to_path().last_path_index(), - commit: commit, + commit, value: amount, status: OutputStatus::Unconfirmed, - height: height, + height, lock_height: 0, is_coinbase: false, tx_log_entry: Some(log_id), @@ -317,8 +327,8 @@ where /// Builds a transaction to send to someone from the HD seed associated with the /// wallet and the amount to send. Handles reading through the wallet data file, /// selecting outputs to spend and building the change. -pub fn select_send_tx<'a, T: ?Sized, C, K, B>( - wallet: &mut T, +pub fn select_send_tx( + wallet: &mut WalletBackend, keychain_mask: Option<&SecretKey>, amount: u64, amount_includes_fee: bool, @@ -339,9 +349,8 @@ pub fn select_send_tx<'a, T: ?Sized, C, K, B>( Error, > where - T: WalletBackend<'a, C, K>, - C: NodeClient + 'a, - K: Keychain + 'a, + C: NodeClient, + K: Keychain, B: ProofBuild, { let (coins, _total, amount, fee) = select_coins_and_fee( @@ -371,8 +380,8 @@ where } /// Select outputs and calculating fee. -pub fn select_coins_and_fee<'a, T: ?Sized, C, K>( - wallet: &mut T, +pub fn select_coins_and_fee( + wallet: &mut WalletBackend, amount: u64, amount_includes_fee: bool, current_height: u64, @@ -391,9 +400,8 @@ pub fn select_coins_and_fee<'a, T: ?Sized, C, K>( Error, > where - T: WalletBackend<'a, C, K>, - C: NodeClient + 'a, - K: Keychain + 'a, + C: NodeClient, + K: Keychain, { // select some spendable coins from the wallet let (max_outputs, mut coins) = select_coins( @@ -404,10 +412,10 @@ where max_outputs, selection_strategy_is_use_all, parent_key_id, - ); + )?; // sender is responsible for setting the fee on the partial tx - // recipient should double check the fee calculation and not blindly trust the + // recipient should double-check the fee calculation and not blindly trust the // sender // First attempt to spend without change @@ -422,8 +430,8 @@ where return Err(Error::NotEnoughFunds { available: 0, available_disp: amount_to_hr_string(0, false), - needed: amount_with_fee as u64, - needed_disp: amount_to_hr_string(amount_with_fee as u64, false), + needed: amount_with_fee, + needed_disp: amount_to_hr_string(amount_with_fee, false), }); } @@ -432,8 +440,8 @@ where return Err(Error::NotEnoughFunds { available: total, available_disp: amount_to_hr_string(total, false), - needed: amount_with_fee as u64, - needed_disp: amount_to_hr_string(amount_with_fee as u64, false), + needed: amount_with_fee, + needed_disp: amount_to_hr_string(amount_with_fee, false), }); } @@ -453,10 +461,10 @@ where // End the loop if we have selected all the outputs and still not enough funds if coins.len() == max_outputs { return Err(Error::NotEnoughFunds { - available: total as u64, + available: total, available_disp: amount_to_hr_string(total, false), - needed: amount_with_fee as u64, - needed_disp: amount_to_hr_string(amount_with_fee as u64, false), + needed: amount_with_fee, + needed_disp: amount_to_hr_string(amount_with_fee, false), }); } @@ -469,7 +477,7 @@ where max_outputs, selection_strategy_is_use_all, parent_key_id, - ) + )? .1; fee = tx_fee(coins.len(), num_outputs, 1); total = coins.iter().map(|c| c.value).sum(); @@ -483,7 +491,7 @@ where // be reduced, to accommodate the fee. let new_amount = match amount_includes_fee { true => amount.checked_sub(fee).ok_or(Error::GenericError( - format!("Transaction amount is too small to include fee").into(), + "Transaction amount is too small to include fee".to_string(), ))?, false => amount, }; @@ -491,9 +499,9 @@ where } /// Selects inputs and change for a transaction -pub fn inputs_and_change<'a, T: ?Sized, C, K, B>( +pub fn inputs_and_change( coins: &[OutputData], - wallet: &mut T, + wallet: &mut WalletBackend, keychain_mask: Option<&SecretKey>, amount: u64, fee: u64, @@ -507,9 +515,8 @@ pub fn inputs_and_change<'a, T: ?Sized, C, K, B>( Error, > where - T: WalletBackend<'a, C, K>, - C: NodeClient + 'a, - K: Keychain + 'a, + C: NodeClient, + K: Keychain, B: ProofBuild, { let mut parts = vec![]; @@ -554,7 +561,7 @@ where part_change }; - let change_key = wallet.next_child(keychain_mask).unwrap(); + let change_key = wallet.next_child(keychain_mask)?; change_amounts_derivations.push((change_amount, change_key.clone(), None)); parts.push(build::output(change_amount, change_key)); @@ -569,26 +576,23 @@ where /// max_outputs). Alternative strategy is to spend smallest outputs first /// but only as many as necessary. When we introduce additional strategies /// we should pass something other than a bool in. -/// TODO: Possibly move this into another trait to be owned by a wallet? - -pub fn select_coins<'a, T: ?Sized, C, K>( - wallet: &mut T, +pub fn select_coins( + wallet: &WalletBackend, amount: u64, current_height: u64, minimum_confirmations: u64, max_outputs: usize, select_all: bool, parent_key_id: &Identifier, -) -> (usize, Vec) +) -> Result<(usize, Vec), Error> // max_outputs_available, Outputs where - T: WalletBackend<'a, C, K>, - C: NodeClient + 'a, - K: Keychain + 'a, + C: NodeClient, + K: Keychain, { // first find all eligible outputs based on number of confirmations let mut eligible = wallet - .iter() + .iter()? .filter(|out| { out.root_key_id == *parent_key_id && out.eligible_to_spend(current_height, minimum_confirmations) @@ -612,7 +616,7 @@ where for window in eligible.windows(max_outputs) { let windowed_eligibles = window.to_vec(); if let Some(outputs) = select_from(amount, select_all, windowed_eligibles) { - return (max_available, outputs); + return Ok((max_available, outputs)); } } // Not exist in any window of which total amount >= amount. @@ -623,20 +627,20 @@ where "Extending maximum number of outputs. {} outputs selected.", outputs.len() ); - return (max_available, outputs); + return Ok((max_available, outputs)); } } else if let Some(outputs) = select_from(amount, select_all, eligible.clone()) { - return (max_available, outputs); + return Ok((max_available, outputs)); } // we failed to find a suitable set of outputs to spend, // so return the largest amount we can so we can provide guidance on what is // possible eligible.reverse(); - ( + Ok(( max_available, eligible.iter().take(max_outputs).cloned().collect(), - ) + )) } fn select_from(amount: u64, select_all: bool, outputs: Vec) -> Option> { @@ -663,21 +667,20 @@ fn select_from(amount: u64, select_all: bool, outputs: Vec) -> Optio } } -/// Repopulates output in the slate's tranacstion +/// Repopulates output in the slate's transaction /// with outputs from the stored context /// change outputs and tx log entry /// Remove the explicitly stored excess -pub fn repopulate_tx<'a, T: ?Sized, C, K>( - wallet: &mut T, +pub fn repopulate_tx( + wallet: &mut WalletBackend, keychain_mask: Option<&SecretKey>, slate: &mut Slate, context: &Context, update_fee: bool, ) -> Result<(), Error> where - T: WalletBackend<'a, C, K>, - C: NodeClient + 'a, - K: Keychain + 'a, + C: NodeClient, + K: Keychain, { // restore the original amount, fee slate.amount = context.amount; @@ -694,7 +697,7 @@ where let mut parts = vec![]; for (id, _, value) in &context.get_inputs() { - let input = wallet.iter().find(|out| out.key_id == *id); + let input = wallet.iter()?.find(|out| out.key_id == *id); if let Some(i) = input { if i.is_coinbase { parts.push(build::coinbase_input(*value, i.key_id.clone())); @@ -704,7 +707,7 @@ where } } for (id, _, value) in &context.get_outputs() { - let output = wallet.iter().find(|out| out.key_id == *id); + let output = wallet.iter()?.find(|out| out.key_id == *id); if let Some(i) = output { parts.push(build::output(*value, i.key_id.clone())); } diff --git a/libwallet/src/internal/tx.rs b/libwallet/src/internal/tx.rs index a5dde1aa9..0b5c07525 100644 --- a/libwallet/src/internal/tx.rs +++ b/libwallet/src/internal/tx.rs @@ -26,14 +26,13 @@ use crate::grin_util::secp::pedersen; use crate::grin_util::Mutex; use crate::internal::{selection, updater}; use crate::slate::Slate; -use crate::types::{Context, NodeClient, StoredProofInfo, TxLogEntryType, WalletBackend}; +use crate::types::{Context, NodeClient, StoredProofInfo, TxLogEntryType}; use crate::util::OnionV3Address; -use crate::InitTxArgs; use crate::{address, Error}; -use ed25519_dalek::Keypair as DalekKeypair; -use ed25519_dalek::PublicKey as DalekPublicKey; -use ed25519_dalek::SecretKey as DalekSecretKey; +use crate::{InitTxArgs, WalletBackend}; use ed25519_dalek::Signature as DalekSignature; +use ed25519_dalek::SigningKey as DalekSecretKey; +use ed25519_dalek::VerifyingKey as DalekPublicKey; use ed25519_dalek::{Signer, Verifier}; use grin_core::core::FeeFields; @@ -44,8 +43,8 @@ lazy_static! { /// Creates a new slate for a transaction, can be called by anyone involved in /// the transaction (sender(s), receiver(s)) -pub fn new_tx_slate<'a, T: ?Sized, C, K>( - wallet: &mut T, +pub fn new_tx_slate( + wallet: &mut WalletBackend, amount: u64, is_invoice: bool, num_participants: u8, @@ -53,9 +52,8 @@ pub fn new_tx_slate<'a, T: ?Sized, C, K>( ttl_blocks: Option, ) -> Result where - T: WalletBackend<'a, C, K>, - C: NodeClient + 'a, - K: Keychain + 'a, + C: NodeClient, + K: Keychain, { let current_height = wallet.w2n_client().get_chain_tip()?.0; let mut slate = Slate::blank(num_participants, is_invoice); @@ -92,8 +90,8 @@ where } /// Estimates locked amount and fee for the transaction without creating one -pub fn estimate_send_tx<'a, T: ?Sized, C, K>( - wallet: &mut T, +pub fn estimate_send_tx( + wallet: &mut WalletBackend, keychain_mask: Option<&SecretKey>, amount: u64, amount_includes_fee: bool, @@ -110,9 +108,8 @@ pub fn estimate_send_tx<'a, T: ?Sized, C, K>( Error, > where - T: WalletBackend<'a, C, K>, - C: NodeClient + 'a, - K: Keychain + 'a, + C: NodeClient, + K: Keychain, { // Get lock height let current_height = wallet.w2n_client().get_chain_tip()?.0; @@ -141,8 +138,8 @@ where } /// Add inputs to the slate (effectively becoming the sender) -pub fn add_inputs_to_slate<'a, T: ?Sized, C, K>( - wallet: &mut T, +pub fn add_inputs_to_slate( + wallet: &mut WalletBackend, keychain_mask: Option<&SecretKey>, slate: &mut Slate, current_height: u64, @@ -156,9 +153,8 @@ pub fn add_inputs_to_slate<'a, T: ?Sized, C, K>( amount_includes_fee: bool, ) -> Result where - T: WalletBackend<'a, C, K>, - C: NodeClient + 'a, - K: Keychain + 'a, + C: NodeClient, + K: Keychain, { // sender should always refresh outputs updater::refresh_outputs(wallet, keychain_mask, parent_key_id, false)?; @@ -207,8 +203,8 @@ where } /// Add receiver output to the slate -pub fn add_output_to_slate<'a, T: ?Sized, C, K>( - wallet: &mut T, +pub fn add_output_to_slate( + wallet: &mut WalletBackend, keychain_mask: Option<&SecretKey>, slate: &mut Slate, current_height: u64, @@ -217,9 +213,8 @@ pub fn add_output_to_slate<'a, T: ?Sized, C, K>( use_test_rng: bool, ) -> Result where - T: WalletBackend<'a, C, K>, - C: NodeClient + 'a, - K: Keychain + 'a, + C: NodeClient, + K: Keychain, { let keychain = wallet.keychain(keychain_mask)?; // create an output using the amount in the slate @@ -252,8 +247,8 @@ where } /// Create context, without adding inputs to slate -pub fn create_late_lock_context<'a, T: ?Sized, C, K>( - wallet: &mut T, +pub fn create_late_lock_context( + wallet: &mut WalletBackend, keychain_mask: Option<&SecretKey>, slate: &mut Slate, current_height: u64, @@ -262,9 +257,8 @@ pub fn create_late_lock_context<'a, T: ?Sized, C, K>( use_test_rng: bool, ) -> Result where - T: WalletBackend<'a, C, K>, - C: NodeClient + 'a, - K: Keychain + 'a, + C: NodeClient, + K: Keychain, { // sender should always refresh outputs updater::refresh_outputs(wallet, keychain_mask, parent_key_id, false)?; @@ -300,16 +294,15 @@ where } /// Complete a transaction -pub fn complete_tx<'a, T: ?Sized, C, K>( - wallet: &mut T, +pub fn complete_tx( + wallet: &mut WalletBackend, keychain_mask: Option<&SecretKey>, slate: &mut Slate, context: &Context, ) -> Result<(), Error> where - T: WalletBackend<'a, C, K>, - C: NodeClient + 'a, - K: Keychain + 'a, + C: NodeClient, + K: Keychain, { // when self sending invoice tx, use initiator nonce to finalize let (sec_key, sec_nonce) = { @@ -333,17 +326,16 @@ where } /// Rollback outputs associated with a transaction in the wallet -pub fn cancel_tx<'a, T: ?Sized, C, K>( - wallet: &mut T, +pub fn cancel_tx( + wallet: &mut WalletBackend, keychain_mask: Option<&SecretKey>, parent_key_id: &Identifier, tx_id: Option, tx_slate_id: Option, ) -> Result<(), Error> where - T: WalletBackend<'a, C, K>, - C: NodeClient + 'a, - K: Keychain + 'a, + C: NodeClient, + K: Keychain, { let mut tx_id_string = String::new(); if let Some(tx_id) = tx_id { @@ -359,42 +351,49 @@ where Some(&parent_key_id), false, )?; - if tx_vec.len() != 1 { + if tx_vec.len() == 0 { return Err(Error::TransactionDoesntExist(tx_id_string)); } - let tx = tx_vec[0].clone(); - match tx.tx_type { - TxLogEntryType::TxSent | TxLogEntryType::TxReceived | TxLogEntryType::TxReverted => {} - _ => return Err(Error::TransactionNotCancellable(tx_id_string)), - } - if tx.confirmed { - return Err(Error::TransactionNotCancellable(tx_id_string)); + // Collect the entries and their outputs first: the batch that cancels them borrows the + // wallet, and a slate can have several entries which must all be cancelled together. + let mut to_cancel = vec![]; + for tx in tx_vec { + debug!("cancel_tx: tx: {}", tx.tx_type); + match tx.tx_type { + TxLogEntryType::TxSent + | TxLogEntryType::TxReceived + | TxLogEntryType::TxReverted + | TxLogEntryType::TxSelfSpend => {} + _ => return Err(Error::TransactionNotCancellable(tx_id_string)), + } + if tx.confirmed { + return Err(Error::TransactionNotCancellable(tx_id_string)); + } + // get outputs associated with tx + let res = updater::retrieve_outputs( + wallet, + keychain_mask, + false, + Some(tx.id), + Some(&parent_key_id), + )?; + let outputs = res.iter().map(|m| m.output.clone()).collect(); + to_cancel.push((tx, outputs)); } - // get outputs associated with tx - let res = updater::retrieve_outputs( - wallet, - keychain_mask, - false, - Some(tx.id), - Some(&parent_key_id), - )?; - let outputs = res.iter().map(|m| m.output.clone()).collect(); - updater::cancel_tx_and_outputs(wallet, keychain_mask, tx, outputs, parent_key_id)?; - Ok(()) + updater::cancel_txs_and_outputs(wallet, keychain_mask, to_cancel, parent_key_id) } /// Update the stored transaction (this update needs to happen when the TX is finalised) -pub fn update_stored_tx<'a, T: ?Sized, C, K>( - wallet: &mut T, +pub fn update_stored_tx( + wallet: &mut WalletBackend, keychain_mask: Option<&SecretKey>, context: &Context, slate: &Slate, is_invoiced: bool, ) -> Result<(), Error> where - T: WalletBackend<'a, C, K>, - C: NodeClient + 'a, - K: Keychain + 'a, + C: NodeClient, + K: Keychain, { // finalize command let tx_vec = updater::retrieve_txs(wallet, None, Some(slate.id), None, None, false)?; @@ -421,25 +420,31 @@ where } if let Some(ref p) = slate.clone().payment_proof { - let derivation_index = match context.payment_proof_derivation_index { - Some(i) => i, - None => 0, - }; - let keychain = wallet.keychain(keychain_mask)?; - let parent_key_id = wallet.parent_key_id(); - let excess = slate.calc_excess(keychain.secp())?; - let sender_key = - address::address_from_derivation_path(&keychain, &parent_key_id, derivation_index)?; - let sender_address = OnionV3Address::from_private(&sender_key.0)?; - let sig = - create_payment_proof_signature(slate.amount, &excess, p.sender_address, sender_key)?; - tx.payment_proof = Some(StoredProofInfo { - receiver_address: p.receiver_address, - receiver_signature: p.receiver_signature, - sender_address_path: derivation_index, - sender_address: sender_address.to_ed25519()?, - sender_signature: Some(sig), - }) + if let Some(saddr) = p.sender_address { + let derivation_index = context.payment_proof_derivation_index.unwrap_or_else(|| 0); + let keychain = wallet.keychain(keychain_mask)?; + let parent_key_id = wallet.parent_key_id(); + let excess = slate.calc_excess(keychain.secp())?; + let sender_key = + address::address_from_derivation_path(&keychain, &parent_key_id, derivation_index)?; + let sender_address = OnionV3Address::from_private(&sender_key.0)?; + let sig = create_payment_proof_signature(slate.amount, &excess, saddr, sender_key)?; + tx.payment_proof = Some(StoredProofInfo { + receiver_address: p.receiver_address, + receiver_signature: p.promise_signature, + sender_address_path: derivation_index, + sender_address: sender_address.to_ed25519()?, + sender_signature: Some(sig), + // Filled in during the contract proof flow; unused on the legacy path. + proof_type: None, + receiver_public_nonce: None, + receiver_public_excess: None, + timestamp: None, + memo: None, + promise_signature: None, + sender_part_sig: None, + }) + } } wallet.store_tx(&format!("{}", tx.tx_slate_id.unwrap()), slate.tx_or_err()?)?; @@ -491,32 +496,21 @@ pub fn create_payment_proof_signature( sec_key: SecretKey, ) -> Result { let msg = payment_proof_message(amount, kernel_commitment, sender_address)?; - let d_skey = match DalekSecretKey::from_bytes(&sec_key.0) { - Ok(k) => k, - Err(e) => { - return Err(Error::ED25519Key(format!("{}", e))); - } - }; - let pub_key: DalekPublicKey = (&d_skey).into(); - let keypair = DalekKeypair { - public: pub_key, - secret: d_skey, - }; - Ok(keypair.sign(&msg)) + let d_skey = DalekSecretKey::from_bytes(&sec_key.0); + Ok(d_skey.sign(&msg)) } /// Verify all aspects of a completed payment proof on the current slate -pub fn verify_slate_payment_proof<'a, T: ?Sized, C, K>( - wallet: &mut T, +pub fn verify_slate_payment_proof( + wallet: &mut WalletBackend, keychain_mask: Option<&SecretKey>, parent_key_id: &Identifier, context: &Context, slate: &Slate, ) -> Result<(), Error> where - T: WalletBackend<'a, C, K>, - C: NodeClient + 'a, - K: Keychain + 'a, + C: NodeClient, + K: Keychain, { let tx_vec = updater::retrieve_txs( wallet, @@ -561,9 +555,15 @@ where let orig_sender_sk = address::address_from_derivation_path(&keychain, parent_key_id, index)?; let orig_sender_address = OnionV3Address::from_private(&orig_sender_sk.0)?; - if p.sender_address != orig_sender_address.to_ed25519()? { + if let Some(saddr) = p.sender_address { + if saddr != orig_sender_address.to_ed25519()? { + return Err(Error::PaymentProof( + "Sender address on slate does not match original sender address".to_owned(), + )); + } + } else { return Err(Error::PaymentProof( - "Sender address on slate does not match original sender address".to_owned(), + "Sender address on slate is not provided".to_owned(), )); } @@ -577,7 +577,7 @@ where &slate.calc_excess(&keychain.secp())?, orig_sender_address.to_ed25519()?, )?; - let sig = match p.receiver_signature { + let sig = match p.promise_signature { Some(s) => s, None => { return Err(Error::PaymentProof( @@ -642,7 +642,7 @@ mod test { let secp = secp_inst.lock(); let mut test_rng = StepRng::new(1_234_567_890_u64, 1); let sec_key = secp::key::SecretKey::new(&secp, &mut test_rng); - let d_skey = DalekSecretKey::from_bytes(&sec_key.0).unwrap(); + let d_skey = DalekSecretKey::from_bytes(&sec_key.0); let address: DalekPublicKey = (&d_skey).into(); diff --git a/libwallet/src/internal/updater.rs b/libwallet/src/internal/updater.rs index 09c8943f5..9049a9e72 100644 --- a/libwallet/src/internal/updater.rs +++ b/libwallet/src/internal/updater.rs @@ -30,32 +30,29 @@ use crate::grin_util::secp::key::SecretKey; use crate::grin_util::secp::pedersen; use crate::grin_util::static_secp_instance; use crate::internal::keys; -use crate::types::{ - NodeClient, OutputData, OutputStatus, TxLogEntry, TxLogEntryType, WalletBackend, WalletInfo, -}; +use crate::types::{NodeClient, OutputData, OutputStatus, TxLogEntry, TxLogEntryType, WalletInfo}; use crate::{ BlockFees, CbData, OutputCommitMapping, RetrieveTxQueryArgs, RetrieveTxQuerySortField, - RetrieveTxQuerySortOrder, + RetrieveTxQuerySortOrder, WalletBackend, }; use num_bigint::BigInt; -/// Retrieve all of the outputs (doesn't attempt to update from node) -pub fn retrieve_outputs<'a, T: ?Sized, C, K>( - wallet: &mut T, +/// Retrieve all the outputs (don't attempt to update from node) +pub fn retrieve_outputs( + wallet: &mut WalletBackend, keychain_mask: Option<&SecretKey>, show_spent: bool, tx_id: Option, parent_key_id: Option<&Identifier>, ) -> Result, Error> where - T: WalletBackend<'a, C, K>, - C: NodeClient + 'a, - K: Keychain + 'a, + C: NodeClient, + K: Keychain, { // just read the wallet here, no need for a write lock let mut outputs = wallet - .iter() + .iter()? .filter(|out| show_spent || out.status != OutputStatus::Spent) .collect::>(); @@ -94,192 +91,214 @@ where } /// Apply advanced filtering to resultset from retrieve_txs below -pub fn apply_advanced_tx_list_filtering<'a, T: ?Sized, C, K>( - wallet: &mut T, +pub fn apply_advanced_tx_list_filtering( + wallet: &mut WalletBackend, parent_key_id: Option<&Identifier>, query_args: &RetrieveTxQueryArgs, -) -> Vec +) -> Result, Error> where - T: WalletBackend<'a, C, K>, - C: NodeClient + 'a, - K: Keychain + 'a, + C: NodeClient, + K: Keychain, { // Apply simple bool, GTE or LTE fields - let txs_iter: Box> = Box::new( - wallet - .tx_log_iter() - .filter(|tx_entry| match parent_key_id { - Some(k) => tx_entry.parent_key_id == *k, - None => true, - }) - .filter(|tx_entry| { - if let Some(v) = query_args.exclude_cancelled { - if v { - tx_entry.tx_type != TxLogEntryType::TxReceivedCancelled - && tx_entry.tx_type != TxLogEntryType::TxSentCancelled - } else { - true - } - } else { - true - } - }) - .filter(|tx_entry| { - if let Some(v) = query_args.include_outstanding_only { - if v { - !tx_entry.confirmed - } else { - true - } - } else { - true - } - }) - .filter(|tx_entry| { - if let Some(v) = query_args.include_confirmed_only { - if v { - tx_entry.confirmed - } else { - true - } - } else { - true - } - }) - .filter(|tx_entry| { - if let Some(v) = query_args.include_sent_only { - if v { - tx_entry.tx_type == TxLogEntryType::TxSent - || tx_entry.tx_type == TxLogEntryType::TxSentCancelled - } else { - true - } + let mut bad_records = 0; + let txs_iter = wallet + .tx_log_iter()? + .filter(|tx| { + if tx.is_err() { + bad_records += 1; + } + tx.is_ok() + }) + .map(|tx| tx.unwrap()) + .filter(|tx_entry| match parent_key_id { + Some(k) => tx_entry.parent_key_id == *k, + None => true, + }) + .filter(|tx_entry| { + if let Some(v) = query_args.exclude_cancelled { + if v { + tx_entry.tx_type != TxLogEntryType::TxReceivedCancelled + && tx_entry.tx_type != TxLogEntryType::TxSentCancelled + && tx_entry.tx_type != TxLogEntryType::TxSelfSpendCancelled } else { true } - }) - .filter(|tx_entry| { - if let Some(v) = query_args.include_received_only { - if v { - tx_entry.tx_type == TxLogEntryType::TxReceived - || tx_entry.tx_type == TxLogEntryType::TxReceivedCancelled - } else { - true - } + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.include_outstanding_only { + if v { + !tx_entry.confirmed } else { true } - }) - .filter(|tx_entry| { - if let Some(v) = query_args.include_coinbase_only { - if v { - tx_entry.tx_type == TxLogEntryType::ConfirmedCoinbase - } else { - true - } + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.include_confirmed_only { + if v { + tx_entry.confirmed } else { true } - }) - .filter(|tx_entry| { - if let Some(v) = query_args.include_reverted_only { - if v { - tx_entry.tx_type == TxLogEntryType::TxReverted - } else { - true - } + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.include_sent_only { + if v { + tx_entry.tx_type == TxLogEntryType::TxSent + || tx_entry.tx_type == TxLogEntryType::TxSentCancelled + || tx_entry.tx_type == TxLogEntryType::TxSelfSpend } else { true } - }) - .filter(|tx_entry| { - if let Some(v) = query_args.min_id { - tx_entry.id >= v + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.include_received_only { + if v { + tx_entry.tx_type == TxLogEntryType::TxReceived + || tx_entry.tx_type == TxLogEntryType::TxReceivedCancelled + || tx_entry.tx_type == TxLogEntryType::TxSelfSpend } else { true } - }) - .filter(|tx_entry| { - if let Some(v) = query_args.max_id { - tx_entry.id <= v + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.include_coinbase_only { + if v { + tx_entry.tx_type == TxLogEntryType::ConfirmedCoinbase } else { true } - }) - .filter(|tx_entry| { - if let Some(v) = query_args.min_amount { - if tx_entry.tx_type == TxLogEntryType::TxSent - || tx_entry.tx_type == TxLogEntryType::TxSentCancelled - { - BigInt::from(tx_entry.amount_debited) - - BigInt::from(tx_entry.amount_credited) - >= BigInt::from(v) - } else { - BigInt::from(tx_entry.amount_credited) - - BigInt::from(tx_entry.amount_debited) - >= BigInt::from(v) - } + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.include_self_spend_only { + if v { + tx_entry.tx_type == TxLogEntryType::TxSelfSpend } else { true } - }) - .filter(|tx_entry| { - if let Some(v) = query_args.max_amount { - if tx_entry.tx_type == TxLogEntryType::TxSent - || tx_entry.tx_type == TxLogEntryType::TxSentCancelled - { - BigInt::from(tx_entry.amount_debited) - - BigInt::from(tx_entry.amount_credited) - <= BigInt::from(v) - } else { - BigInt::from(tx_entry.amount_credited) - - BigInt::from(tx_entry.amount_debited) - <= BigInt::from(v) - } + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.include_reverted_only { + if v { + tx_entry.tx_type == TxLogEntryType::TxReverted } else { true } - }) - .filter(|tx_entry| { - if let Some(v) = query_args.min_creation_timestamp { - tx_entry.creation_ts >= v + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.min_id { + tx_entry.id >= v + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.max_id { + tx_entry.id <= v + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.min_amount { + if tx_entry.tx_type == TxLogEntryType::TxSent + || tx_entry.tx_type == TxLogEntryType::TxSentCancelled + { + BigInt::from(tx_entry.amount_debited) - BigInt::from(tx_entry.amount_credited) + >= BigInt::from(v) } else { - true + BigInt::from(tx_entry.amount_credited) - BigInt::from(tx_entry.amount_debited) + >= BigInt::from(v) } - }) - .filter(|tx_entry| { - if let Some(v) = query_args.min_confirmed_timestamp { - tx_entry.creation_ts <= v + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.max_amount { + if tx_entry.tx_type == TxLogEntryType::TxSent + || tx_entry.tx_type == TxLogEntryType::TxSentCancelled + { + BigInt::from(tx_entry.amount_debited) - BigInt::from(tx_entry.amount_credited) + <= BigInt::from(v) } else { - true + BigInt::from(tx_entry.amount_credited) - BigInt::from(tx_entry.amount_debited) + <= BigInt::from(v) } - }) - .filter(|tx_entry| { - if let Some(v) = query_args.min_confirmed_timestamp { - if let Some(t) = tx_entry.confirmation_ts { - t >= v - } else { - true - } + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.min_creation_timestamp { + tx_entry.creation_ts >= v + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.min_confirmed_timestamp { + tx_entry.creation_ts <= v + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.min_confirmed_timestamp { + if let Some(t) = tx_entry.confirmation_ts { + t >= v } else { true } - }) - .filter(|tx_entry| { - if let Some(v) = query_args.max_confirmed_timestamp { - if let Some(t) = tx_entry.confirmation_ts { - t <= v - } else { - true - } + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.max_confirmed_timestamp { + if let Some(t) = tx_entry.confirmation_ts { + t <= v } else { true } - }), - ); + } else { + true + } + }); + // }; + //TODO: apply limit + introduce skip before collecting all records. + // Introduce comparator for LMDB to not request all records with limit. let mut return_txs: Vec = txs_iter.collect(); + if bad_records != 0 { + error!("apply_advanced_tx_list_filtering: tx history is missing {} records, cause db read error", bad_records); + } + // Now apply requested sorting if let Some(ref s) = query_args.sort_field { match s { @@ -326,13 +345,13 @@ where return_txs = return_txs.into_iter().take(l as usize).collect() } - return_txs + Ok(return_txs) } -/// Retrieve all of the transaction entries, or a particular entry +/// Retrieve all the transaction entries, or a particular entry /// if `parent_key_id` is set, only return entries from that key -pub fn retrieve_txs<'a, T: ?Sized, C, K>( - wallet: &mut T, +pub fn retrieve_txs( + wallet: &mut WalletBackend, tx_id: Option, tx_slate_id: Option, query_args: Option, @@ -340,18 +359,25 @@ pub fn retrieve_txs<'a, T: ?Sized, C, K>( outstanding_only: bool, ) -> Result, Error> where - T: WalletBackend<'a, C, K>, - C: NodeClient + 'a, - K: Keychain + 'a, + C: NodeClient, + K: Keychain, { let mut txs; // Adding in new transaction list query logic. If `tx_id` or `tx_slate_id` // is provided, then `query_args` is ignored and old logic is followed. if query_args.is_some() && tx_id.is_none() && tx_slate_id.is_none() { - txs = apply_advanced_tx_list_filtering(wallet, parent_key_id, &query_args.unwrap()) + txs = apply_advanced_tx_list_filtering(wallet, parent_key_id, &query_args.unwrap())? } else { + let mut bad_records = 0; txs = wallet - .tx_log_iter() + .tx_log_iter()? + .filter(|tx| { + if tx.is_err() { + bad_records += 1; + } + tx.is_ok() + }) + .map(|tx| tx.unwrap()) .filter(|tx_entry| { let f_pk = match parent_key_id { Some(k) => tx_entry.parent_key_id == *k, @@ -370,13 +396,20 @@ where !tx_entry.confirmed && (tx_entry.tx_type == TxLogEntryType::TxReceived || tx_entry.tx_type == TxLogEntryType::TxSent - || tx_entry.tx_type == TxLogEntryType::TxReverted) + || tx_entry.tx_type == TxLogEntryType::TxReverted + || tx_entry.tx_type == TxLogEntryType::TxSelfSpend) } false => true, }; f_pk && f_tx_id && f_txs && f_outstanding }) .collect(); + if bad_records != 0 { + error!( + "retrieve_txs: tx history is missing {} records, cause db read error", + bad_records + ); + } txs.sort_by_key(|tx| tx.creation_ts); } Ok(txs) @@ -384,16 +417,15 @@ where /// Refreshes the outputs in a wallet with the latest information /// from a node -pub fn refresh_outputs<'a, T: ?Sized, C, K>( - wallet: &mut T, +pub fn refresh_outputs( + wallet: &mut WalletBackend, keychain_mask: Option<&SecretKey>, parent_key_id: &Identifier, update_all: bool, ) -> Result<(), Error> where - T: WalletBackend<'a, C, K>, - C: NodeClient + 'a, - K: Keychain + 'a, + C: NodeClient, + K: Keychain, { let height = wallet.w2n_client().get_chain_tip()?.0; refresh_output_state(wallet, keychain_mask, height, parent_key_id, update_all)?; @@ -402,21 +434,20 @@ where /// build a local map of wallet outputs keyed by commit /// and a list of outputs we want to query the node for -pub fn map_wallet_outputs<'a, T: ?Sized, C, K>( - wallet: &mut T, +pub fn map_wallet_outputs( + wallet: &mut WalletBackend, keychain_mask: Option<&SecretKey>, parent_key_id: &Identifier, update_all: bool, ) -> Result, Option, bool)>, Error> where - T: WalletBackend<'a, C, K>, - C: NodeClient + 'a, - K: Keychain + 'a, + C: NodeClient, + K: Keychain, { let mut wallet_outputs = HashMap::new(); let keychain = wallet.keychain(keychain_mask)?; let unspents: Vec = wallet - .iter() + .iter()? .filter(|x| x.root_key_id == *parent_key_id && x.status != OutputStatus::Spent) .collect(); @@ -453,45 +484,49 @@ where } /// Cancel transaction and associated outputs -pub fn cancel_tx_and_outputs<'a, T: ?Sized, C, K>( - wallet: &mut T, +pub fn cancel_txs_and_outputs( + wallet: &mut WalletBackend, keychain_mask: Option<&SecretKey>, - mut tx: TxLogEntry, - outputs: Vec, + txs: Vec<(TxLogEntry, Vec)>, parent_key_id: &Identifier, ) -> Result<(), Error> where - T: WalletBackend<'a, C, K>, - C: NodeClient + 'a, - K: Keychain + 'a, + C: NodeClient, + K: Keychain, { + // A single slate can have several tx log entries, so they are all cancelled in one + // batch: committing per entry would let a failure part way through leave some of them + // cancelled and the rest untouched. let mut batch = wallet.batch(keychain_mask)?; - for mut o in outputs { - // unlock locked outputs - if o.status == OutputStatus::Unconfirmed || o.status == OutputStatus::Reverted { - batch.delete(&o.key_id, &o.mmr_index)?; - } - if o.status == OutputStatus::Locked { - o.status = OutputStatus::Unspent; - batch.save(o)?; + for (mut tx, outputs) in txs { + for mut o in outputs { + // unlock locked outputs + if o.status == OutputStatus::Unconfirmed || o.status == OutputStatus::Reverted { + batch.delete(&o.key_id, &o.mmr_index)?; + } + if o.status == OutputStatus::Locked { + o.status = OutputStatus::Unspent; + batch.save(o)?; + } } - } - match tx.tx_type { - TxLogEntryType::TxSent => tx.tx_type = TxLogEntryType::TxSentCancelled, - TxLogEntryType::TxReceived | TxLogEntryType::TxReverted => { - tx.tx_type = TxLogEntryType::TxReceivedCancelled + match tx.tx_type { + TxLogEntryType::TxSent => tx.tx_type = TxLogEntryType::TxSentCancelled, + TxLogEntryType::TxReceived | TxLogEntryType::TxReverted => { + tx.tx_type = TxLogEntryType::TxReceivedCancelled + } + TxLogEntryType::TxSelfSpend => tx.tx_type = TxLogEntryType::TxSelfSpendCancelled, + _ => {} } - _ => {} + batch.save_tx_log_entry(tx, parent_key_id)?; } - batch.save_tx_log_entry(tx, parent_key_id)?; batch.commit()?; Ok(()) } /// Apply refreshed API output data to the wallet -pub fn apply_api_outputs<'a, T: ?Sized, C, K>( - wallet: &mut T, +pub fn apply_api_outputs( + wallet: &mut WalletBackend, keychain_mask: Option<&SecretKey>, wallet_outputs: &HashMap, Option, bool)>, api_outputs: &HashMap, @@ -500,9 +535,8 @@ pub fn apply_api_outputs<'a, T: ?Sized, C, K>( parent_key_id: &Identifier, ) -> Result<(), Error> where - T: WalletBackend<'a, C, K>, - C: NodeClient + 'a, - K: Keychain + 'a, + C: NodeClient, + K: Keychain, { // now for each commit, find the output in the wallet and the corresponding // api output (if it exists) and refresh it in-place in the wallet. @@ -556,10 +590,14 @@ where && (output.status == OutputStatus::Unconfirmed || output.status == OutputStatus::Reverted) { - let tx = batch.tx_log_iter().find(|t| { - Some(t.id) == output.tx_log_entry - && t.parent_key_id == *parent_key_id - }); + let tx = batch + .tx_log_iter()? + .filter(|t| t.is_ok()) + .map(|t| t.unwrap()) + .find(|t| { + Some(t.id) == output.tx_log_entry + && t.parent_key_id == *parent_key_id + }); if let Some(mut t) = tx { if t.tx_type == TxLogEntryType::TxReverted { t.tx_type = TxLogEntryType::TxReceived; @@ -568,6 +606,10 @@ where t.update_confirmation_ts(); t.confirmed = true; batch.save_tx_log_entry(t, &parent_key_id)?; + } else { + if let Some(tx_id) = output.tx_log_entry { + error!("apply_api_outputs: tx with id {:?} not found", tx_id); + } } } output.height = o.1; @@ -590,7 +632,18 @@ where } } - for mut tx in batch.tx_log_iter() { + let mut txs_to_save = vec![]; + let mut bad_records = 0; + for mut tx in batch + .tx_log_iter()? + .filter(|tx| { + if tx.is_err() { + bad_records += 1; + } + tx.is_ok() + }) + .map(|t| t.unwrap()) + { if reverted_kernels.contains(&tx.id) && tx.parent_key_id == *parent_key_id { tx.tx_type = TxLogEntryType::TxReverted; tx.reverted_after = tx.confirmation_ts.clone().and_then(|t| { @@ -598,10 +651,21 @@ where (now - t).to_std().ok() }); tx.confirmed = false; - batch.save_tx_log_entry(tx, &parent_key_id)?; + txs_to_save.push(tx); } } + if bad_records > 0 { + error!( + "apply_api_outputs: tx history is missing {} records, cause db read error", + bad_records + ); + } + + for tx in txs_to_save { + batch.save_tx_log_entry(tx, &parent_key_id)?; + } + { batch.save_last_confirmed_height(parent_key_id, height)?; } @@ -612,17 +676,16 @@ where /// Builds a single api query to retrieve the latest output data from the node. /// So we can refresh the local wallet outputs. -fn refresh_output_state<'a, T: ?Sized, C, K>( - wallet: &mut T, +fn refresh_output_state( + wallet: &mut WalletBackend, keychain_mask: Option<&SecretKey>, height: u64, parent_key_id: &Identifier, update_all: bool, ) -> Result<(), Error> where - T: WalletBackend<'a, C, K>, - C: NodeClient + 'a, - K: Keychain + 'a, + C: NodeClient, + K: Keychain, { debug!("Refreshing wallet outputs"); @@ -654,16 +717,15 @@ where Ok(()) } -fn find_reverted_kernels<'a, T: ?Sized, C, K>( - wallet: &mut T, +fn find_reverted_kernels( + wallet: &mut WalletBackend, wallet_outputs: &HashMap, Option, bool)>, api_outputs: &HashMap, parent_key_id: &Identifier, ) -> Result, Error> where - T: WalletBackend<'a, C, K>, - C: NodeClient + 'a, - K: Keychain + 'a, + C: NodeClient, + K: Keychain, { let mut client = wallet.w2n_client().clone(); let mut ids = HashSet::new(); @@ -678,8 +740,16 @@ where } // Get corresponding kernels + let mut bad_records = 0; let kernels = wallet - .tx_log_iter() + .tx_log_iter()? + .filter(|tx| { + if tx.is_err() { + bad_records += 1; + } + tx.is_ok() + }) + .map(|t| t.unwrap()) .filter(|t| { ids.contains(&t.id) && t.parent_key_id == *parent_key_id @@ -698,24 +768,30 @@ where } } + if bad_records > 0 { + error!( + "find_reverted_kernels: tx history is missing {} records, cause db read error", + bad_records + ); + } + Ok(reverted) } -fn clean_old_unconfirmed<'a, T: ?Sized, C, K>( - wallet: &mut T, +fn clean_old_unconfirmed( + wallet: &mut WalletBackend, keychain_mask: Option<&SecretKey>, height: u64, ) -> Result<(), Error> where - T: WalletBackend<'a, C, K>, - C: NodeClient + 'a, - K: Keychain + 'a, + C: NodeClient, + K: Keychain, { if height < 50 { return Ok(()); } let mut ids_to_del = vec![]; - for out in wallet.iter() { + for out in wallet.iter()? { if out.status == OutputStatus::Unconfirmed && out.height > 0 && out.height < height - 50 @@ -734,19 +810,18 @@ where /// Retrieve summary info about the wallet /// caller should refresh first if desired -pub fn retrieve_info<'a, T: ?Sized, C, K>( - wallet: &mut T, +pub fn retrieve_info( + wallet: &mut WalletBackend, parent_key_id: &Identifier, minimum_confirmations: u64, ) -> Result where - T: WalletBackend<'a, C, K>, - C: NodeClient + 'a, - K: Keychain + 'a, + C: NodeClient, + K: Keychain, { let current_height = wallet.last_confirmed_height()?; let outputs = wallet - .iter() + .iter()? .filter(|out| out.root_key_id == *parent_key_id); let mut unspent_total = 0; @@ -800,16 +875,15 @@ where } /// Build a coinbase output and insert into wallet -pub fn build_coinbase<'a, T: ?Sized, C, K>( - wallet: &mut T, +pub fn build_coinbase( + wallet: &mut WalletBackend, keychain_mask: Option<&SecretKey>, block_fees: &BlockFees, test_mode: bool, ) -> Result where - T: WalletBackend<'a, C, K>, - C: NodeClient + 'a, - K: Keychain + 'a, + C: NodeClient, + K: Keychain, { let (out, kern, block_fees) = receive_coinbase(wallet, keychain_mask, block_fees, test_mode)?; @@ -822,16 +896,15 @@ where //TODO: Split up the output creation and the wallet insertion /// Build a coinbase output and the corresponding kernel -pub fn receive_coinbase<'a, T: ?Sized, C, K>( - wallet: &mut T, +pub fn receive_coinbase( + wallet: &mut WalletBackend, keychain_mask: Option<&SecretKey>, block_fees: &BlockFees, test_mode: bool, ) -> Result<(Output, TxKernel, BlockFees), Error> where - T: WalletBackend<'a, C, K>, - C: NodeClient + 'a, - K: Keychain + 'a, + C: NodeClient, + K: Keychain, { let height = block_fees.height; let lock_height = height + global::coinbase_maturity(); @@ -856,11 +929,11 @@ where key_id: key_id.clone(), n_child: key_id.to_path().last_path_index(), mmr_index: None, - commit: commit, + commit, value: amount, status: OutputStatus::Unconfirmed, - height: height, - lock_height: lock_height, + height, + lock_height, is_coinbase: true, tx_log_entry: None, })?; diff --git a/libwallet/src/lib.rs b/libwallet/src/lib.rs index 95185c5cb..5f30b3dd2 100644 --- a/libwallet/src/lib.rs +++ b/libwallet/src/lib.rs @@ -35,6 +35,8 @@ use blake2_rfc as blake2; #[macro_use] extern crate serde_derive; #[macro_use] +extern crate serde_with; +#[macro_use] extern crate log; #[macro_use] extern crate lazy_static; @@ -45,6 +47,8 @@ extern crate strum_macros; pub mod address; pub mod api_impl; +mod backend; +pub mod contract; mod error; mod internal; pub mod mwixnet; @@ -69,15 +73,17 @@ pub use api_impl::types::{ NodeHeightResult, OutputCommitMapping, PaymentProof, RetrieveTxQueryArgs, RetrieveTxQuerySortField, RetrieveTxQuerySortOrder, VersionInfo, }; +pub use backend::{WalletBackend, WalletBatch}; pub use internal::scan::scan; pub use slate_versions::ser as dalek_ser; pub use types::{ AcctPathMapping, BlockIdentifier, CbData, Context, NodeClient, NodeVersionInfo, OutputData, OutputStatus, ScannedBlockInfo, StoredProofInfo, TxLogEntry, TxLogEntryType, TxWrapper, - ViewWallet, WalletBackend, WalletInfo, WalletInitStatus, WalletInst, WalletLCProvider, - WalletOutputBatch, + ViewWallet, WalletInfo, WalletInitStatus, WalletInst, WalletLCProvider, }; +pub use contract::can_finalize; + /// Helper for taking a lock on the wallet instance #[macro_export] macro_rules! wallet_lock { diff --git a/libwallet/src/mwixnet/mod.rs b/libwallet/src/mwixnet/mod.rs index 06052658a..ebc5a9c70 100644 --- a/libwallet/src/mwixnet/mod.rs +++ b/libwallet/src/mwixnet/mod.rs @@ -13,7 +13,7 @@ // limitations under the License. //! Onion modules for mxmixnet -mod onion; +pub mod onion; mod types; pub use onion::{ diff --git a/libwallet/src/mwixnet/onion/crypto/comsig.rs b/libwallet/src/mwixnet/onion/crypto/comsig.rs index bf9e3479f..89eb8f85c 100644 --- a/libwallet/src/mwixnet/onion/crypto/comsig.rs +++ b/libwallet/src/mwixnet/onion/crypto/comsig.rs @@ -204,7 +204,6 @@ mod tests { use super::{ComSigError, ComSignature, ContextFlag, Secp256k1, SecretKey}; use grin_util::secp::rand::{thread_rng, RngCore}; - use rand::Rng; /// Test signing and verification of ComSignatures #[test] @@ -213,13 +212,13 @@ mod tests { let amount = thread_rng().next_u64(); let blind = SecretKey::new(&secp, &mut thread_rng()); - let msg: [u8; 16] = rand::thread_rng().gen(); + let msg: [u8; 16] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let comsig = ComSignature::sign(amount, &blind, &msg.to_vec(), false)?; let commit = secp.commit(amount, blind.clone())?; assert!(comsig.verify(&commit, &msg.to_vec()).is_ok()); - let wrong_msg: [u8; 16] = rand::thread_rng().gen(); + let wrong_msg: [u8; 16] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 17]; assert!(comsig.verify(&commit, &wrong_msg.to_vec()).is_err()); let wrong_commit = secp.commit(amount, SecretKey::new(&secp, &mut thread_rng()))?; diff --git a/libwallet/src/mwixnet/onion/crypto/dalek.rs b/libwallet/src/mwixnet/onion/crypto/dalek.rs index 38fd408cf..adbebd7c5 100644 --- a/libwallet/src/mwixnet/onion/crypto/dalek.rs +++ b/libwallet/src/mwixnet/onion/crypto/dalek.rs @@ -14,9 +14,10 @@ //! Dalek key wrapper for mwixnet primitives +use ed25519_dalek::Verifier; use grin_util::secp::key::SecretKey; -use ed25519_dalek::{PublicKey, Signature, Verifier}; +use ed25519_dalek::VerifyingKey; use grin_core::ser::{self, Readable, Reader, Writeable, Writer}; use grin_util::ToHex; use thiserror::Error; @@ -37,7 +38,7 @@ pub enum DalekError { /// Encapsulates an ed25519_dalek::PublicKey and provides (de-)serialization #[derive(Clone, Debug, PartialEq)] -pub struct DalekPublicKey(PublicKey); +pub struct DalekPublicKey(VerifyingKey); impl DalekPublicKey { /// Convert DalekPublicKey to hex string @@ -47,28 +48,27 @@ impl DalekPublicKey { /// Convert hex string to DalekPublicKey. pub fn from_hex(hex: &str) -> Result { - let bytes = grin_util::from_hex(hex) - .map_err(|_| DalekError::HexError(format!("failed to decode {}", hex)))?; - let pk = PublicKey::from_bytes(bytes.as_ref()) - .map_err(|_| DalekError::HexError(format!("failed to decode {}", hex)))?; + let err = DalekError::HexError(format!("failed to decode {}", hex)); + let bytes = grin_util::from_hex(hex).map_err(|_| err.clone())?; + let b = <&[u8; 32]>::try_from(bytes.as_slice()).map_err(|_| err.clone())?; + let pk = VerifyingKey::from_bytes(b).map_err(|_| err)?; Ok(DalekPublicKey(pk)) } /// Compute DalekPublicKey from a SecretKey pub fn from_secret(key: &SecretKey) -> Self { - let secret = ed25519_dalek::SecretKey::from_bytes(&key.0).unwrap(); - let pk: PublicKey = (&secret).into(); + let secret = ed25519_dalek::SigningKey::from_bytes(&key.0); + let pk: VerifyingKey = (&secret).into(); DalekPublicKey(pk) } } -impl AsRef for DalekPublicKey { - fn as_ref(&self) -> &PublicKey { +impl AsRef for DalekPublicKey { + fn as_ref(&self) -> &VerifyingKey { &self.0 } } -#[cfg(test)] /// Serializes an Option to and from hex pub mod option_dalek_pubkey_serde { use super::DalekPublicKey; @@ -103,8 +103,9 @@ pub mod option_dalek_pubkey_serde { impl Readable for DalekPublicKey { fn read(reader: &mut R) -> Result { - let pk = PublicKey::from_bytes(&reader.read_fixed_bytes(32)?) - .map_err(|_| ser::Error::CorruptedData)?; + let bytes = reader.read_fixed_bytes(32)?; + let b = <&[u8; 32]>::try_from(bytes.as_slice()).map_err(|_| ser::Error::CorruptedData)?; + let pk = VerifyingKey::from_bytes(b).map_err(|_| ser::Error::CorruptedData)?; Ok(DalekPublicKey(pk)) } } @@ -119,7 +120,7 @@ impl Writeable for DalekPublicKey { /// Encapsulates an ed25519_dalek::Signature and provides (de-)serialization #[allow(dead_code)] #[derive(Clone, Debug, PartialEq)] -pub struct DalekSignature(Signature); +pub struct DalekSignature(ed25519_dalek::Signature); impl DalekSignature { /// Convert hex string to DalekSignature. @@ -127,7 +128,9 @@ impl DalekSignature { pub fn from_hex(hex: &str) -> Result { let bytes = grin_util::from_hex(hex) .map_err(|_| DalekError::HexError(format!("failed to decode {}", hex)))?; - let sig = Signature::from_bytes(bytes.as_ref()) + let b = <[u8; 64]>::try_from(bytes.as_slice()) + .map_err(|_| DalekError::HexError(format!("failed to decode {}", hex)))?; + let sig = ed25519_dalek::Signature::try_from(b) .map_err(|_| DalekError::HexError(format!("failed to decode {}", hex)))?; Ok(DalekSignature(sig)) } @@ -141,14 +144,13 @@ impl DalekSignature { } } -impl AsRef for DalekSignature { - fn as_ref(&self) -> &Signature { +impl AsRef for DalekSignature { + fn as_ref(&self) -> &ed25519_dalek::Signature { &self.0 } } /// Serializes a DalekSignature to and from hex -#[cfg(test)] pub mod dalek_sig_serde { use super::DalekSignature; use grin_util::ToHex; @@ -160,7 +162,7 @@ pub mod dalek_sig_serde { where S: Serializer, { - serializer.serialize_str(&sig.0.to_hex()) + serializer.serialize_str(&sig.0.to_bytes().to_hex()) } /// @@ -175,25 +177,20 @@ pub mod dalek_sig_serde { } /// Dalek signature sign wrapper -// TODO: This is likely duplicated throughout crate, check -#[cfg(test)] pub fn sign(sk: &SecretKey, message: &[u8]) -> Result { - use ed25519_dalek::{Keypair, Signer}; - let secret = - ed25519_dalek::SecretKey::from_bytes(&sk.0).map_err(|_| DalekError::KeyParseError)?; - let public: PublicKey = (&secret).into(); - let keypair = Keypair { secret, public }; - let sig = keypair.sign(&message); + use ed25519_dalek::{Signer, SigningKey}; + let secret = SigningKey::from_bytes(&sk.0); + let sig = secret.sign(&message); Ok(DalekSignature(sig)) } #[cfg(test)] mod tests { use super::*; + use crate::mwixnet::onion::crypto::secp::Secp256k1; use crate::mwixnet::onion::test_util::rand_keypair; use grin_core::ser::{self, ProtocolVersion}; use grin_util::ToHex; - use rand::Rng; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -262,23 +259,27 @@ mod tests { #[test] fn sig_test() -> Result<(), Box> { // Sign a message - let (sk, pk) = rand_keypair(); - let msg: [u8; 16] = rand::thread_rng().gen(); + let s = Secp256k1::new(); + let sk = SecretKey::from_slice(&s, &[1; 32]).unwrap(); + let pk = DalekPublicKey::from_secret(&sk); + + let msg: [u8; 16] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; let sig = sign(&sk, &msg).unwrap(); // Verify signature assert!(sig.verify(&pk, &msg).is_ok()); // Wrong message - let wrong_msg: [u8; 16] = rand::thread_rng().gen(); + let wrong_msg: [u8; 16] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 17]; assert!(sig.verify(&pk, &wrong_msg).is_err()); // Wrong pubkey - let wrong_pk = rand_keypair().1; + let wrong_sk = SecretKey::from_slice(&s, &[2; 32]).unwrap(); + let wrong_pk = DalekPublicKey::from_secret(&wrong_sk); assert!(sig.verify(&wrong_pk, &msg).is_err()); // Test from_hex - let sig_from_hex = DalekSignature::from_hex(sig.0.to_hex().as_str()).unwrap(); + let sig_from_hex = DalekSignature::from_hex(sig.0.to_bytes().to_hex().as_str()).unwrap(); assert_eq!(sig.0, sig_from_hex.0); // Test serde (de-)serialization @@ -286,7 +287,7 @@ mod tests { let val = serde_json::to_value(serde_test.clone()).unwrap(); if let Value::Object(o) = &val { if let Value::String(s) = o.get("sig").unwrap() { - assert_eq!(s, &sig.0.to_hex()); + assert_eq!(s, &sig.0.to_bytes().to_hex()); } else { panic!("Invalid type"); } diff --git a/libwallet/src/mwixnet/onion/crypto/secp.rs b/libwallet/src/mwixnet/onion/crypto/secp.rs index eda6abe77..200c24f8c 100644 --- a/libwallet/src/mwixnet/onion/crypto/secp.rs +++ b/libwallet/src/mwixnet/onion/crypto/secp.rs @@ -18,7 +18,7 @@ pub use grin_util::secp::{ self as secp256k1zkp, constants::SECRET_KEY_SIZE, key::{SecretKey, ZERO_KEY}, - pedersen::Commitment, + pedersen::{Commitment, RangeProof}, rand::thread_rng, ContextFlag, Secp256k1, }; @@ -47,7 +47,6 @@ pub fn read_secret_key(reader: &mut R) -> Result Result { let secp = Secp256k1::with_caps(ContextFlag::Commit); let commit = secp.commit(value, blind.clone())?; @@ -76,7 +75,6 @@ pub fn sub_value(commitment: &Commitment, value: u64) -> Result, /// Receiver address pub receiver_address: DalekPublicKey, - /// Receiver signature - pub receiver_signature: Option, + /// Promise signature + pub promise_signature: Option, + /// Timestamp (seconds) + pub timestamp: DateTime, + /// Memo + pub memo: Option, } /// Public data for each participant in the slate @@ -81,7 +101,7 @@ impl ParticipantData { } } -/// A 'Slate' is passed around to all parties to build up all of the public +/// A 'Slate' is passed around to all parties to build up all the public /// transaction data needed to create a finalized transaction. Callers can pass /// the slate around by whatever means they choose, (but we can provide some /// binary or JSON serialization helpers here). @@ -115,7 +135,7 @@ pub struct Slate { /// 2: height_locked /// 3: NRD pub kernel_features: u8, - /// Offset, needed when posting of transasction is deferred + /// Offset, needed when posting of transaction is deferred pub offset: BlindingFactor, /// Participant data, each participant in the transaction will /// insert their public data here. For now, 0 is sender and 1 @@ -134,7 +154,7 @@ impl fmt::Display for Slate { } /// Slate state definition -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] pub enum SlateState { /// Unknown, coming from earlier slate versions Unknown, @@ -148,7 +168,7 @@ pub enum SlateState { Invoice1, ///Invoice flow, return journey Invoice2, - /// Invoice flow, ready for tranasction posting + /// Invoice flow, ready for transaction posting Invoice3, } @@ -226,10 +246,11 @@ impl Slate { /// Upgrade a versioned slate pub fn upgrade(v_slate: VersionedSlate) -> Result { - let v4: SlateV4 = match v_slate { - VersionedSlate::V4(s) => s, + let internal: Slate = match v_slate { + VersionedSlate::V4(s) => s.into(), + VersionedSlate::V5(s) => s.into(), }; - Ok(v4.into()) + Ok(internal.into()) } /// Compact the slate for initial sending, storing the excess + offset explicit /// and removing my input/output data @@ -276,8 +297,12 @@ impl Slate { kernel_features_args: None, } } + /// Removes any signature data that isn't mine, for compacting /// slates for a return journey + // TODO: Check if this is a noop when we have only 2 parties. The first sig appears at + // step2 and removing everything except your sig means you remove nothing. For more than + // 2 parties, we should probably never remove the part_sigs so that everyone can verify them. pub fn remove_other_sigdata( &mut self, keychain: &K, @@ -310,13 +335,22 @@ impl Slate { K: Keychain, B: ProofBuild, { + debug!("slate::add_transaction_elements => start"); self.update_kernel()?; + + debug!("slate::add_transaction_elements => kernel updated"); if elems.is_empty() { + debug!("slate::add_transaction_elements => elems is empty, returning"); return Ok(BlindingFactor::zero()); } + let (tx, blind) = build::partial_transaction(self.tx_or_err()?.clone(), &elems, keychain, builder)?; + + debug!("slate::add_transaction_elements => built partial transaction"); self.tx = Some(tx); + + debug!("slate::add_transaction_elements => slate.tx is set"); Ok(blind) } @@ -377,6 +411,29 @@ impl Slate { Ok(msg) } + /// Matches a participant index on the slate with the stored context + pub fn find_index_matching_context( + &self, + keychain: &K, + context: &Context, + ) -> Result + where + K: Keychain, + { + let calc_pub_excess = PublicKey::from_secret_key(keychain.secp(), &context.sec_key)?; + let calc_pub_nonce = PublicKey::from_secret_key(keychain.secp(), &context.sec_nonce)?; + // Iterate over the entries the slate actually carries rather than indexing by + // position up to num_participants, which a counterparty can send short. Both keys + // have to match, as fill_round_2 requires, so an entry sharing only one of them is + // not mistaken for ours. + for (i, p) in self.participant_data.iter().enumerate() { + // find my entry + if p.public_blind_excess == calc_pub_excess && p.public_nonce == calc_pub_nonce { + return Ok(i); + } + } + return Err(Error::ContextToIndex); + } /// Completes caller's part of round 2, completing signatures pub fn fill_round_2( &mut self, @@ -402,12 +459,11 @@ impl Slate { )?; let pub_excess = PublicKey::from_secret_key(keychain.secp(), &sec_key)?; let pub_nonce = PublicKey::from_secret_key(keychain.secp(), &sec_nonce)?; - for i in 0..self.num_participants() as usize { + // As in find_index_matching_context, iterate the entries we actually have + for p in self.participant_data.iter_mut() { // find my entry - if self.participant_data[i].public_blind_excess == pub_excess - && self.participant_data[i].public_nonce == pub_nonce - { - self.participant_data[i].part_sig = Some(sig_part); + if p.public_blind_excess == pub_excess && p.public_nonce == pub_nonce { + p.part_sig = Some(sig_part); break; } } @@ -686,13 +742,460 @@ impl Serialize for Slate { where S: Serializer, { - let v4 = SlateV4::from(self); - v4.serialize(serializer) + let v5 = SlateV5::from(self); + v5.serialize(serializer) } } // Current slate version to versioned conversions -// Slate to versioned +////// V5 +impl From for SlateV5 { + fn from(slate: Slate) -> SlateV5 { + let Slate { + num_participants: num_parts, + id, + state, + tx: _, + amount, + fee_fields, + kernel_features, + ttl_cutoff_height: ttl, + offset: off, + participant_data, + version_info, + payment_proof, + kernel_features_args, + } = slate.clone(); + let participant_data = map_vec!(participant_data, |data| ParticipantDataV5::from(data)); + let ver = VersionCompatInfoV5::from(&version_info); + let payment_proof = match payment_proof { + Some(p) => Some(PaymentInfoV5::from(&p)), + None => None, + }; + let feat_args = match kernel_features_args { + Some(a) => Some(KernelFeaturesArgsV5::from(&a)), + None => None, + }; + let sta = SlateStateV5::from(&state); + SlateV5 { + num_parts, + id, + sta, + coms: (&slate).into(), + amt: amount, + fee: fee_fields, + feat: kernel_features, + ttl, + off, + sigs: participant_data, + ver, + proof: payment_proof, + feat_args, + } + } +} + +impl From<&Slate> for SlateV5 { + fn from(slate: &Slate) -> SlateV5 { + let Slate { + num_participants: num_parts, + id, + state, + tx: _, + amount, + fee_fields, + kernel_features, + ttl_cutoff_height: ttl, + offset, + participant_data, + version_info, + payment_proof, + kernel_features_args, + } = slate; + let num_parts = *num_parts; + let id = *id; + let amount = *amount; + let fee_fields = *fee_fields; + let feat = *kernel_features; + let ttl = *ttl; + let off = offset.clone(); + let participant_data = map_vec!(participant_data, |data| ParticipantDataV5::from(data)); + let ver = VersionCompatInfoV5::from(version_info); + let payment_proof = match payment_proof { + Some(p) => Some(PaymentInfoV5::from(p)), + None => None, + }; + let sta = SlateStateV5::from(state); + let feat_args = match kernel_features_args { + Some(a) => Some(KernelFeaturesArgsV5::from(a)), + None => None, + }; + SlateV5 { + num_parts, + id, + sta, + coms: slate.into(), + amt: amount, + fee: fee_fields, + feat, + ttl, + off, + sigs: participant_data, + ver, + proof: payment_proof, + feat_args, + } + } +} + +impl From<&Slate> for Option> { + fn from(slate: &Slate) -> Self { + match slate.tx { + None => None, + Some(ref tx) => { + let mut ret_vec = vec![]; + match tx.inputs() { + Inputs::CommitOnly(_) => panic!("commit only inputs unsupported"), + Inputs::FeaturesAndCommit(ref inputs) => { + for input in inputs { + ret_vec.push(input.into()); + } + } + } + for output in tx.outputs() { + ret_vec.push(output.into()); + } + Some(ret_vec) + } + } + } +} + +impl From<&ParticipantData> for ParticipantDataV5 { + fn from(data: &ParticipantData) -> ParticipantDataV5 { + let ParticipantData { + public_blind_excess, + public_nonce, + part_sig, + } = data; + let public_blind_excess = *public_blind_excess; + let public_nonce = *public_nonce; + let part_sig = *part_sig; + ParticipantDataV5 { + xs: public_blind_excess, + nonce: public_nonce, + part: part_sig, + } + } +} + +impl From<&SlateState> for SlateStateV5 { + fn from(data: &SlateState) -> SlateStateV5 { + match data { + SlateState::Unknown => SlateStateV5::Unknown, + SlateState::Standard1 => SlateStateV5::Standard1, + SlateState::Standard2 => SlateStateV5::Standard2, + SlateState::Standard3 => SlateStateV5::Standard3, + SlateState::Invoice1 => SlateStateV5::Invoice1, + SlateState::Invoice2 => SlateStateV5::Invoice2, + SlateState::Invoice3 => SlateStateV5::Invoice3, + } + } +} + +impl From<&KernelFeaturesArgs> for KernelFeaturesArgsV5 { + fn from(data: &KernelFeaturesArgs) -> KernelFeaturesArgsV5 { + let KernelFeaturesArgs { lock_height } = data; + let lock_hgt = *lock_height; + KernelFeaturesArgsV5 { lock_hgt } + } +} + +impl From<&VersionCompatInfo> for VersionCompatInfoV5 { + fn from(data: &VersionCompatInfo) -> VersionCompatInfoV5 { + let VersionCompatInfo { + version: _, + block_header_version, + } = data; + // The declared version describes the structure we are producing, so it is fixed + // here rather than carried over from the internal slate. Readers rely on it to + // select the matching variant. + VersionCompatInfoV5 { + version: 5, + block_header_version: *block_header_version, + } + } +} + +impl From<&PaymentInfo> for PaymentInfoV5 { + fn from(data: &PaymentInfo) -> PaymentInfoV5 { + let PaymentInfo { + sender_address, + receiver_address, + promise_signature, + timestamp, + memo, + } = data; + let sender_address = *sender_address; + // Downgrading a proof with no sender address emits an all-zero placeholder key + // (a valid small-order ed25519 point, so from_bytes never panics). It can never be + // accepted as a real sender: every verification path binds the sender address into + // the signed message (contract::proofs::InvoiceProof::verify_promise_signature, and + // the legacy api_impl::owner::verify_payment_proof via internal::tx::payment_proof_message), + // so a proof only validates against the address the verifier supplies, never one + // derived from the proof. From is infallible by design, so we do not fail here. + let saddr = match sender_address { + Some(a) => a, + None => DalekPublicKey::from_bytes(&[0u8; 32]).unwrap(), + }; + let receiver_address = *receiver_address; + let promise_signature = *promise_signature; + let timestamp = *timestamp; + let memo = match memo { + Some(m) => Some(PaymentMemoV5 { + memo_type: m.memo_type, + memo: m.memo, + }), + None => None, + }; + PaymentInfoV5 { + saddr, + raddr: receiver_address, + psig: promise_signature, + ts: timestamp, + memo: memo, + } + } +} + +impl From for OutputFeaturesV5 { + fn from(of: OutputFeatures) -> OutputFeaturesV5 { + let index = match of { + OutputFeatures::Plain => 0, + OutputFeatures::Coinbase => 1, + }; + OutputFeaturesV5(index) + } +} + +///// V5 +impl From for Slate { + fn from(slate: SlateV5) -> Slate { + let SlateV5 { + num_parts: num_participants, + id, + sta, + coms: _, + amt: amount, + fee: fee_fields, + feat: kernel_features, + ttl: ttl_cutoff_height, + off: offset, + sigs: participant_data, + ver, + proof: payment_proof, + feat_args, + } = slate.clone(); + let participant_data = map_vec!(participant_data, |data| ParticipantData::from(data)); + let version_info = VersionCompatInfo::from(&ver); + let payment_proof = match &payment_proof { + Some(p) => Some(PaymentInfo::from(p)), + None => None, + }; + let kernel_features_args = match &feat_args { + Some(a) => Some(KernelFeaturesArgs::from(a)), + None => None, + }; + let state = SlateState::from(&sta); + Slate { + num_participants, + id, + state, + tx: (&slate).into(), + amount, + fee_fields, + kernel_features, + ttl_cutoff_height, + offset, + participant_data, + version_info, + payment_proof, + kernel_features_args, + } + } +} + +pub fn tx_from_slate_v5(slate: &SlateV5) -> Option { + let coms = match slate.coms.as_ref() { + Some(c) => c, + None => return None, + }; + let secp = static_secp_instance(); + let secp = secp.lock(); + let mut calc_slate = Slate::blank(2, false); + calc_slate.fee_fields = slate.fee; + for d in slate.sigs.iter() { + calc_slate.participant_data.push(ParticipantData { + public_blind_excess: d.xs, + public_nonce: d.nonce, + part_sig: d.part, + }); + } + let excess = match calc_slate.calc_excess(&secp) { + Ok(e) => e, + Err(_) => Commitment::from_vec(vec![0]), + }; + let excess_sig = match calc_slate.finalize_signature(&secp) { + Ok(s) => s, + Err(_) => Signature::from_raw_data(&[0; 64]).unwrap(), + }; + let kernel = TxKernel { + features: match slate.feat { + 0 => KernelFeatures::Plain { fee: slate.fee }, + 1 => KernelFeatures::HeightLocked { + fee: slate.fee, + lock_height: match slate.feat_args.as_ref() { + Some(a) => a.lock_hgt, + None => 0, + }, + }, + _ => KernelFeatures::Plain { fee: slate.fee }, + }, + excess, + excess_sig, + }; + let mut tx = Slate::empty_transaction().with_kernel(kernel); + + let mut outputs = vec![]; + let mut inputs = vec![]; + + for c in coms.iter() { + match &c.p { + Some(p) => { + outputs.push(Output::new(c.f.into(), c.c, p.clone())); + } + None => { + inputs.push(Input { + features: c.f.into(), + commit: c.c, + }); + } + } + } + + tx.body = tx + .body + .replace_inputs(inputs.as_slice().into()) + .replace_outputs(outputs.as_slice()); + tx.offset = slate.off.clone(); + Some(tx) +} + +// Node's Transaction object and lock height to SlateV5 `coms` +impl From<&SlateV5> for Option { + fn from(slate: &SlateV5) -> Option { + tx_from_slate_v5(slate) + } +} + +impl From<&ParticipantDataV5> for ParticipantData { + fn from(data: &ParticipantDataV5) -> ParticipantData { + let ParticipantDataV5 { + xs: public_blind_excess, + nonce: public_nonce, + part: part_sig, + } = data; + let public_blind_excess = *public_blind_excess; + let public_nonce = *public_nonce; + let part_sig = *part_sig; + ParticipantData { + public_blind_excess, + public_nonce, + part_sig, + } + } +} + +impl From<&KernelFeaturesArgsV5> for KernelFeaturesArgs { + fn from(data: &KernelFeaturesArgsV5) -> KernelFeaturesArgs { + let KernelFeaturesArgsV5 { lock_hgt } = data; + let lock_height = *lock_hgt; + KernelFeaturesArgs { lock_height } + } +} + +impl From<&SlateStateV5> for SlateState { + fn from(data: &SlateStateV5) -> SlateState { + match data { + SlateStateV5::Unknown => SlateState::Unknown, + SlateStateV5::Standard1 => SlateState::Standard1, + SlateStateV5::Standard2 => SlateState::Standard2, + SlateStateV5::Standard3 => SlateState::Standard3, + SlateStateV5::Invoice1 => SlateState::Invoice1, + SlateStateV5::Invoice2 => SlateState::Invoice2, + SlateStateV5::Invoice3 => SlateState::Invoice3, + } + } +} + +impl From<&VersionCompatInfoV5> for VersionCompatInfo { + fn from(data: &VersionCompatInfoV5) -> VersionCompatInfo { + let VersionCompatInfoV5 { + version, + block_header_version, + } = data; + let version = *version; + let block_header_version = *block_header_version; + VersionCompatInfo { + version, + block_header_version, + } + } +} + +impl From<&PaymentInfoV5> for PaymentInfo { + fn from(data: &PaymentInfoV5) -> PaymentInfo { + let PaymentInfoV5 { + saddr: sender_address, + raddr: receiver_address, + psig: promise_signature, + ts: timestamp, + memo, + } = data; + let sender_address = *sender_address; + let receiver_address = *receiver_address; + let promise_signature = *promise_signature; + let timestamp = *timestamp; + let memo: Option = match memo { + Some(m) => { + //memo_ret.copy_from_slice(&grin_util::from_hex(m.memo).unwrap_or_default()[0..32]); + Some(PaymentMemo { + memo_type: m.memo_type, + memo: m.memo, + }) + } + None => None, + }; + PaymentInfo { + sender_address: Some(sender_address), + receiver_address, + promise_signature: promise_signature, + timestamp, + memo, + } + } +} + +impl From for OutputFeatures { + fn from(of: OutputFeaturesV5) -> OutputFeatures { + match of.0 { + 1 => OutputFeatures::Coinbase, + 0 | _ => OutputFeatures::Plain, + } + } +} + +///////// V4 impl From for SlateV4 { fn from(slate: Slate) -> SlateV4 { let Slate { @@ -858,14 +1361,13 @@ impl From<&KernelFeaturesArgs> for KernelFeaturesArgsV4 { impl From<&VersionCompatInfo> for VersionCompatInfoV4 { fn from(data: &VersionCompatInfo) -> VersionCompatInfoV4 { let VersionCompatInfo { - version, + version: _, block_header_version, } = data; - let version = *version; - let block_header_version = *block_header_version; + // See VersionCompatInfoV5: the declared version always matches the structure. VersionCompatInfoV4 { - version, - block_header_version, + version: 4, + block_header_version: *block_header_version, } } } @@ -875,13 +1377,24 @@ impl From<&PaymentInfo> for PaymentInfoV4 { let PaymentInfo { sender_address, receiver_address, - receiver_signature, + promise_signature: receiver_signature, + timestamp: _, + memo: _, } = data; let sender_address = *sender_address; + // As in the V5 downgrade: an all-zero placeholder key for a missing sender address. + // It can never be accepted as a real sender because the legacy verify path binds the + // sender address into the signed message (internal::tx::payment_proof_message, checked + // in api_impl::owner::verify_payment_proof). V4 carries only the legacy receiver + // signature, so the placeholder is inert. From is infallible by design. + let saddr = match sender_address { + Some(a) => a, + None => DalekPublicKey::from_bytes(&[0u8; 32]).unwrap(), + }; let receiver_address = *receiver_address; let receiver_signature = *receiver_signature; PaymentInfoV4 { - saddr: sender_address, + saddr, raddr: receiver_address, rsig: receiver_signature, } @@ -1084,9 +1597,11 @@ impl From<&PaymentInfoV4> for PaymentInfo { let receiver_address = *receiver_address; let receiver_signature = *receiver_signature; PaymentInfo { - sender_address, + sender_address: Some(sender_address), receiver_address, - receiver_signature, + promise_signature: receiver_signature, + timestamp: DateTime::from_timestamp(0, 0).unwrap(), + memo: None, } } } @@ -1099,3 +1614,71 @@ impl From for OutputFeatures { } } } + +#[cfg(test)] +mod test { + use super::*; + use crate::grin_keychain::ExtKeychain; + + // A counterparty controls the participant list, so it can be shorter than + // num_participants claims, or hold an entry that matches only one of our two keys. + fn context_and_keys() -> (ExtKeychain, Context, PublicKey, PublicKey) { + let keychain = ExtKeychain::from_random_seed(true).unwrap(); + let parent_key_id = ExtKeychain::derive_key_id(1, 0, 0, 0, 0); + let context = Context::new(keychain.secp(), &parent_key_id, true, true); + let our_excess = PublicKey::from_secret_key(keychain.secp(), &context.sec_key).unwrap(); + let our_nonce = PublicKey::from_secret_key(keychain.secp(), &context.sec_nonce).unwrap(); + (keychain, context, our_excess, our_nonce) + } + + #[test] + fn find_index_handles_short_participant_list() { + let (keychain, context, _, _) = context_and_keys(); + // A key that matches neither of ours, so the search runs off the end of the list + let other_id = ExtKeychain::derive_key_id(1, 1, 0, 0, 0); + let other_key = keychain + .derive_key(0, &other_id, SwitchCommitmentType::Regular) + .unwrap(); + let other = PublicKey::from_secret_key(keychain.secp(), &other_key).unwrap(); + + let mut slate = Slate::blank(2, false); + // Claims two participants but carries one, and it is not ours + slate.num_participants = 2; + slate.participant_data.push(ParticipantData { + public_blind_excess: other, + public_nonce: other, + part_sig: None, + }); + assert!(slate + .find_index_matching_context(&keychain, &context) + .is_err()); + } + + #[test] + fn find_index_requires_both_keys_to_match() { + let (keychain, context, our_excess, our_nonce) = context_and_keys(); + let mut slate = Slate::blank(2, false); + // Shares our nonce but not our excess, so it is not our entry + slate.participant_data.push(ParticipantData { + public_blind_excess: our_nonce, + public_nonce: our_nonce, + part_sig: None, + }); + assert!(slate + .find_index_matching_context(&keychain, &context) + .is_err()); + + // Both keys match, so this one is ours + slate.participant_data.push(ParticipantData { + public_blind_excess: our_excess, + public_nonce: our_nonce, + part_sig: None, + }); + assert_eq!( + slate + .find_index_matching_context(&keychain, &context) + .unwrap(), + 1 + ); + } +} diff --git a/libwallet/src/slate_versions/mod.rs b/libwallet/src/slate_versions/mod.rs index ca2e749f8..41797abf1 100644 --- a/libwallet/src/slate_versions/mod.rs +++ b/libwallet/src/slate_versions/mod.rs @@ -20,6 +20,8 @@ use crate::slate::Slate; use crate::slate_versions::v4::{CoinbaseV4, SlateV4}; use crate::slate_versions::v4_bin::SlateV4Bin; +use crate::slate_versions::v5::{CoinbaseV5, SlateV5}; +use crate::slate_versions::v5_bin::SlateV5Bin; use crate::types::CbData; use crate::Error; use std::convert::TryFrom; @@ -30,9 +32,13 @@ pub mod ser; pub mod v4; #[allow(missing_docs)] pub mod v4_bin; +#[allow(missing_docs)] +pub mod v5; +#[allow(missing_docs)] +pub mod v5_bin; /// The most recent version of the slate -pub const CURRENT_SLATE_VERSION: u16 = 4; +pub const CURRENT_SLATE_VERSION: u16 = 5; /// The grin block header this slate is intended to be compatible with pub const GRIN_BLOCK_HEADER_VERSION: u16 = 3; @@ -40,16 +46,33 @@ pub const GRIN_BLOCK_HEADER_VERSION: u16 = 3; /// Existing versions of the slate #[derive(EnumIter, Serialize, Deserialize, Clone, Debug, PartialEq, PartialOrd, Eq, Ord)] pub enum SlateVersion { - /// V4 (most current) + /// V5 (Most Current) + V5, + /// V4 V4, } +impl SlateVersion { + /// The lowest version that can represent this slate without losing data, so a slate + /// is only sent as V5 when it has to be. V4 carries the payment proof's sender and + /// receiver addresses and its signature, but neither the timestamp nor the memo, so + /// only a proof using one of those needs V5. A legacy send proof sets neither. + pub fn lowest_for(slate: &Slate) -> SlateVersion { + match &slate.payment_proof { + Some(p) if p.memo.is_some() || p.timestamp.timestamp() != 0 => SlateVersion::V5, + _ => SlateVersion::V4, + } + } +} + #[derive(Debug, Serialize, Deserialize)] #[serde(untagged)] /// Versions are ordered newest to oldest so serde attempts to /// deserialize newer versions first, then falls back to older versions. pub enum VersionedSlate { - /// Current (4.0.0 Onwards ) + /// Current (5.0.0 Onwards?) + V5(SlateV5), + /// Current (4.0.0) V4(SlateV4), } @@ -57,6 +80,7 @@ impl VersionedSlate { /// Return slate version pub fn version(&self) -> SlateVersion { match *self { + VersionedSlate::V5(_) => SlateVersion::V5, VersionedSlate::V4(_) => SlateVersion::V4, } } @@ -64,6 +88,7 @@ impl VersionedSlate { /// convert this slate type to a specified older version pub fn into_version(slate: Slate, version: SlateVersion) -> Result { match version { + SlateVersion::V5 => Ok(VersionedSlate::V5(slate.into())), SlateVersion::V4 => Ok(VersionedSlate::V4(slate.into())), } } @@ -72,6 +97,7 @@ impl VersionedSlate { impl From for Slate { fn from(slate: VersionedSlate) -> Slate { match slate { + VersionedSlate::V5(s) => Slate::from(s), VersionedSlate::V4(s) => Slate::from(s), } } @@ -81,7 +107,11 @@ impl From for Slate { #[serde(untagged)] /// Binary versions, can only be parsed 1:1 into the appropriate /// version, and VersionedSlate can up/downgrade from there +/// NB (IMPORTANT): Ensure the slates are listed in reverse chronological +/// order (latest first) pub enum VersionedBinSlate { + /// Version 5, binary + V5(SlateV5Bin), /// Version 4, binary V4(SlateV4Bin), } @@ -90,6 +120,7 @@ impl TryFrom for VersionedBinSlate { type Error = Error; fn try_from(slate: VersionedSlate) -> Result { match slate { + VersionedSlate::V5(s) => Ok(VersionedBinSlate::V5(SlateV5Bin(s))), VersionedSlate::V4(s) => Ok(VersionedBinSlate::V4(SlateV4Bin(s))), } } @@ -98,6 +129,7 @@ impl TryFrom for VersionedBinSlate { impl From for VersionedSlate { fn from(slate: VersionedBinSlate) -> VersionedSlate { match slate { + VersionedBinSlate::V5(s) => VersionedSlate::V5(s.0), VersionedBinSlate::V4(s) => VersionedSlate::V4(s.0), } } @@ -109,6 +141,8 @@ impl From for VersionedSlate { /// deserialize newer versions first, then falls back to older versions. pub enum VersionedCoinbase { /// Current supported coinbase version. + V5(CoinbaseV5), + /// Previous version (no difference) V4(CoinbaseV4), } @@ -116,7 +150,298 @@ impl VersionedCoinbase { /// convert this coinbase data to a specific versioned representation for the json api. pub fn into_version(cb: CbData, version: SlateVersion) -> VersionedCoinbase { match version { + SlateVersion::V5 => VersionedCoinbase::V5(cb.into()), SlateVersion::V4 => VersionedCoinbase::V4(cb.into()), } } } +/// Shared slate fixtures, used by the version conversion tests +#[cfg(test)] +pub mod tests { + use crate::grin_core::core::transaction::OutputFeatures; + use crate::grin_util::from_hex; + use crate::grin_util::secp::key::PublicKey; + use crate::grin_util::secp::pedersen::{Commitment, RangeProof}; + use crate::grin_util::secp::Signature; + use crate::slate::{KernelFeaturesArgs, ParticipantData, PaymentInfo, PaymentMemo}; + use crate::slate_versions::v5::{CommitsV5, SlateV5}; + use crate::{ + slate, Error, Slate, SlateVersion, Slatepacker, SlatepackerArgs, VersionedBinSlate, + VersionedSlate, + }; + use chrono::{DateTime, Utc}; + use ed25519_dalek::Signature as DalekSignature; + use ed25519_dalek::VerifyingKey as DalekPublicKey; + use grin_core::global::{set_local_chain_type, ChainTypes}; + use grin_keychain::{ExtKeychain, Keychain, SwitchCommitmentType}; + use grin_wallet_util::byte_ser; + use std::convert::TryInto; + + /// Populate a test internal slate with all fields to test conversions + pub fn populate_test_slate() -> Result { + let keychain = ExtKeychain::from_random_seed(true).unwrap(); + let switch = SwitchCommitmentType::Regular; + + let mut slate_internal = Slate::blank(2, false); + let id1 = ExtKeychain::derive_key_id(1, 1, 0, 0, 0); + let id2 = ExtKeychain::derive_key_id(1, 1, 1, 0, 0); + let skey1 = keychain.derive_key(0, &id1, switch).unwrap(); + let skey2 = keychain.derive_key(0, &id2, switch).unwrap(); + let xs = PublicKey::from_secret_key(keychain.secp(), &skey1).unwrap(); + let nonce = PublicKey::from_secret_key(keychain.secp(), &skey2).unwrap(); + + let part = ParticipantData { + public_blind_excess: xs, + public_nonce: nonce, + part_sig: None, + }; + let part2 = ParticipantData { + public_blind_excess: xs, + public_nonce: nonce, + part_sig: Some(Signature::from_raw_data(&[11; 64]).unwrap()), + }; + slate_internal.participant_data.push(part.clone()); + slate_internal.participant_data.push(part2); + slate_internal.participant_data.push(part); + + // Another temp slate to convert commit data into internal 'transaction' like data + // add some random commit data + let slate_tmp = Slate::blank(1, false); + let mut v5 = SlateV5::from(slate_tmp); + + let com1 = CommitsV5 { + f: OutputFeatures::Plain.into(), + c: Commitment::from_vec([3u8; 1].to_vec()), + p: None, + }; + let com2 = CommitsV5 { + f: OutputFeatures::Plain.into(), + c: Commitment::from_vec([4u8; 1].to_vec()), + p: Some(RangeProof::zero()), + }; + + let mut coms = vec![]; + coms.push(com1.clone()); + coms.push(com1.clone()); + coms.push(com1.clone()); + coms.push(com2); + + v5.coms = Some(coms); + + slate_internal.tx = slate::tx_from_slate_v5(&v5); + + // basic fields + slate_internal.amount = 23820323; + slate_internal.kernel_features = 1; + slate_internal.num_participants = 2; + slate_internal.kernel_features_args = Some(KernelFeaturesArgs { + lock_height: 2323223, + }); + + // current style payment proof + let raw_pubkey_str = "d03c09e9c19bb74aa9ea44e0fe5ae237a9bf40bddf0941064a80913a4459c8bb"; + let b = from_hex(raw_pubkey_str).unwrap(); + let b = <&[u8; 32]>::try_from(b.as_slice()).unwrap(); + let d_pkey = DalekPublicKey::from_bytes(b).unwrap(); + // Need to remove milliseconds component for comparison. Won't be serialized + let ts = DateTime::from_timestamp(Utc::now().timestamp(), 0).unwrap(); + let pm = PaymentMemo { + memo_type: 1, + memo: [9; 32], + }; + + let psig = DalekSignature::from_bytes(&[0u8; 64]); + slate_internal.payment_proof = Some(PaymentInfo { + sender_address: Some(d_pkey.clone()), + receiver_address: d_pkey.clone(), + timestamp: ts.clone(), + promise_signature: Some(psig), + memo: Some(pm), + }); + + Ok(slate_internal) + } + + #[test] + fn ser_deser_current_slate() -> Result<(), Error> { + let slate_internal = populate_test_slate()?; + // Serialize slate into slatepack + let slatepacker_args = SlatepackerArgs { + sender: None, + recipients: vec![], + dec_key: None, + }; + + let slate_packer = Slatepacker::new(slatepacker_args); + let slate_packed = slate_packer.create_slatepack(&slate_internal).unwrap(); + + let slate_unpacked = slate_packer.get_slate(&slate_packed).unwrap(); + + // Just verifying payment proof for now, extend later to cover EQ for full slate if needs + // be + assert_eq!(slate_internal.payment_proof, slate_unpacked.payment_proof); + Ok(()) + } + + #[test] + fn create_slatepack_emits_lowest_version() -> Result<(), Error> { + set_local_chain_type(ChainTypes::Mainnet); + let packer = Slatepacker::new(SlatepackerArgs { + sender: None, + recipients: vec![], + dec_key: None, + }); + + // A slate carrying a payment proof needs V5 (the proof carries memo/timestamp + // that V4 can't represent), and the V5 fields survive the round trip. + let slate_with_proof = populate_test_slate()?; + assert!(slate_with_proof.payment_proof.is_some()); + let recovered_v5 = packer.get_slate(&packer.create_slatepack(&slate_with_proof)?)?; + assert_eq!(recovered_v5.version_info.version, 5); + assert!(recovered_v5.payment_proof.unwrap().memo.is_some()); + + // Without a proof, V4 is sufficient and is emitted for interoperability with V4 peers. + let mut slate_no_proof = populate_test_slate()?; + slate_no_proof.payment_proof = None; + let recovered_v4 = packer.get_slate(&packer.create_slatepack(&slate_no_proof)?)?; + assert_eq!(recovered_v4.version_info.version, 4); + + // A legacy send proof uses no V5 field: the timestamp is left at the epoch and + // there is no memo. It must stay V4 so V4-only wallets can still read the slate, + // and the proof has to survive the round trip intact. + let mut slate_legacy_proof = populate_test_slate()?; + let mut proof = slate_legacy_proof.payment_proof.clone().unwrap(); + proof.timestamp = DateTime::from_timestamp(0, 0).unwrap(); + proof.memo = None; + slate_legacy_proof.payment_proof = Some(proof.clone()); + let recovered_legacy = packer.get_slate(&packer.create_slatepack(&slate_legacy_proof)?)?; + assert_eq!(recovered_legacy.version_info.version, 4); + let recovered_proof = recovered_legacy.payment_proof.unwrap(); + assert_eq!(recovered_proof.sender_address, proof.sender_address); + assert_eq!(recovered_proof.receiver_address, proof.receiver_address); + assert_eq!(recovered_proof.promise_signature, proof.promise_signature); + + Ok(()) + } + + #[test] + fn slatepack_version_v4_v5() -> Result<(), Error> { + set_local_chain_type(ChainTypes::Mainnet); + + // Convert V5 slate into V4 slate, check result + let slate_internal = populate_test_slate()?; + let v5 = VersionedSlate::V5(slate_internal.clone().into()); + let v4 = VersionedSlate::V4(slate_internal.into()); + + let v5_converted: Slate = v5.into(); + let v4_converted: Slate = v4.into(); + + assert!(v5_converted.payment_proof.as_ref().unwrap().memo.is_some()); + + // Converted from v4 will not have memos and ts will be zeroed out + assert!(v4_converted.payment_proof.as_ref().unwrap().memo.is_none()); + assert_eq!( + v4_converted + .payment_proof + .as_ref() + .unwrap() + .timestamp + .timestamp(), + 0 + ); + + Ok(()) + } + + // A slate must deserialize back into the version it declares. VersionedSlate is + // untagged and tries V5 first, so without a version check in each deserializer a + // V4 slate is silently parsed (and reported) as V5. + #[test] + fn versioned_slate_json_round_trip() -> Result<(), Error> { + set_local_chain_type(ChainTypes::Mainnet); + let slate_internal = populate_test_slate()?; + + // A slate without a payment proof is the case that matters: the V4 and V5 proof + // shapes differ, so a proof makes the variants distinguishable by accident. + for with_proof in [true, false] { + for version in [SlateVersion::V4, SlateVersion::V5] { + let mut slate = slate_internal.clone(); + if !with_proof { + slate.payment_proof = None; + } + slate.version_info.version = match version { + SlateVersion::V4 => 4, + SlateVersion::V5 => 5, + }; + let versioned = VersionedSlate::into_version(slate, version.clone())?; + let json = serde_json::to_string(&versioned).unwrap(); + let recovered: VersionedSlate = serde_json::from_str(&json).unwrap(); + assert_eq!(recovered.version(), version, "json round trip: {}", json); + } + } + + Ok(()) + } + + #[test] + fn versioned_bin_slate_round_trip() -> Result<(), Error> { + set_local_chain_type(ChainTypes::Mainnet); + let slate_internal = populate_test_slate()?; + + for with_proof in [true, false] { + for version in [SlateVersion::V4, SlateVersion::V5] { + let mut slate = slate_internal.clone(); + if !with_proof { + slate.payment_proof = None; + } + slate.version_info.version = match version { + SlateVersion::V4 => 4, + SlateVersion::V5 => 5, + }; + let versioned = VersionedSlate::into_version(slate, version.clone())?; + let bin: VersionedBinSlate = versioned.try_into().unwrap(); + let bytes = byte_ser::to_bytes(&bin).unwrap(); + let recovered: VersionedSlate = byte_ser::from_bytes::(&bytes) + .unwrap() + .into(); + assert_eq!(recovered.version(), version); + } + } + + Ok(()) + } + + #[test] + fn slatepack_version_v4_v5_bin() -> Result<(), Error> { + set_local_chain_type(ChainTypes::Mainnet); + + // Convert V5 slate into V4 slate, check result + let slate_internal = populate_test_slate()?; + let v5 = VersionedSlate::V5(slate_internal.clone().into()); + let v5_bin: VersionedBinSlate = v5.try_into().unwrap(); + + let v4 = VersionedSlate::V4(slate_internal.into()); + let v4_bin: VersionedBinSlate = v4.try_into().unwrap(); + + let v5_versioned: VersionedSlate = v5_bin.into(); + let v4_versioned: VersionedSlate = v4_bin.into(); + + let v5_converted: Slate = v5_versioned.into(); + let v4_converted: Slate = v4_versioned.into(); + + assert!(v5_converted.payment_proof.as_ref().unwrap().memo.is_some()); + // Converted from v4 will not have memos and ts will be zeroed out + assert!(v4_converted.payment_proof.as_ref().unwrap().memo.is_none()); + assert_eq!( + v4_converted + .payment_proof + .as_ref() + .unwrap() + .timestamp + .timestamp(), + 0 + ); + + Ok(()) + } +} diff --git a/libwallet/src/slate_versions/ser.rs b/libwallet/src/slate_versions/ser.rs index 8e3510c76..7a7a43138 100644 --- a/libwallet/src/slate_versions/ser.rs +++ b/libwallet/src/slate_versions/ser.rs @@ -131,7 +131,7 @@ pub mod ov3_serde { /// Serializes an ed25519 PublicKey to and from hex pub mod dalek_seckey_serde { use crate::grin_util::{from_hex, ToHex}; - use ed25519_dalek::SecretKey as DalekSecretKey; + use ed25519_dalek::SigningKey as DalekSecretKey; use serde::{Deserialize, Deserializer, Serializer}; /// @@ -151,7 +151,9 @@ pub mod dalek_seckey_serde { String::deserialize(deserializer) .and_then(|string| from_hex(&string).map_err(|err| Error::custom(err.to_string()))) .and_then(|bytes: Vec| { - DalekSecretKey::from_bytes(&bytes).map_err(|err| Error::custom(err.to_string())) + let b = <&[u8; 32]>::try_from(bytes.as_slice()) + .map_err(|_| Error::custom("Error deserializing secret key"))?; + Ok(DalekSecretKey::from_bytes(b)) }) } } @@ -159,7 +161,7 @@ pub mod dalek_seckey_serde { /// Serializes an ed25519 PublicKey to and from hex pub mod dalek_pubkey_serde { use crate::grin_util::{from_hex, ToHex}; - use ed25519_dalek::PublicKey as DalekPublicKey; + use ed25519_dalek::VerifyingKey as DalekPublicKey; use serde::{Deserialize, Deserializer, Serializer}; /// @@ -179,12 +181,17 @@ pub mod dalek_pubkey_serde { String::deserialize(deserializer) .and_then(|string| from_hex(&string).map_err(|err| Error::custom(err.to_string()))) .and_then(|bytes: Vec| { - DalekPublicKey::from_bytes(&bytes).map_err(|err| Error::custom(err.to_string())) + let b = <&[u8; 32]>::try_from(bytes.as_slice()) + .map_err(|_| Error::custom("Error deserializing public key"))?; + Ok(match DalekPublicKey::from_bytes(b) { + Ok(pk) => pk, + Err(e) => return Err(Error::custom(format!("{:?}", e))), + }) }) } } -/// Serializes an x25519 PublicKey to and from hex +/// Serializes a x25519 PublicKey to and from hex pub mod dalek_xpubkey_serde { use crate::grin_util::{from_hex, ToHex}; use serde::{Deserialize, Deserializer, Serializer}; @@ -207,6 +214,9 @@ pub mod dalek_xpubkey_serde { String::deserialize(deserializer) .and_then(|string| from_hex(&string).map_err(|err| Error::custom(err.to_string()))) .and_then(|bytes: Vec| { + if bytes.len() != 32 { + return Err(Error::custom("unexpected byte length")); + } let mut b = [0u8; 32]; b.copy_from_slice(&bytes[0..32]); Ok(xDalekPublicKey::from(b)) @@ -217,7 +227,7 @@ pub mod dalek_xpubkey_serde { /// Serializes an ed25519 PublicKey to and from base64 pub mod dalek_pubkey_base64 { use base64; - use ed25519_dalek::PublicKey as DalekPublicKey; + use ed25519_dalek::VerifyingKey as DalekPublicKey; use serde::{Deserialize, Deserializer, Serializer}; /// @@ -239,7 +249,12 @@ pub mod dalek_pubkey_base64 { base64::decode(&string).map_err(|err| Error::custom(err.to_string())) }) .and_then(|bytes: Vec| { - DalekPublicKey::from_bytes(&bytes).map_err(|err| Error::custom(err.to_string())) + let b = <&[u8; 32]>::try_from(bytes.as_slice()) + .map_err(|_| Error::custom("Error deserializing public key"))?; + Ok(match DalekPublicKey::from_bytes(b) { + Ok(pk) => pk, + Err(e) => return Err(Error::custom(format!("{:?}", e))), + }) }) } } @@ -247,7 +262,7 @@ pub mod dalek_pubkey_base64 { /// Serializes an Option to and from hex pub mod option_dalek_pubkey_base64 { use base64; - use ed25519_dalek::PublicKey as DalekPublicKey; + use ed25519_dalek::VerifyingKey as DalekPublicKey; use serde::de::Error; use serde::{Deserialize, Deserializer, Serializer}; @@ -271,6 +286,9 @@ pub mod option_dalek_pubkey_base64 { Some(string) => base64::decode(&string) .map_err(|err| Error::custom(err.to_string())) .and_then(|bytes: Vec| { + if bytes.len() != 32 { + return Err(Error::custom("unexpected byte length")); + } let mut b = [0u8; 32]; b.copy_from_slice(&bytes[0..32]); DalekPublicKey::from_bytes(&b) @@ -287,7 +305,7 @@ pub mod option_dalek_pubkey_base64 { /// Serializes an Option to and from hex pub mod option_dalek_pubkey_serde { - use ed25519_dalek::PublicKey as DalekPublicKey; + use ed25519_dalek::VerifyingKey as DalekPublicKey; use serde::de::Error; use serde::{Deserialize, Deserializer, Serializer}; @@ -313,6 +331,9 @@ pub mod option_dalek_pubkey_serde { Some(string) => from_hex(&string) .map_err(|err| Error::custom(err.to_string())) .and_then(|bytes: Vec| { + if bytes.len() != 32 { + return Err(Error::custom("unexpected byte length")); + } let mut b = [0u8; 32]; b.copy_from_slice(&bytes[0..32]); DalekPublicKey::from_bytes(&b) @@ -352,6 +373,9 @@ pub mod option_xdalek_pubkey_serde { Some(string) => from_hex(&string) .map_err(|err| Error::custom(err.to_string())) .and_then(|bytes: Vec| { + if bytes.len() != 32 { + return Err(Error::custom("unexpected byte length")); + } let mut b = [0u8; 32]; b.copy_from_slice(&bytes[0..32]); Ok(Some(xDalekPublicKey::from(b))) @@ -472,7 +496,7 @@ pub mod option_dalek_sig_base64 { } } -/// Serializes slates 'version_info' field +/// Serializes slates 'version_info' field - V4 pub mod version_info_v4 { use serde::de::Error; use serde::{Deserialize, Deserializer, Serializer}; @@ -502,7 +526,15 @@ pub mod version_info_v4 { return Err(Error::custom("Cannot parse version")); } match u16::from_str_radix(v[0], 10) { - Ok(u) => retval.version = u, + // VersionedSlate is untagged, so the declared version must be checked here: + // otherwise a slate is silently parsed as the wrong variant. + Ok(4) => retval.version = 4, + Ok(u) => { + return Err(Error::custom(format!( + "Expected a V4 slate, found version {}", + u + ))) + } Err(e) => return Err(Error::custom(format!("Cannot parse version: {}", e))), } match u16::from_str_radix(v[1], 10) { @@ -514,7 +546,57 @@ pub mod version_info_v4 { } } -/// Serializes slates 'state' field +/// Serializes slates 'version_info' field - V5 +pub mod version_info_v5 { + use serde::de::Error; + use serde::{Deserialize, Deserializer, Serializer}; + + use crate::slate_versions::v5::VersionCompatInfoV5; + + /// + pub fn serialize(v: &VersionCompatInfoV5, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&format!("{}:{}", v.version, v.block_header_version)) + } + + /// + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + String::deserialize(deserializer).and_then(|s| { + let mut retval = VersionCompatInfoV5 { + version: 0, + block_header_version: 0, + }; + let v: Vec<&str> = s.split(':').collect(); + if v.len() != 2 { + return Err(Error::custom("Cannot parse version")); + } + match u16::from_str_radix(v[0], 10) { + // See the V4 deserializer: the declared version disambiguates the + // untagged VersionedSlate variants. + Ok(5) => retval.version = 5, + Ok(u) => { + return Err(Error::custom(format!( + "Expected a V5 slate, found version {}", + u + ))) + } + Err(e) => return Err(Error::custom(format!("Cannot parse version: {}", e))), + } + match u16::from_str_radix(v[1], 10) { + Ok(u) => retval.block_header_version = u, + Err(e) => return Err(Error::custom(format!("Cannot parse version: {}", e))), + } + Ok(retval) + }) + } +} + +/// Serializes slates 'state' field - V4 pub mod slate_state_v4 { use serde::de::Error; use serde::{Deserialize, Deserializer, Serializer}; @@ -559,6 +641,51 @@ pub mod slate_state_v4 { } } +/// Serializes slates 'state' field - V5 +pub mod slate_state_v5 { + use serde::de::Error; + use serde::{Deserialize, Deserializer, Serializer}; + + use crate::slate_versions::v5::SlateStateV5; + + /// + pub fn serialize(st: &SlateStateV5, serializer: S) -> Result + where + S: Serializer, + { + let label = match st { + SlateStateV5::Unknown => "NA", + SlateStateV5::Standard1 => "S1", + SlateStateV5::Standard2 => "S2", + SlateStateV5::Standard3 => "S3", + SlateStateV5::Invoice1 => "I1", + SlateStateV5::Invoice2 => "I2", + SlateStateV5::Invoice3 => "I3", + }; + serializer.serialize_str(label) + } + + /// + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + String::deserialize(deserializer).and_then(|s| { + let retval = match s.as_str() { + "NA" => SlateStateV5::Unknown, + "S1" => SlateStateV5::Standard1, + "S2" => SlateStateV5::Standard2, + "S3" => SlateStateV5::Standard3, + "I1" => SlateStateV5::Invoice1, + "I2" => SlateStateV5::Invoice2, + "I3" => SlateStateV5::Invoice3, + _ => return Err(Error::custom("Invalid Slate state")), + }; + Ok(retval) + }) + } +} + /// Serializes an secp256k1 pubkey to base64 pub mod uuid_base64 { use base64; @@ -584,6 +711,9 @@ pub mod uuid_base64 { base64::decode(&string).map_err(|err| Error::custom(err.to_string())) }) .and_then(|bytes: Vec| { + if bytes.len() != 16 { + return Err(Error::custom("unexpected byte length")); + } let mut b = [0u8; 16]; b.copy_from_slice(&bytes[0..16]); Ok(Uuid::from_bytes(b)) @@ -597,11 +727,10 @@ mod test { use rand::rngs::mock::StepRng; use crate::grin_util::{secp, static_secp_instance}; - use ed25519_dalek::Keypair; - use ed25519_dalek::PublicKey as DalekPublicKey; - use ed25519_dalek::SecretKey as DalekSecretKey; use ed25519_dalek::Signature as DalekSignature; use ed25519_dalek::Signer; + use ed25519_dalek::SigningKey as DalekSecretKey; + use ed25519_dalek::VerifyingKey as DalekPublicKey; use serde::Deserialize; use serde_json; @@ -624,15 +753,10 @@ mod test { let secp = secp_inst.lock(); let mut test_rng = StepRng::new(1234567890u64, 1); let sec_key = secp::key::SecretKey::new(&secp, &mut test_rng); - let d_skey = DalekSecretKey::from_bytes(&sec_key.0).unwrap(); + let d_skey = DalekSecretKey::from_bytes(&sec_key.0); let d_pub_key: DalekPublicKey = (&d_skey).into(); - let keypair = Keypair { - public: d_pub_key, - secret: d_skey, - }; - - let d_sig = keypair.sign("test sig".as_bytes()); + let d_sig = d_skey.sign("test sig".as_bytes()); println!("D sig: {:?}", d_sig); SerTest { @@ -657,4 +781,22 @@ mod test { assert_eq!(s, deserialized); } } + + #[test] + fn rejects_malformed_dalek_fields() { + // A too-short fixed-length field used to panic inside copy_from_slice during + // deserialization; it must now surface as an error instead. + let json = serde_json::to_string(&SerTest::random()).unwrap(); + let value: serde_json::Value = serde_json::from_str(&json).unwrap(); + + // Signature field shorter than 64 bytes (exercises the dalek_sig_serde guard). + let mut bad = value.clone(); + bad["sig"] = serde_json::Value::String("00".to_string()); + assert!(serde_json::from_value::(bad).is_err()); + + // Public-key field shorter than 32 bytes. + let mut bad = value; + bad["pub_key"] = serde_json::Value::String("00".to_string()); + assert!(serde_json::from_value::(bad).is_err()); + } } diff --git a/libwallet/src/slate_versions/v4.rs b/libwallet/src/slate_versions/v4.rs index 26c59e024..a3ba1b14a 100644 --- a/libwallet/src/slate_versions/v4.rs +++ b/libwallet/src/slate_versions/v4.rs @@ -62,8 +62,8 @@ use crate::grin_util::secp::key::PublicKey; use crate::grin_util::secp::pedersen::{Commitment, RangeProof}; use crate::grin_util::secp::Signature; use crate::{slate_versions::ser, CbData}; -use ed25519_dalek::PublicKey as DalekPublicKey; use ed25519_dalek::Signature as DalekSignature; +use ed25519_dalek::VerifyingKey as DalekPublicKey; use uuid::Uuid; #[derive(Serialize, Deserialize, Debug, Clone)] diff --git a/libwallet/src/slate_versions/v4_bin.rs b/libwallet/src/slate_versions/v4_bin.rs index 455af3f50..8a66a310e 100644 --- a/libwallet/src/slate_versions/v4_bin.rs +++ b/libwallet/src/slate_versions/v4_bin.rs @@ -21,8 +21,8 @@ use crate::grin_keychain::BlindingFactor; use crate::grin_util::secp::key::PublicKey; use crate::grin_util::secp::pedersen::{Commitment, RangeProof}; use crate::grin_util::secp::Signature; -use ed25519_dalek::PublicKey as DalekPublicKey; use ed25519_dalek::Signature as DalekSignature; +use ed25519_dalek::VerifyingKey as DalekPublicKey; use std::convert::TryFrom; use uuid::Uuid; @@ -64,7 +64,7 @@ impl Readable for SlateStateV4 { } /// Allow serializing of Uuids not defined in crate -struct UuidWrap(Uuid); +pub struct UuidWrap(pub Uuid); impl Writeable for UuidWrap { fn write(&self, writer: &mut W) -> Result<(), grin_ser::Error> { @@ -82,7 +82,7 @@ impl Readable for UuidWrap { } /// Helper struct to serialize optional fields efficiently -struct SlateOptFields { +pub struct SlateOptFields { /// num parts, default 2 pub num_parts: u8, /// amt, default 0 @@ -345,10 +345,18 @@ impl<'a> Writeable for ProofWrapRef<'a> { impl Readable for ProofWrap { fn read(reader: &mut R) -> Result { - let saddr = DalekPublicKey::from_bytes(&reader.read_fixed_bytes(32)?) + let saddr_bytes = reader.read_fixed_bytes(32)?; + let saddr_bytes = <&[u8; 32]>::try_from(saddr_bytes.as_slice()) .map_err(|_| grin_ser::Error::CorruptedData)?; - let raddr = DalekPublicKey::from_bytes(&reader.read_fixed_bytes(32)?) + let saddr = + DalekPublicKey::from_bytes(saddr_bytes).map_err(|_| grin_ser::Error::CorruptedData)?; + + let raddr_bytes = reader.read_fixed_bytes(32)?; + let raddr_bytes = <&[u8; 32]>::try_from(raddr_bytes.as_slice()) .map_err(|_| grin_ser::Error::CorruptedData)?; + let raddr = + DalekPublicKey::from_bytes(raddr_bytes).map_err(|_| grin_ser::Error::CorruptedData)?; + let rsig = match reader.read_u8()? { 0 => None, 1 | _ => Some( @@ -455,8 +463,17 @@ impl Writeable for SlateV4Bin { impl Readable for SlateV4Bin { fn read(reader: &mut R) -> Result { + // VersionedBinSlate is untagged, so reject a slate that does not declare V4 here; + // otherwise it can be parsed as the wrong variant. + let version = reader.read_u16()?; + if version != 4 { + return Err(grin_ser::Error::UnexpectedData { + expected: 4u16.to_be_bytes().to_vec(), + received: version.to_be_bytes().to_vec(), + }); + } let ver = VersionCompatInfoV4 { - version: reader.read_u16()?, + version, block_header_version: reader.read_u16()?, }; let id = UuidWrap::read(reader)?.0; @@ -500,6 +517,7 @@ fn slate_v4_serialize_deserialize() { use crate::Slate; use grin_core::global::{set_local_chain_type, ChainTypes}; use grin_keychain::{ExtKeychain, Keychain, SwitchCommitmentType}; + set_local_chain_type(ChainTypes::Mainnet); let slate = Slate::blank(1, false); let mut v4 = SlateV4::from(slate); @@ -573,8 +591,9 @@ fn slate_v4_serialize_deserialize() { // Include Payment proof, remove coms to mix it up a bit let mut v4 = v4_1_copy; let raw_pubkey_str = "d03c09e9c19bb74aa9ea44e0fe5ae237a9bf40bddf0941064a80913a4459c8bb"; - let b = from_hex(raw_pubkey_str).unwrap(); - let d_pkey = DalekPublicKey::from_bytes(&b).unwrap(); + let bytes = from_hex(raw_pubkey_str).unwrap(); + let b = <&[u8; 32]>::try_from(bytes.as_slice()).unwrap(); + let d_pkey = DalekPublicKey::from_bytes(b.as_array().unwrap()).unwrap(); v4.proof = Some(PaymentInfoV4 { raddr: d_pkey.clone(), saddr: d_pkey.clone(), diff --git a/libwallet/src/slate_versions/v5.rs b/libwallet/src/slate_versions/v5.rs new file mode 100644 index 000000000..33338f011 --- /dev/null +++ b/libwallet/src/slate_versions/v5.rs @@ -0,0 +1,382 @@ +// Copyright 2023 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Contains V5 of the slate (version as yet undetermined) +//! +//! TODO: Should be considered experimental and remain in an experimental branch +//! until such time as the relevant RFCs are accepted +//! +//! Changes from V4: +//! #### Top-Level Slate Struct +//! * +//! #### PaymentInfoV5 +//! * `saddr`, i.e. `sender_address` in main Slate becomes optional +//! * `rsig` is renamed to `psig`, corresponding to rename of `receiver_signature` to `promise_signature` in main Slate +//! +//! * `ts` added (`timestamp` in main slate). Serialized as i64 representing epoch time in seconds +//! * `memo` added as optional MemoV5 Struct, which contains: +//! * `memo_type`: u8 +//! * 0x00 = payment details directly embedded +//! * 0x01 = Blake2b hash of an arbitrary invoice document +//! * `memo`: [u8;32] the memo data itself + +use crate::grin_core::core::FeeFields; +use crate::grin_core::core::{Input, Output, TxKernel}; +use crate::grin_core::libtx::secp_ser; +use crate::grin_keychain::{BlindingFactor, Identifier}; +use crate::grin_util::secp; +use crate::grin_util::secp::key::PublicKey; +use crate::grin_util::secp::pedersen::{Commitment, RangeProof}; +use crate::grin_util::secp::Signature; +use crate::{slate_versions::ser, CbData}; +use chrono::prelude::{DateTime, Utc}; +use ed25519_dalek::Signature as DalekSignature; +use ed25519_dalek::VerifyingKey as DalekPublicKey; +use serde_with::TimestampSeconds; +use uuid::Uuid; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct SlateV5 { + // Required Fields + /// Versioning info + #[serde(with = "ser::version_info_v5")] + pub ver: VersionCompatInfoV5, + /// Unique transaction ID, selected by sender + pub id: Uuid, + /// Slate state + #[serde(with = "ser::slate_state_v5")] + pub sta: SlateStateV5, + /// Offset, modified by each participant inserting inputs + /// as the transaction progresses + #[serde( + serialize_with = "secp_ser::as_hex", + deserialize_with = "secp_ser::blind_from_hex" + )] + #[serde(default = "default_offset_zero")] + #[serde(skip_serializing_if = "offset_is_zero")] + pub off: BlindingFactor, + // Optional fields depending on state + /// The number of participants intended to take part in this transaction + #[serde(default = "default_num_participants_2")] + #[serde(skip_serializing_if = "num_parts_is_2")] + pub num_parts: u8, + /// base amount (excluding fee) + #[serde(with = "secp_ser::string_or_u64")] + #[serde(skip_serializing_if = "u64_is_blank")] + #[serde(default = "default_u64_0")] + pub amt: u64, + /// fee + #[serde(skip_serializing_if = "fee_is_zero")] + #[serde(default = "default_fee")] + pub fee: FeeFields, + /// kernel features, if any + #[serde(skip_serializing_if = "u8_is_blank")] + #[serde(default = "default_u8_0")] + pub feat: u8, + /// TTL, the block height at which wallets + /// should refuse to process the transaction and unlock all + #[serde(with = "secp_ser::string_or_u64")] + #[serde(skip_serializing_if = "u64_is_blank")] + #[serde(default = "default_u64_0")] + pub ttl: u64, + // Structs always required + /// Participant data, each participant in the transaction will + /// insert their public data here. For now, 0 is sender and 1 + /// is receiver, though this will change for multi-party + pub sigs: Vec, + // Situational, but required at some point in the tx + /// Inputs/Output commits added to slate + #[serde(default = "default_coms_none")] + #[serde(skip_serializing_if = "Option::is_none")] + pub coms: Option>, + // Optional Structs + /// Payment Proof + #[serde(default = "default_payment_none")] + #[serde(skip_serializing_if = "Option::is_none")] + pub proof: Option, + /// Kernel features arguments + #[serde(default = "default_kernel_features_none")] + #[serde(skip_serializing_if = "Option::is_none")] + pub feat_args: Option, +} + +fn default_payment_none() -> Option { + None +} + +fn default_offset_zero() -> BlindingFactor { + BlindingFactor::zero() +} + +fn offset_is_zero(o: &BlindingFactor) -> bool { + *o == BlindingFactor::zero() +} + +fn default_coms_none() -> Option> { + None +} + +fn default_u64_0() -> u64 { + 0 +} + +fn num_parts_is_2(n: &u8) -> bool { + *n == 2 +} + +fn default_num_participants_2() -> u8 { + 2 +} + +fn default_kernel_features_none() -> Option { + None +} + +/// Slate state definition +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum SlateStateV5 { + /// Unknown, coming from earlier versions of the slate + Unknown, + /// Standard flow, freshly init + Standard1, + /// Standard flow, return journey + Standard2, + /// Standard flow, ready for transaction posting + Standard3, + /// Invoice flow, freshly init + Invoice1, + ///Invoice flow, return journey + Invoice2, + /// Invoice flow, ready for tranasction posting + Invoice3, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +/// Kernel features arguments definition +pub struct KernelFeaturesArgsV5 { + /// Lock height, for HeightLocked + pub lock_hgt: u64, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct VersionCompatInfoV5 { + /// The current version of the slate format + pub version: u16, + /// Version of grin block header this slate is compatible with + pub block_header_version: u16, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct ParticipantDataV5 { + /// Public key corresponding to private blinding factor + #[serde(with = "secp_ser::pubkey_serde")] + pub xs: PublicKey, + /// Public key corresponding to private nonce + #[serde(with = "secp_ser::pubkey_serde")] + pub nonce: PublicKey, + /// Public partial signature + #[serde(default = "default_part_sig_none")] + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(with = "secp_ser::option_sig_serde")] + pub part: Option, +} + +fn default_part_sig_none() -> Option { + None +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct PaymentMemoV5 { + // The type of memo + // 0x00 is directly embedded additional payment details + // 0x01 represents the blake2b hash of an arbitrary invoice document + pub memo_type: u8, + // memo data itself + pub memo: [u8; 32], +} + +#[serde_as] +#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] +pub struct PaymentInfoV5 { + #[serde(with = "ser::dalek_pubkey_serde")] + pub saddr: DalekPublicKey, + #[serde(with = "ser::dalek_pubkey_serde")] + pub raddr: DalekPublicKey, + #[serde_as(as = "TimestampSeconds")] + pub ts: DateTime, + #[serde(default = "default_promise_signature_none")] + #[serde(with = "ser::option_dalek_sig_serde")] + #[serde(skip_serializing_if = "Option::is_none")] + pub psig: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub memo: Option, +} + +fn default_promise_signature_none() -> Option { + None +} + +#[derive(Debug, Copy, Clone, Serialize, Deserialize)] +pub struct CommitsV5 { + /// Options for an output's structure or use + #[serde(default = "default_output_feature")] + #[serde(skip_serializing_if = "output_feature_is_plain")] + pub f: OutputFeaturesV5, + /// The homomorphic commitment representing the output amount + #[serde( + serialize_with = "secp_ser::as_hex", + deserialize_with = "secp_ser::commitment_from_hex" + )] + pub c: Commitment, + /// A proof that the commitment is in the right range + /// Only applies for transaction outputs + #[serde(with = "ser::option_rangeproof_hex")] + #[serde(default = "default_range_proof")] + #[serde(skip_serializing_if = "Option::is_none")] + pub p: Option, +} + +impl From<&Output> for CommitsV5 { + fn from(out: &Output) -> CommitsV5 { + CommitsV5 { + f: out.features().into(), + c: out.commitment(), + p: Some(out.proof()), + } + } +} + +// This will need to be reworked once we no longer support input features with "commit only" inputs. +impl From<&Input> for CommitsV5 { + fn from(input: &Input) -> CommitsV5 { + CommitsV5 { + f: input.features.into(), + c: input.commitment(), + p: None, + } + } +} + +fn default_output_feature() -> OutputFeaturesV5 { + OutputFeaturesV5(0) +} + +fn output_feature_is_plain(o: &OutputFeaturesV5) -> bool { + o.0 == 0 +} + +#[derive(Serialize, Deserialize, Copy, Debug, Clone, PartialEq, Eq)] +pub struct OutputFeaturesV5(pub u8); + +pub fn sig_is_blank(s: &secp::Signature) -> bool { + for b in s.to_raw_data().iter() { + if *b != 0 { + return false; + } + } + true +} + +fn default_range_proof() -> Option { + None +} + +fn u64_is_blank(u: &u64) -> bool { + *u == 0 +} + +fn default_u8_0() -> u8 { + 0 +} + +fn u8_is_blank(u: &u8) -> bool { + *u == 0 +} + +fn fee_is_zero(f: &FeeFields) -> bool { + f.is_zero() +} + +fn default_fee() -> FeeFields { + FeeFields::zero() +} + +/// A mining node requests new coinbase via the foreign api every time a new candidate block is built. +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct CoinbaseV5 { + /// Output + output: CbOutputV5, + /// Kernel + kernel: CbKernelV5, + /// Key Id + key_id: Option, +} + +impl From for CoinbaseV5 { + fn from(cb: CbData) -> CoinbaseV5 { + CoinbaseV5 { + output: CbOutputV5::from(&cb.output), + kernel: CbKernelV5::from(&cb.kernel), + key_id: cb.key_id, + } + } +} + +impl From<&Output> for CbOutputV5 { + fn from(output: &Output) -> CbOutputV5 { + CbOutputV5 { + features: CbOutputFeatures::Coinbase, + commit: output.commitment(), + proof: output.proof(), + } + } +} + +impl From<&TxKernel> for CbKernelV5 { + fn from(kernel: &TxKernel) -> CbKernelV5 { + CbKernelV5 { + features: CbKernelFeatures::Coinbase, + excess: kernel.excess, + excess_sig: kernel.excess_sig, + } + } +} + +#[derive(Debug, Copy, Clone, Serialize, Deserialize)] +enum CbOutputFeatures { + Coinbase, +} + +#[derive(Debug, Copy, Clone, Serialize, Deserialize)] +enum CbKernelFeatures { + Coinbase, +} + +#[derive(Debug, Copy, Clone, Serialize, Deserialize)] +struct CbOutputV5 { + features: CbOutputFeatures, + #[serde(serialize_with = "secp_ser::as_hex")] + commit: Commitment, + #[serde(serialize_with = "secp_ser::as_hex")] + proof: RangeProof, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +struct CbKernelV5 { + features: CbKernelFeatures, + #[serde(serialize_with = "secp_ser::as_hex")] + excess: Commitment, + #[serde(with = "secp_ser::sig_serde")] + excess_sig: secp::Signature, +} diff --git a/libwallet/src/slate_versions/v5_bin.rs b/libwallet/src/slate_versions/v5_bin.rs new file mode 100644 index 000000000..c695006b8 --- /dev/null +++ b/libwallet/src/slate_versions/v5_bin.rs @@ -0,0 +1,544 @@ +// Copyright 2023 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Wraps a V5 Slate into a V5 Binary slate + +use crate::grin_core::core::transaction::OutputFeatures; +use crate::grin_core::ser as grin_ser; +use crate::grin_core::ser::{Readable, Reader, Writeable, Writer}; +use crate::grin_keychain::BlindingFactor; +use crate::grin_util::secp::key::PublicKey; +use crate::grin_util::secp::pedersen::{Commitment, RangeProof}; +use crate::grin_util::secp::Signature; +use chrono::DateTime; +use ed25519_dalek::Signature as DalekSignature; +use ed25519_dalek::VerifyingKey as DalekPublicKey; +use std::convert::{TryFrom, TryInto}; + +use crate::slate_versions::v5::{ + CommitsV5, KernelFeaturesArgsV5, ParticipantDataV5, PaymentInfoV5, PaymentMemoV5, SlateStateV5, + SlateV5, VersionCompatInfoV5, +}; + +use crate::slate_versions::v4_bin::{SlateOptFields, UuidWrap}; + +impl Writeable for SlateStateV5 { + fn write(&self, writer: &mut W) -> Result<(), grin_ser::Error> { + let b = match self { + SlateStateV5::Unknown => 0, + SlateStateV5::Standard1 => 1, + SlateStateV5::Standard2 => 2, + SlateStateV5::Standard3 => 3, + SlateStateV5::Invoice1 => 4, + SlateStateV5::Invoice2 => 5, + SlateStateV5::Invoice3 => 6, + }; + writer.write_u8(b) + } +} + +impl Readable for SlateStateV5 { + fn read(reader: &mut R) -> Result { + let b = reader.read_u8()?; + let sta = match b { + 0 => SlateStateV5::Unknown, + 1 => SlateStateV5::Standard1, + 2 => SlateStateV5::Standard2, + 3 => SlateStateV5::Standard3, + 4 => SlateStateV5::Invoice1, + 5 => SlateStateV5::Invoice2, + 6 => SlateStateV5::Invoice3, + _ => SlateStateV5::Unknown, + }; + Ok(sta) + } +} + +struct SigsWrap(Vec); +struct SigsWrapRef<'a>(&'a Vec); + +impl<'a> Writeable for SigsWrapRef<'a> { + fn write(&self, writer: &mut W) -> Result<(), grin_ser::Error> { + writer.write_u8(self.0.len() as u8)?; + for s in self.0.iter() { + //0 means part sig is not yet included + //1 means part sig included + if s.part.is_some() { + writer.write_u8(1)?; + } else { + writer.write_u8(0)?; + } + s.xs.write(writer)?; + s.nonce.write(writer)?; + if let Some(s) = s.part { + s.write(writer)?; + } + } + Ok(()) + } +} + +impl Readable for SigsWrap { + fn read(reader: &mut R) -> Result { + let sigs_len = reader.read_u8()?; + let sigs = { + let mut ret = vec![]; + for _ in 0..sigs_len as usize { + let has_partial = reader.read_u8()?; + let c = ParticipantDataV5 { + xs: PublicKey::read(reader)?, + nonce: PublicKey::read(reader)?, + part: match has_partial { + 1 => Some(Signature::read(reader)?), + 0 | _ => None, + }, + }; + ret.push(c); + } + ret + }; + Ok(SigsWrap(sigs)) + } +} + +/// Serialization of optional structs +struct SlateOptStructsRef<'a> { + /// coms, default none + pub coms: &'a Option>, + ///// proof, default none + pub proof: &'a Option, +} + +/// Serialization of optional structs +struct SlateOptStructs { + /// coms, default none + pub coms: Option>, + /// proof, default none + pub proof: Option, +} + +impl<'a> Writeable for SlateOptStructsRef<'a> { + fn write(&self, writer: &mut W) -> Result<(), grin_ser::Error> { + // Status byte, bits determing which optional structs are serialized + // 0 0 0 0 0 0 1 1 + // p c + let mut status = 0u8; + if self.coms.is_some() { + status |= 0x01 + }; + if self.proof.is_some() { + status |= 0x02 + }; + writer.write_u8(status)?; + if let Some(c) = self.coms { + ComsWrapRef(&c).write(writer)?; + } + if let Some(p) = self.proof { + ProofWrapRef(&p).write(writer)?; + } + Ok(()) + } +} + +impl Readable for SlateOptStructs { + fn read(reader: &mut R) -> Result { + let status = reader.read_u8()?; + let coms = if status & 0x01 > 0 { + Some(ComsWrap::read(reader)?.0) + } else { + None + }; + let proof = if status & 0x02 > 0 { + Some(ProofWrap::read(reader)?.0) + } else { + None + }; + Ok(SlateOptStructs { coms, proof }) + } +} + +struct ComsWrap(Vec); +struct ComsWrapRef<'a>(&'a Vec); + +impl<'a> Writeable for ComsWrapRef<'a> { + fn write(&self, writer: &mut W) -> Result<(), grin_ser::Error> { + writer.write_u16(self.0.len() as u16)?; + for o in self.0.iter() { + //0 means input + //1 means output with proof + if o.p.is_some() { + writer.write_u8(1)?; + } else { + writer.write_u8(0)?; + } + OutputFeatures::from(o.f).write(writer)?; + o.c.write(writer)?; + if let Some(p) = o.p { + p.write(writer)?; + } + } + Ok(()) + } +} + +impl Readable for ComsWrap { + fn read(reader: &mut R) -> Result { + let coms_len = reader.read_u16()?; + let coms = { + let mut ret = vec![]; + for _ in 0..coms_len as usize { + let is_output = reader.read_u8()?; + let c = CommitsV5 { + f: OutputFeatures::read(reader)?.into(), + c: Commitment::read(reader)?, + p: match is_output { + 1 => Some(RangeProof::read(reader)?), + 0 | _ => None, + }, + }; + ret.push(c); + } + ret + }; + Ok(ComsWrap(coms)) + } +} + +struct ProofWrap(PaymentInfoV5); +struct ProofWrapRef<'a>(&'a PaymentInfoV5); + +impl<'a> Writeable for ProofWrapRef<'a> { + fn write(&self, writer: &mut W) -> Result<(), grin_ser::Error> { + writer.write_fixed_bytes(self.0.saddr.to_bytes())?; + writer.write_fixed_bytes(self.0.raddr.to_bytes())?; + writer.write_i64(self.0.ts.timestamp())?; + match self.0.psig { + Some(s) => { + writer.write_u8(1)?; + writer.write_fixed_bytes(&s.to_bytes().to_vec())?; + } + None => writer.write_u8(0)?, + } + match &self.0.memo { + Some(s) => { + writer.write_u8(1)?; + writer.write_u8(s.memo_type)?; + writer.write_fixed_bytes(&s.memo)?; + } + None => writer.write_u8(0)?, + } + Ok(()) + } +} + +impl Readable for ProofWrap { + fn read(reader: &mut R) -> Result { + let saddr_bytes = reader.read_fixed_bytes(32)?; + let saddr_bytes = <&[u8; 32]>::try_from(saddr_bytes.as_slice()) + .map_err(|_| grin_ser::Error::CorruptedData)?; + let saddr = + DalekPublicKey::from_bytes(saddr_bytes).map_err(|_| grin_ser::Error::CorruptedData)?; + + let raddr_bytes = reader.read_fixed_bytes(32)?; + let raddr_bytes = <&[u8; 32]>::try_from(raddr_bytes.as_slice()) + .map_err(|_| grin_ser::Error::CorruptedData)?; + let raddr = + DalekPublicKey::from_bytes(raddr_bytes).map_err(|_| grin_ser::Error::CorruptedData)?; + let ts_raw: i64 = match reader.read_i64() { + Ok(v) => v, + Err(_) => return Err(grin_ser::Error::CorruptedData), + }; + let ts = match DateTime::from_timestamp(ts_raw, 0) { + Some(ts) => ts, + None => return Err(grin_ser::Error::CorruptedData), + }; + let psig = match reader.read_u8()? { + 0 => None, + 1 | _ => Some( + DalekSignature::try_from(&reader.read_fixed_bytes(64)?[..]) + .map_err(|_| grin_ser::Error::CorruptedData)?, + ), + }; + let memo = match reader.read_u8()? { + 0 => None, + 1 | _ => Some(PaymentMemoV5 { + memo_type: reader.read_u8()?, + memo: reader.read_fixed_bytes(32)?.try_into().unwrap_or_default(), + }), + }; + Ok(ProofWrap(PaymentInfoV5 { + saddr, + raddr, + ts, + psig, + memo, + })) + } +} + +#[derive(Debug, Clone)] +pub struct SlateV5Bin(pub SlateV5); + +impl From for SlateV5Bin { + fn from(slate: SlateV5) -> SlateV5Bin { + SlateV5Bin(slate) + } +} + +impl From for SlateV5 { + fn from(slate: SlateV5Bin) -> SlateV5 { + slate.0 + } +} + +impl serde::Serialize for SlateV5Bin { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut vec = vec![]; + grin_ser::serialize(&mut vec, grin_ser::ProtocolVersion(4), self) + .map_err(|err| serde::ser::Error::custom(err.to_string()))?; + serializer.serialize_bytes(&vec) + } +} + +impl<'de> serde::Deserialize<'de> for SlateV5Bin { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct SlateV5BinVisitor; + + impl<'de> serde::de::Visitor<'de> for SlateV5BinVisitor { + type Value = SlateV5Bin; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(formatter, "a serialised binary V5 slate") + } + + fn visit_bytes(self, value: &[u8]) -> Result + where + E: serde::de::Error, + { + let mut reader = std::io::Cursor::new(value.to_vec()); + let s = grin_ser::deserialize( + &mut reader, + grin_ser::ProtocolVersion(4), + grin_ser::DeserializationMode::default(), + ) + .map_err(|err| serde::de::Error::custom(err.to_string()))?; + Ok(s) + } + } + deserializer.deserialize_bytes(SlateV5BinVisitor) + } +} + +impl Writeable for SlateV5Bin { + fn write(&self, writer: &mut W) -> Result<(), grin_ser::Error> { + let v5 = &self.0; + writer.write_u16(v5.ver.version)?; + writer.write_u16(v5.ver.block_header_version)?; + (UuidWrap(v5.id)).write(writer)?; + v5.sta.write(writer)?; + v5.off.write(writer)?; + SlateOptFields { + num_parts: v5.num_parts, + amt: v5.amt, + fee: v5.fee, + feat: v5.feat, + ttl: v5.ttl, + } + .write(writer)?; + (SigsWrapRef(&v5.sigs)).write(writer)?; + SlateOptStructsRef { + coms: &v5.coms, + proof: &v5.proof, + } + .write(writer)?; + // Write lock height for height locked kernels + if v5.feat == 2 { + let lock_hgt = match &v5.feat_args { + Some(l) => l.lock_hgt, + None => 0, + }; + writer.write_u64(lock_hgt)?; + } + Ok(()) + } +} + +impl Readable for SlateV5Bin { + fn read(reader: &mut R) -> Result { + // See SlateV4Bin: the declared version disambiguates the untagged + // VersionedBinSlate variants. + let version = reader.read_u16()?; + if version != 5 { + return Err(grin_ser::Error::UnexpectedData { + expected: 5u16.to_be_bytes().to_vec(), + received: version.to_be_bytes().to_vec(), + }); + } + let ver = VersionCompatInfoV5 { + version, + block_header_version: reader.read_u16()?, + }; + let id = UuidWrap::read(reader)?.0; + let sta = SlateStateV5::read(reader)?; + let off = BlindingFactor::read(reader)?; + + let opts = SlateOptFields::read(reader)?; + let sigs = SigsWrap::read(reader)?.0; + let opt_structs = SlateOptStructs::read(reader)?; + + let feat_args = if opts.feat == 2 { + Some(KernelFeaturesArgsV5 { + lock_hgt: reader.read_u64()?, + }) + } else { + None + }; + + Ok(SlateV5Bin(SlateV5 { + ver, + id, + sta, + off, + num_parts: opts.num_parts, + amt: opts.amt, + fee: opts.fee, + feat: opts.feat, + ttl: opts.ttl, + sigs, + coms: opt_structs.coms, + proof: opt_structs.proof, + feat_args, + })) + } +} + +#[test] +fn slate_v5_serialize_deserialize() { + use crate::grin_util::from_hex; + use crate::grin_util::secp::key::PublicKey; + use crate::Slate; + use chrono::Utc; + use grin_core::global::{set_local_chain_type, ChainTypes}; + use grin_keychain::{ExtKeychain, Keychain, SwitchCommitmentType}; + set_local_chain_type(ChainTypes::Mainnet); + let slate = Slate::blank(1, false); + let mut v5 = SlateV5::from(slate); + use chrono::prelude::*; + + let keychain = ExtKeychain::from_random_seed(true).unwrap(); + let switch = SwitchCommitmentType::Regular; + // add some sig data + let id1 = ExtKeychain::derive_key_id(1, 1, 0, 0, 0); + let id2 = ExtKeychain::derive_key_id(1, 1, 1, 0, 0); + let skey1 = keychain.derive_key(0, &id1, switch).unwrap(); + let skey2 = keychain.derive_key(0, &id2, switch).unwrap(); + let xs = PublicKey::from_secret_key(keychain.secp(), &skey1).unwrap(); + let nonce = PublicKey::from_secret_key(keychain.secp(), &skey2).unwrap(); + let part = ParticipantDataV5 { + xs, + nonce, + part: None, + }; + let part2 = ParticipantDataV5 { + xs, + nonce, + part: Some(Signature::from_raw_data(&[11; 64]).unwrap()), + }; + v5.sigs.push(part.clone()); + v5.sigs.push(part2); + v5.sigs.push(part); + + // add some random commit data + let com1 = CommitsV5 { + f: OutputFeatures::Plain.into(), + c: Commitment::from_vec([3u8; 1].to_vec()), + p: None, + }; + let com2 = CommitsV5 { + f: OutputFeatures::Plain.into(), + c: Commitment::from_vec([4u8; 1].to_vec()), + p: Some(RangeProof::zero()), + }; + let mut coms = vec![]; + coms.push(com1.clone()); + coms.push(com1.clone()); + coms.push(com1.clone()); + coms.push(com2); + + v5.coms = Some(coms); + v5.amt = 234324899824; + v5.feat = 1; + v5.num_parts = 2; + v5.feat_args = Some(KernelFeaturesArgsV5 { lock_hgt: 23092039 }); + let v4_1 = v5.clone(); + let v4_1_copy = v5.clone(); + + let v4_bin = SlateV5Bin(v5); + let mut vec = Vec::new(); + let _ = grin_ser::serialize_default(&mut vec, &v4_bin).expect("serialization failed"); + let b4_bin_2: SlateV5Bin = grin_ser::deserialize_default(&mut &vec[..]).unwrap(); + let v4_2 = b4_bin_2.0.clone(); + assert_eq!(v4_1.ver, v4_2.ver); + assert_eq!(v4_1.id, v4_2.id); + assert_eq!(v4_1.amt, v4_2.amt); + assert_eq!(v4_1.fee, v4_2.fee); + let v4_2_coms = v4_2.coms.as_ref().unwrap().clone(); + for (i, c) in v4_1.coms.unwrap().iter().enumerate() { + assert_eq!(c.f, v4_2_coms[i].f); + assert_eq!(c.c, v4_2_coms[i].c); + assert_eq!(c.p, v4_2_coms[i].p); + } + assert_eq!(v4_1.sigs, v4_2.sigs); + assert_eq!(v4_1.proof, v4_2.proof); + + // Include Payment proof, remove coms to mix it up a bit + let mut v5 = v4_1_copy; + let raw_pubkey_str = "d03c09e9c19bb74aa9ea44e0fe5ae237a9bf40bddf0941064a80913a4459c8bb"; + let b = from_hex(raw_pubkey_str).unwrap(); + let b = <&[u8; 32]>::try_from(b.as_slice()).unwrap(); + let d_pkey = DalekPublicKey::from_bytes(b).unwrap(); + // Need to remove milliseconds component for comparison. Won't be serialized + let ts = DateTime::from_timestamp(Utc::now().timestamp(), 0).unwrap(); + let pm = PaymentMemoV5 { + memo_type: 0, + memo: [9; 32], + }; + v5.proof = Some(PaymentInfoV5 { + raddr: d_pkey.clone(), + saddr: d_pkey.clone(), + ts: ts.clone(), + psig: None, + memo: Some(pm), + }); + v5.coms = None; + let v5_1 = v5.clone(); + let v5_bin = SlateV5Bin(v5); + let mut vec = Vec::new(); + let _ = grin_ser::serialize_default(&mut vec, &v5_bin).expect("serialization failed"); + let b4_bin_2: SlateV5Bin = grin_ser::deserialize_default(&mut &vec[..]).unwrap(); + let v5_2 = b4_bin_2.0.clone(); + assert_eq!(v5_1.ver, v5_2.ver); + assert_eq!(v5_1.id, v5_2.id); + assert_eq!(v5_1.amt, v5_2.amt); + assert_eq!(v5_1.fee, v5_2.fee); + assert!(v5_1.coms.is_none()); + assert_eq!(v5_1.sigs, v5_2.sigs); + assert_eq!(v5_1.proof, v5_2.proof); +} diff --git a/libwallet/src/slatepack/address.rs b/libwallet/src/slatepack/address.rs index 15c08b847..9368e38d2 100644 --- a/libwallet/src/slatepack/address.rs +++ b/libwallet/src/slatepack/address.rs @@ -13,9 +13,9 @@ // limitations under the License. use bech32::{self, FromBase32, ToBase32}; +use ed25519_dalek::SigningKey as edDalekSecretKey; /// Slatepack Address definition -use ed25519_dalek::PublicKey as edDalekPublicKey; -use ed25519_dalek::SecretKey as edDalekSecretKey; +use ed25519_dalek::VerifyingKey as edDalekPublicKey; use rand::{thread_rng, Rng}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use x25519_dalek::PublicKey as xDalekPublicKey; @@ -41,15 +41,18 @@ pub struct SlatepackAddress { pub pub_key: edDalekPublicKey, } +fn slatepack_hrp() -> &'static str { + match global::get_chain_type() { + global::ChainTypes::Mainnet => "grin", + _ => "tgrin", + } +} + impl SlatepackAddress { /// new with default hrp pub fn new(pub_key: &edDalekPublicKey) -> Self { - let hrp = match global::get_chain_type() { - global::ChainTypes::Mainnet => "grin", - _ => "tgrin", - }; Self { - hrp: String::from(hrp), + hrp: String::from(slatepack_hrp()), pub_key: pub_key.clone(), } } @@ -57,7 +60,7 @@ impl SlatepackAddress { /// new with a random key pub fn random() -> Self { let bytes: [u8; 32] = thread_rng().gen(); - let pub_key = edDalekPublicKey::from(&edDalekSecretKey::from_bytes(&bytes).unwrap()); + let pub_key = edDalekPublicKey::from(&edDalekSecretKey::from_bytes(&bytes)); SlatepackAddress::new(&pub_key) } @@ -75,11 +78,19 @@ impl SlatepackAddress { let x_key = xDalekPublicKey::try_from(self)?; Ok(bech32::encode("age", x_key.as_bytes().to_base32())?.to_string()) } + + /// Check if address network is valid. + pub fn valid_network(&self) -> bool { + valid_network(&self.hrp) + } } impl Display for SlatepackAddress { fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str(&String::try_from(self).unwrap()) + match String::try_from(self) { + Ok(encoded) => formatter.write_str(&encoded), + Err(_) => formatter.write_str(""), + } } } @@ -87,8 +98,16 @@ impl TryFrom<&str> for SlatepackAddress { type Error = Error; fn try_from(encoded: &str) -> Result { let (hrp, data) = bech32::decode(&encoded)?; + if !valid_network(&hrp) { + return Err(Error::SlatepackAddress(format!( + "wrong address prefix for chain {:?}", + global::get_chain_type() + ))); + } let bytes = Vec::::from_base32(&data)?; - let pub_key = match edDalekPublicKey::from_bytes(&bytes) { + let b = <&[u8; 32]>::try_from(bytes.as_slice()) + .map_err(|_| Error::SlatepackAddress("Wrong encoded data".to_string()))?; + let pub_key = match edDalekPublicKey::from_bytes(&b) { Ok(k) => k, Err(e) => { return Err(Error::ED25519Key(format!("{}", e))); @@ -124,7 +143,10 @@ impl TryFrom<&SlatepackAddress> for xDalekPublicKey { fn try_from(addr: &SlatepackAddress) -> Result { let cep = curve25519_dalek::edwards::CompressedEdwardsY::from_slice(addr.pub_key.as_bytes()); - let ep = match cep.decompress() { + let ep = match cep + .map_err(|e| Error::ED25519Key(format!("{}", e)))? + .decompress() + { Some(p) => p, None => { return Err(Error::ED25519Key( @@ -140,15 +162,7 @@ impl TryFrom<&SlatepackAddress> for xDalekPublicKey { impl TryFrom<&SecretKey> for SlatepackAddress { type Error = Error; fn try_from(key: &SecretKey) -> Result { - let d_skey = match edDalekSecretKey::from_bytes(&key.0) { - Ok(k) => k, - Err(e) => { - return Err(Error::ED25519Key(format!( - "Can't create slatepack address from SecretKey: {}", - e - ))); - } - }; + let d_skey = edDalekSecretKey::from_bytes(&key.0); let d_pub_key: edDalekPublicKey = (&d_skey).into(); Ok(Self::new(&d_pub_key)) } @@ -245,13 +259,55 @@ impl Readable for SlatepackAddress { } } +/// Check if encoded HRP is for valid network. +fn valid_network(hrp: &str) -> bool { + slatepack_hrp() == hrp +} + #[test] fn slatepack_address() -> Result<(), Error> { use rand::{thread_rng, Rng}; global::set_local_chain_type(global::ChainTypes::AutomatedTesting); + + // Check validation. + let bytes: [u8; 32] = thread_rng().gen(); + let pub_key = edDalekPublicKey::from(&edDalekSecretKey::from_bytes(&bytes)); + let wrong_net_addr = SlatepackAddress { + hrp: "grin".to_string(), + pub_key, + }; + assert!(!wrong_net_addr.valid_network()); + let right_addr = SlatepackAddress { + hrp: "tgrin".to_string(), + pub_key, + }; + assert!(right_addr.valid_network()); + let invalid_addr = SlatepackAddress { + hrp: "invalid hrp".to_string(), + pub_key, + }; + assert!(!invalid_addr.valid_network()); + assert_eq!(invalid_addr.to_string(), ""); + + let valid_addr = "tgrin1xtxavwfgs48ckf3gk8wwgcndmn0nt4tvkl8a7ltyejjcy2mc6nfs9gm2lp"; + let parsed_valid_addr = SlatepackAddress::try_from(valid_addr); + assert!(parsed_valid_addr.is_ok()); + + let valid_addr2 = "TGRIN1XTXAVWFGS48CKF3GK8WWGCNDMN0NT4TVKL8A7LTYEJJCY2MC6NFS9GM2LP"; + let parsed_valid_addr2 = SlatepackAddress::try_from(valid_addr2); + assert!(parsed_valid_addr2.is_ok()); + + let invalid_addr = "slatepack10qlk22rxjap2ny8qltc2tl996kenxr3hhwuu6hrzs6tdq08yaqgqnlumr7"; + let parsed_invalid_addr = SlatepackAddress::try_from(invalid_addr); + assert!(parsed_invalid_addr.is_err()); + + let wrong_net_addr = "grin1dvge9z4uqgqlpspmljrd7smh3grrw9xu2r9lkz3u67s3emj3ud2sd5gk9p"; + let parsed_wrong_net_addr = SlatepackAddress::try_from(wrong_net_addr); + assert!(parsed_wrong_net_addr.is_err()); + let sec_key_bytes: [u8; 32] = thread_rng().gen(); - let ed_sec_key = edDalekSecretKey::from_bytes(&sec_key_bytes).unwrap(); + let ed_sec_key = edDalekSecretKey::from_bytes(&sec_key_bytes); let ed_pub_key = edDalekPublicKey::from(&ed_sec_key); let addr = SlatepackAddress::new(&ed_pub_key); let x_pub_key = xDalekPublicKey::try_from(&addr)?; diff --git a/libwallet/src/slatepack/packer.rs b/libwallet/src/slatepack/packer.rs index f1c54e92c..27dda49b7 100644 --- a/libwallet/src/slatepack/packer.rs +++ b/libwallet/src/slatepack/packer.rs @@ -24,7 +24,7 @@ use crate::{ use grin_wallet_util::byte_ser; -use ed25519_dalek::SecretKey as edSecretKey; +use ed25519_dalek::SigningKey as edSecretKey; #[derive(Clone)] /// Arguments, mostly for encrypting decrypting a slatepack @@ -94,7 +94,10 @@ impl<'a> Slatepacker<'a> { /// Create slatepack from slate and args pub fn create_slatepack(&self, slate: &Slate) -> Result { - let out_slate = VersionedSlate::into_version(slate.clone(), SlateVersion::V4)?; + // Emit the lowest version that can represent this slate, for maximum + // interoperability with V4-only wallets. + let version = SlateVersion::lowest_for(slate); + let out_slate = VersionedSlate::into_version(slate.clone(), version)?; let bin_slate = VersionedBinSlate::try_from(out_slate).map_err(|_| Error::SlatepackSer)?; let mut slatepack = Slatepack::default(); slatepack.payload = byte_ser::to_bytes(&bin_slate).map_err(|_| Error::SlatepackSer)?; diff --git a/libwallet/src/slatepack/types.rs b/libwallet/src/slatepack/types.rs index fc12f4985..72867a9ae 100644 --- a/libwallet/src/slatepack/types.rs +++ b/libwallet/src/slatepack/types.rs @@ -15,7 +15,7 @@ use bech32::{self, ToBase32}; use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; /// Slatepack Types + Serialization implementation -use ed25519_dalek::SecretKey as edSecretKey; +use ed25519_dalek::SigningKey as edSecretKey; use sha2::{Digest, Sha512}; use x25519_dalek::StaticSecret; @@ -768,15 +768,15 @@ fn slatepack_bin_future() -> Result<(), grin_wallet_util::byte_ser::Error> { fn slatepack_encrypted_meta() -> Result<(), Error> { use crate::grin_core::global; use crate::{Slate, SlateVersion, VersionedBinSlate, VersionedSlate}; - use ed25519_dalek::PublicKey as edDalekPublicKey; - use ed25519_dalek::SecretKey as edDalekSecretKey; + use ed25519_dalek::SigningKey as edDalekSecretKey; + use ed25519_dalek::VerifyingKey as edDalekPublicKey; use rand::{thread_rng, Rng}; use std::convert::TryFrom; global::set_local_chain_type(global::ChainTypes::AutomatedTesting); let sec_key_bytes: [u8; 32] = thread_rng().gen(); - let ed_sec_key = edDalekSecretKey::from_bytes(&sec_key_bytes).unwrap(); + let ed_sec_key = edDalekSecretKey::from_bytes(&sec_key_bytes); let ed_pub_key = edDalekPublicKey::from(&ed_sec_key); let addr = SlatepackAddress::new(&ed_pub_key); @@ -817,15 +817,15 @@ fn slatepack_encrypted_meta() -> Result<(), Error> { fn slatepack_encrypted_meta_future() -> Result<(), Error> { use crate::grin_core::global; use crate::{Slate, SlateVersion, VersionedBinSlate, VersionedSlate}; - use ed25519_dalek::PublicKey as edDalekPublicKey; - use ed25519_dalek::SecretKey as edDalekSecretKey; + use ed25519_dalek::SigningKey as edDalekSecretKey; + use ed25519_dalek::VerifyingKey as edDalekPublicKey; use rand::{thread_rng, Rng}; use std::convert::TryFrom; global::set_local_chain_type(global::ChainTypes::AutomatedTesting); let sec_key_bytes: [u8; 32] = thread_rng().gen(); - let ed_sec_key = edDalekSecretKey::from_bytes(&sec_key_bytes).unwrap(); + let ed_sec_key = edDalekSecretKey::from_bytes(&sec_key_bytes); let ed_pub_key = edDalekPublicKey::from(&ed_sec_key); let addr = SlatepackAddress::new(&ed_pub_key); @@ -864,11 +864,11 @@ fn slatepack_encrypted_meta_future() -> Result<(), Error> { #[cfg(test)] fn slatepack_test_decryption_key() -> (edSecretKey, SlatepackAddress) { - use ed25519_dalek::PublicKey as edDalekPublicKey; + use ed25519_dalek::VerifyingKey as edDalekPublicKey; use rand::{thread_rng, Rng}; let sec_key_bytes: [u8; 32] = thread_rng().gen(); - let ed_sec_key = edSecretKey::from_bytes(&sec_key_bytes).unwrap(); + let ed_sec_key = edSecretKey::from_bytes(&sec_key_bytes); let ed_pub_key = edDalekPublicKey::from(&ed_sec_key); let addr = SlatepackAddress::new(&ed_pub_key); diff --git a/libwallet/src/types.rs b/libwallet/src/types.rs index 12b552ed7..8bfe2958b 100644 --- a/libwallet/src/types.rs +++ b/libwallet/src/types.rs @@ -16,6 +16,7 @@ //! implementation use crate::config::{TorConfig, WalletConfig}; +use crate::contract::types::ContractSetupArgsAPI; use crate::error::Error; use crate::grin_core::core::hash::Hash; use crate::grin_core::core::FeeFields; @@ -25,13 +26,14 @@ use crate::grin_core::{global, ser}; use crate::grin_keychain::{Identifier, Keychain}; use crate::grin_util::logger::LoggingConfig; use crate::grin_util::secp::key::{PublicKey, SecretKey}; -use crate::grin_util::secp::{self, pedersen, Secp256k1}; +use crate::grin_util::secp::{self, pedersen, Secp256k1, Signature}; use crate::grin_util::{ToHex, ZeroingString}; +use crate::slate::PaymentMemo; use crate::slate_versions::ser as dalek_ser; -use crate::InitTxArgs; +use crate::{InitTxArgs, SlateState, WalletBackend}; use chrono::prelude::*; -use ed25519_dalek::PublicKey as DalekPublicKey; use ed25519_dalek::Signature as DalekSignature; +use ed25519_dalek::VerifyingKey as DalekPublicKey; use rand::rngs::mock::StepRng; use rand::thread_rng; use serde; @@ -49,7 +51,7 @@ where K: Keychain + 'a, { /// Return the stored instance - fn lc_provider(&mut self) -> Result<&mut (dyn WalletLCProvider<'a, C, K> + 'a), Error>; + fn lc_provider(&mut self) -> Result<&mut dyn WalletLCProvider<'a, C, K>, Error>; } /// Trait for a provider of wallet lifecycle methods @@ -131,182 +133,7 @@ where fn delete_wallet(&self, name: Option<&str>) -> Result<(), Error>; /// return wallet instance - fn wallet_inst(&mut self) -> Result<&mut Box + 'a>, Error>; -} - -/// TODO: -/// Wallets should implement this backend for their storage. All functions -/// here expect that the wallet instance has instantiated itself or stored -/// whatever credentials it needs -pub trait WalletBackend<'ck, C, K>: Send + Sync -where - C: NodeClient + 'ck, - K: Keychain + 'ck, -{ - /// Set the keychain, which should already be initialized - /// Optionally return a token value used to XOR the stored - /// key value - fn set_keychain( - &mut self, - k: Box, - mask: bool, - use_test_rng: bool, - ) -> Result, Error>; - - /// Close wallet and remove any stored credentials (TBD) - fn close(&mut self) -> Result<(), Error>; - - /// Return the keychain being used. Ensure a cloned copy so it will be dropped - /// and zeroized by the caller - /// Can optionally take a mask value - fn keychain(&self, mask: Option<&SecretKey>) -> Result; - - /// Return the client being used to communicate with the node - fn w2n_client(&mut self) -> &mut C; - - /// return the commit for caching if allowed, none otherwise - fn calc_commit_for_cache( - &mut self, - keychain_mask: Option<&SecretKey>, - amount: u64, - id: &Identifier, - ) -> Result, Error>; - - /// Set parent key id by stored account name - fn set_parent_key_id_by_name(&mut self, label: &str) -> Result<(), Error>; - - /// The BIP32 path of the parent path to use for all output-related - /// functions, (essentially 'accounts' within a wallet. - fn set_parent_key_id(&mut self, _: Identifier); - - /// return the parent path - fn parent_key_id(&mut self) -> Identifier; - - /// Iterate over all output data stored by the backend - fn iter<'a>(&'a self) -> Box + 'a>; - - /// Get output data by id - fn get(&self, id: &Identifier, mmr_index: &Option) -> Result; - - /// Get an (Optional) tx log entry by uuid - fn get_tx_log_entry(&self, uuid: &Uuid) -> Result, Error>; - - /// Retrieves the private context associated with a given slate id - fn get_private_context( - &mut self, - keychain_mask: Option<&SecretKey>, - slate_id: &[u8], - ) -> Result; - - /// Iterate over all output data stored by the backend - fn tx_log_iter<'a>(&'a self) -> Box + 'a>; - - /// Iterate over all stored account paths - fn acct_path_iter<'a>(&'a self) -> Box + 'a>; - - /// Gets an account path for a given label - fn get_acct_path(&self, label: String) -> Result, Error>; - - /// Stores a transaction - fn store_tx(&self, uuid: &str, tx: &Transaction) -> Result<(), Error>; - - /// Retrieves a stored transaction from a TxLogEntry - fn get_stored_tx(&self, uuid: &str) -> Result, Error>; - - /// Create a new write batch to update or remove output data - fn batch<'a>( - &'a mut self, - keychain_mask: Option<&SecretKey>, - ) -> Result + 'a>, Error>; - - /// Batch for use when keychain isn't available or required - fn batch_no_mask<'a>(&'a mut self) -> Result + 'a>, Error>; - - /// Return the current child Index - fn current_child_index(&mut self, parent_key_id: &Identifier) -> Result; - - /// Next child ID when we want to create a new output, based on current parent - fn next_child(&mut self, keychain_mask: Option<&SecretKey>) -> Result; - - /// last verified height of outputs directly descending from the given parent key - fn last_confirmed_height(&mut self) -> Result; - - /// last block scanned during scan or restore - fn last_scanned_block(&mut self) -> Result; - - /// Flag whether the wallet needs a full UTXO scan on next update attempt - fn init_status(&mut self) -> Result; -} - -/// Batch trait to update the output data backend atomically. Trying to use a -/// batch after commit MAY result in a panic. Due to this being a trait, the -/// commit method can't take ownership. -/// TODO: Should these be split into separate batch objects, for outputs, -/// tx_log entries and meta/details? -pub trait WalletOutputBatch -where - K: Keychain, -{ - /// Return the keychain being used - fn keychain(&mut self) -> &mut K; - - /// Add or update data about an output to the backend - fn save(&mut self, out: OutputData) -> Result<(), Error>; - - /// Gets output data by id - fn get(&self, id: &Identifier, mmr_index: &Option) -> Result; - - /// Iterate over all output data stored by the backend - fn iter(&self) -> Box>; - - /// Delete data about an output from the backend - fn delete(&mut self, id: &Identifier, mmr_index: &Option) -> Result<(), Error>; - - /// Save last stored child index of a given parent - fn save_child_index(&mut self, parent_key_id: &Identifier, child_n: u32) -> Result<(), Error>; - - /// Save last confirmed height of outputs for a given parent - fn save_last_confirmed_height( - &mut self, - parent_key_id: &Identifier, - height: u64, - ) -> Result<(), Error>; - - /// Save the last PMMR index that was scanned via a scan operation - fn save_last_scanned_block(&mut self, block: ScannedBlockInfo) -> Result<(), Error>; - - /// Save flag indicating whether wallet needs a full UTXO scan - fn save_init_status(&mut self, value: WalletInitStatus) -> Result<(), Error>; - - /// get next tx log entry for the parent - fn next_tx_log_id(&mut self, parent_key_id: &Identifier) -> Result; - - /// Iterate over tx log data stored by the backend - fn tx_log_iter(&self) -> Box>; - - /// save a tx log entry - fn save_tx_log_entry(&mut self, t: TxLogEntry, parent_id: &Identifier) -> Result<(), Error>; - - /// delete a tx log entry - fn delete_tx_log_entry(&mut self, tx_id: u32, parent_id: &Identifier) -> Result<(), Error>; - - /// save an account label -> path mapping - fn save_acct_path(&mut self, mapping: AcctPathMapping) -> Result<(), Error>; - - /// Iterate over account names stored in backend - fn acct_path_iter(&self) -> Box>; - - /// Save an output as locked in the backend - fn lock_output(&mut self, out: &mut OutputData) -> Result<(), Error>; - - /// Saves the private context associated with a slate id - fn save_private_context(&mut self, slate_id: &[u8], ctx: &Context) -> Result<(), Error>; - - /// Delete the private context associated with the slate id - fn delete_private_context(&mut self, slate_id: &[u8]) -> Result<(), Error>; - - /// Write the wallet data to backend file - fn commit(&self) -> Result<(), Error>; + fn wallet_inst(&mut self) -> Result<&mut WalletBackend, Error>; } /// Encapsulate all wallet-node communication functions. No functions within libwallet @@ -566,6 +393,12 @@ pub struct Context { /// for invoice I2 Only, store the tx excess so we can /// remove it from the slate on return pub calculated_excess: Option, + /// Arguments that define which outputs to pick for a contract + pub setup_args: Option, + /// TxLogEntry id (needed to avoid a linear scan). Services that keep a long + /// history might need to search through a list to update a txlogentry, so we + /// keep the id in the context. + pub log_id: Option, } impl Context { @@ -615,11 +448,25 @@ impl Context { payment_proof_derivation_index: None, late_lock_args: None, calculated_excess: None, + setup_args: None, + log_id: None, } } } impl Context { + /// Returns net_change for the contract. Context is shared with the standard + /// transaction flows, where setup_args is None, so a context that does not belong to + /// a contract is reported rather than unwrapped. + pub fn get_net_change(&self) -> Result { + self.setup_args + .as_ref() + .and_then(|args| args.net_change) + .ok_or_else(|| { + Error::GenericError("Context carries no contract net change".to_string()) + }) + } + /// Tracks an output contributing to my excess value (if it needs to /// be kept between invocations pub fn add_output(&mut self, output_id: &Identifier, mmr_index: &Option, amount: u64) { @@ -772,6 +619,10 @@ pub enum TxLogEntryType { TxSentCancelled, /// Received transaction that was reverted on-chain TxReverted, + /// Self spend, as per contracts and mwixnet + TxSelfSpend, + /// Self Spend Cancelled (has to happen before sent to chain, flag rather than delete) + TxSelfSpendCancelled, } impl fmt::Display for TxLogEntryType { @@ -783,6 +634,8 @@ impl fmt::Display for TxLogEntryType { TxLogEntryType::TxReceivedCancelled => write!(f, "Received Tx\n- Cancelled"), TxLogEntryType::TxSentCancelled => write!(f, "Sent Tx\n- Cancelled"), TxLogEntryType::TxReverted => write!(f, "Received Tx\n- Reverted"), + TxLogEntryType::TxSelfSpend => write!(f, "Self Spend"), + TxLogEntryType::TxSelfSpendCancelled => write!(f, "Self Spend\n- Cancelled"), } } } @@ -798,6 +651,8 @@ pub struct TxLogEntry { pub id: u32, /// Slate transaction this entry is associated with, if any pub tx_slate_id: Option, + /// Transaction slate state + pub tx_slate_state: Option, /// Transaction type (as above) pub tx_type: TxLogEntryType, /// Time this tx entry was created @@ -861,10 +716,11 @@ impl TxLogEntry { /// Return a new blank with TS initialised with next entry pub fn new(parent_key_id: Identifier, t: TxLogEntryType, id: u32) -> Self { TxLogEntry { - parent_key_id: parent_key_id, + parent_key_id, tx_type: t, - id: id, + id, tx_slate_id: None, + tx_slate_state: None, creation_ts: Utc::now(), confirmation_ts: None, confirmed: false, @@ -914,6 +770,23 @@ pub struct StoredProofInfo { /// sender signature #[serde(with = "dalek_ser::option_dalek_sig_serde")] pub sender_signature: Option, + // Fields beyond here are specific to early payment proofs, + // invoice and sender nonce + /// Assumed to be 0x00 (Legacy) if missing + pub proof_type: Option, + /// receiver's public nonce from signing + pub receiver_public_nonce: Option, + /// receiver's public excess from signing + pub receiver_public_excess: Option, + /// Timestamp provided by recipient when signing + pub timestamp: Option>, + /// Optional payment memo + pub memo: Option, + /// recipient promise signature + #[serde(with = "dalek_ser::option_dalek_sig_serde")] + pub promise_signature: Option, + /// Original Sender partial key + pub sender_part_sig: Option, } impl ser::Writeable for StoredProofInfo { @@ -1053,7 +926,7 @@ pub struct ViewWalletOutputResult { impl ViewWalletOutputResult { pub fn num_confirmations(&self, tip_height: u64) -> u64 { if self.height > tip_height { - return 0; + 0 } else { 1 + (tip_height - self.height) } diff --git a/src/bin/grin-wallet.rs b/src/bin/grin-wallet.rs index 9189f0290..6e9099554 100644 --- a/src/bin/grin-wallet.rs +++ b/src/bin/grin-wallet.rs @@ -31,7 +31,7 @@ use std::env; use std::path::PathBuf; use std::path::MAIN_SEPARATOR; -// include build information +/// Include build information pub mod built_info { include!(concat!(env!("OUT_DIR"), "/built.rs")); } @@ -55,7 +55,7 @@ pub fn info_strings() -> (String, String) { ) } -// Helper fuction to format paths according to OS, avoids bugs on Linux +/// Helper function to format paths according to OS, avoids bugs on Linux pub fn fmt_path(path: String) -> String { let sep = &MAIN_SEPARATOR.to_string(); let path = path.replace("/", &sep).replace("\\", &sep); @@ -117,24 +117,24 @@ fn real_main() -> i32 { // Load relevant config, try and load a wallet config file // Use defaults for configuration if config file not found anywhere - let mut config = match config::initial_setup_wallet(&chain_type, current_dir, create_path) { + let config = match config::initial_setup_wallet(&chain_type, current_dir, create_path) { Ok(c) => c, - Err(e) => match e { - ConfigError::PathNotFoundError(m) => { - println!("Wallet configuration not found at {}. (Run `grin-wallet init` to create a new wallet)", m); - return 0; + Err(e) => { + return match e { + ConfigError::PathNotFoundError(m) => { + println!("Wallet configuration not found at {}. (Run `grin-wallet init` to create a new wallet)", m); + 0 + } + m => { + println!("Unable to load wallet configuration: {} (Run `grin-wallet init` to create a new wallet)", m); + 0 + } } - m => { - println!("Unable to load wallet configuration: {} (Run `grin-wallet init` to create a new wallet)", m); - return 0; - } - }, + } }; - //config.members.as_mut().unwrap().wallet.chain_type = Some(chain_type); - // Load logging config - let mut l = config.members.as_mut().unwrap().logging.clone().unwrap(); + let mut l = config.members.logging.clone().unwrap(); // no logging to stdout if we're running cli match args.subcommand() { ("cli", _) => l.log_to_stdout = true, @@ -143,24 +143,16 @@ fn real_main() -> i32 { init_logger(Some(l), None); info!( "Using wallet configuration file at {}", - config.config_file_path.as_ref().unwrap().to_str().unwrap() + config.config_file_path.to_str().unwrap() ); log_build_info(); - global::init_global_chain_type( - config - .members - .as_ref() - .unwrap() - .wallet - .chain_type - .as_ref() - .unwrap() - .clone(), - ); + global::init_global_chain_type(config.members.wallet.chain_type.as_ref().unwrap().clone()); - global::init_global_accept_fee_base(config.members.as_ref().unwrap().wallet.accept_fee_base()); - let wallet_config = config.clone().members.unwrap().wallet; - let node_client = HTTPNodeClient::new(&wallet_config.check_node_api_http_addr, None).unwrap(); + global::init_global_accept_fee_base(config.members.wallet.accept_fee_base()); + let wallet_config = config.clone().members.wallet; + let timeout = wallet_config.api_request_timeout(); + let node_client = + HTTPNodeClient::new(&wallet_config.check_node_api_http_addr, None, timeout).unwrap(); cmd::wallet_command(&args, config, node_client) } diff --git a/src/bin/grin-wallet.yml b/src/bin/grin-wallet.yml index 39a8962ca..fc0fe26a2 100644 --- a/src/bin/grin-wallet.yml +++ b/src/bin/grin-wallet.yml @@ -132,7 +132,7 @@ subcommands: default_value: "1" takes_value: true - dest: - help: Intended recipient's Slatepack Address (or http listener address (DEPRECATED)) + help: Intended recipient's Slatepack Address short: d long: dest takes_value: true @@ -446,3 +446,112 @@ subcommands: - input: help: Filename of a proof file index: 1 + - contract: + subcommands: + - new: + about: Create a new contract with initial setup + args: + - encrypt-for: + help: The counter party grin address (optional; omit to produce an unencrypted slatepack) + short: e + long: encrypt-for + takes_value: true + # index: 1 + - receive: + help: How much you want to receive + short: r + long: receive + takes_value: true + - send: + help: How much you want to send + short: s + long: send + takes_value: true + - num-participants: + help: How many participants are involved? (can be 1 or 2) + short: n + long: num-participants + takes_value: true + default_value: "2" + - as-json: + help: Show result as JSON + short: j + long: as-json + takes_value: false + - no-payjoin: + help: Don't make it a payjoin (if receiver) + long: no-payjoin + takes_value: false + - add-outputs: + help: Defines whether we should pick inputs/outputs in the first step. + short: a + long: add-outputs + takes_value: false + - use-inputs: + help: Which inputs you want to use (provide comma separated commitments) + short: i + long: use-inputs + takes_value: true + - make-outputs: + help: Which outputs should we create? (provide comma separated amounts) + short: o + long: make-outputs + takes_value: true + - sign: + about: Sign a contract + args: + - encrypt-for: + help: The counter party grin address (optional; defaults to the slate sender, else unencrypted) + short: e + long: encrypt-for + takes_value: true + # index: 1 + - receive: + help: How much you want to receive + short: r + long: receive + takes_value: true + - send: + help: How much you want to send + short: s + long: send + takes_value: true + - as-json: + help: Show result as JSON + short: j + long: as-json + takes_value: false + - no-payjoin: + help: Don't make it a payjoin (if receiver) + long: no-payjoin + takes_value: false + - use-inputs: + help: Which inputs you want to use (provide comma separated commitments) + short: i + long: use-inputs + takes_value: true + - make-outputs: + help: Which outputs should we create? (provide comma separated amounts) + short: o + long: make-outputs + takes_value: true + - no-broadcast: + help: Don't broadcast the transaction if it was finalized + long: no-broadcast + takes_value: false + - view: + about: View a contract + args: + - input: + help: Slatepack file containing the contract to view. + short: i + long: input + takes_value: true + - revoke: + about: Attempt to revoke a contract + args: + - tx-id: + help: Id of a transaction we want to cancel + short: i + long: tx-id + takes_value: true diff --git a/src/cli/cli.rs b/src/cli/cli.rs index c14f3d457..d4107857f 100644 --- a/src/cli/cli.rs +++ b/src/cli/cli.rs @@ -19,7 +19,8 @@ use clap::App; //use colored::Colorize; use grin_keychain as keychain; use grin_wallet_api::Owner; -use grin_wallet_config::{TorConfig, WalletConfig}; +use grin_wallet_config::config::get_global_config; +use grin_wallet_config::GlobalWalletConfig; use grin_wallet_controller::command::GlobalArgs; use grin_wallet_controller::Error; use grin_wallet_impls::DefaultWalletImpl; @@ -107,15 +108,14 @@ pub fn start_updater_thread(rx: Receiver) -> Result<(), Error> { } pub fn command_loop( + config: GlobalWalletConfig, wallet_inst: Arc>>>, keychain_mask: Option, - wallet_config: &WalletConfig, - tor_config: &TorConfig, global_wallet_args: &GlobalArgs, test_mode: bool, ) -> Result<(), Error> where - DefaultWalletImpl<'static, C>: WalletInst<'static, L, C, K>, + DefaultWalletImpl: WalletInst<'static, L, C, K>, L: WalletLCProvider<'static, C, K> + 'static, C: NodeClient + 'static, K: keychain::Keychain + 'static, @@ -151,7 +151,7 @@ where // catch updater messages let (tx, rx) = channel(); - let mut owner_api = Owner::new(wallet_inst, Some(tx)); + let mut owner_api = Owner::new(wallet_inst, Some(tx), config.config_file_path.clone()); start_updater_thread(rx)?; // start the automatic updater @@ -189,7 +189,7 @@ where let lc = wallet_lock.lc_provider().unwrap(); let mask = match lc.open_wallet( None, - wallet_args::prompt_password(&global_wallet_args.password), + wallet_args::prompt_password(&global_wallet_args.password)?, false, false, ) { @@ -218,11 +218,15 @@ where } _ => keychain_mask, }; + let config = get_global_config(&config.config_file_path) + .map_err(|e| Error::GenericError(e.to_string()))?; + let tor_config = config.tor_config(); + let wallet_config = config.members.wallet; match wallet_args::parse_and_execute( &mut owner_api, keychain_mask.clone(), &wallet_config, - &tor_config, + tor_config, &global_wallet_args, &args, test_mode, @@ -276,6 +280,8 @@ impl Completer for EditorHelper { } impl Hinter for EditorHelper { + type Hint = String; + fn hint(&self, line: &str, _pos: usize, _ctx: &Context<'_>) -> Option { let mut contents = STDIN_CONTENTS.lock(); *contents = line.into(); diff --git a/src/cmd/wallet.rs b/src/cmd/wallet.rs index 2a76b98bf..e538ef6ea 100644 --- a/src/cmd/wallet.rs +++ b/src/cmd/wallet.rs @@ -30,37 +30,35 @@ pub fn wallet_command( where C: NodeClient + 'static, { - // just get defaults from the global config - let wallet_config = config.members.clone().unwrap().wallet; - - let tor_config = config.members.unwrap().tor; - // Check the node version info, and exit with report if we're not compatible - let global_wallet_args = wallet_args::parse_global_args(&wallet_config, &wallet_args) + let global_wallet_args = wallet_args::parse_global_args(&config.members.wallet, &wallet_args) .expect("Can't read configuration file"); node_client.set_node_api_secret(global_wallet_args.node_api_secret.clone()); // This will also cache the node version info for calls to foreign API check middleware if let Some(v) = node_client.clone().get_version_info() { - if Version::parse(&v.node_version) < Version::parse(MIN_COMPAT_NODE_VERSION) { - println!("The Grin Node in use (version {}) is outdated and incompatible with this wallet version.", v.node_version); + let node_version = Version::parse(&v.node_version); + let min_version = Version::parse(MIN_COMPAT_NODE_VERSION).ok(); + if let Ok(v) = node_version { + if Some(v.clone()) < min_version { + println!("The Grin Node in use (version {}) is outdated and incompatible with this wallet version.", v); + println!( + "Please update the node to version {} or later and try again.", + MIN_COMPAT_NODE_VERSION + ); + return 1; + } + } else { println!( - "Please update the node to version {} or later and try again.", - MIN_COMPAT_NODE_VERSION + "Can not parse node version {}. Minimal compatible node version: {}", + v.node_version, MIN_COMPAT_NODE_VERSION ); return 1; } } // ... if node isn't available, allow offline functions - let res = wallet_args::wallet_command( - wallet_args, - wallet_config, - tor_config, - node_client, - false, - |_| {}, - ); + let res = wallet_args::wallet_command(wallet_args, config, node_client, false, |_| {}); // we need to give log output a chance to catch up before exiting thread::sleep(Duration::from_millis(100)); diff --git a/src/cmd/wallet_args.rs b/src/cmd/wallet_args.rs index 205cc0e20..18f0af60b 100644 --- a/src/cmd/wallet_args.rs +++ b/src/cmd/wallet_args.rs @@ -12,19 +12,19 @@ // See the License for the specific language governing permissions and // limitations under the License. +/// Argument parsing and error handling for wallet commands use crate::api::TLSConfig; use crate::cli::command_loop; use crate::config::GRIN_WALLET_DIR; use crate::util::file::get_first_line; use crate::util::secp::key::SecretKey; use crate::util::{Mutex, ZeroingString}; -/// Argument parsing and error handling for wallet commands use clap::ArgMatches; use grin_core as core; use grin_core::core::amount_to_hr_string; use grin_keychain as keychain; use grin_wallet_api::Owner; -use grin_wallet_config::{TorConfig, WalletConfig}; +use grin_wallet_config::{GlobalWalletConfig, TorConfig, WalletConfig}; use grin_wallet_controller::{command, Error}; use grin_wallet_impls::{DefaultLCProvider, DefaultWalletImpl}; use grin_wallet_libwallet::{self, Slate, SlatepackAddress, SlatepackArmor}; @@ -33,6 +33,7 @@ use linefeed::terminal::Signal; use linefeed::{Interface, ReadResult}; use rpassword; use std::convert::TryFrom; +use std::io::{IsTerminal, Write}; use std::path::{Path, PathBuf}; use std::sync::Arc; @@ -67,32 +68,46 @@ impl From for ParseError { } } -fn prompt_password_stdout(prompt: &str) -> ZeroingString { - ZeroingString::from(rpassword::prompt_password_stdout(prompt).unwrap()) +fn prompt_password_internal(prompt: &str) -> Result { + print!("{}", prompt); + std::io::stdout().flush().unwrap(); + let stdin = std::io::stdin(); + let password = if stdin.is_terminal() { + rpassword::read_password() + } else { + rpassword::read_password_with_config( + rpassword::ConfigBuilder::new() + .input_reader(stdin) + .output_discard() + .build(), + ) + } + .map_err(|e| Error::GenericError(format!("{}", e)))?; + Ok(ZeroingString::from(password)) } -pub fn prompt_password(password: &Option) -> ZeroingString { +pub fn prompt_password(password: &Option) -> Result { match password { - None => prompt_password_stdout("Password: "), - Some(p) => p.clone(), + None => Ok(prompt_password_internal("Password: ")?), + Some(p) => Ok(p.clone()), } } -fn prompt_password_confirm() -> ZeroingString { +fn prompt_password_confirm() -> Result { let mut first = ZeroingString::from("first"); let mut second = ZeroingString::from("second"); while first != second { - first = prompt_password_stdout("Password: "); - second = prompt_password_stdout("Confirm Password: "); + first = prompt_password_internal("Password: ")?; + second = prompt_password_internal("Confirm Password: ")?; } - first + Ok(first) } fn prompt_recovery_phrase( wallet: Arc>>>, ) -> Result where - DefaultWalletImpl<'static, C>: WalletInst<'static, L, C, K>, + DefaultWalletImpl: WalletInst<'static, L, C, K>, L: WalletLCProvider<'static, C, K>, C: NodeClient + 'static, K: keychain::Keychain + 'static, @@ -224,12 +239,12 @@ pub fn inst_wallet( node_client: C, ) -> Result>>>, ParseError> where - DefaultWalletImpl<'static, C>: WalletInst<'static, L, C, K>, + DefaultWalletImpl: WalletInst<'static, L, C, K>, L: WalletLCProvider<'static, C, K>, C: NodeClient + 'static, K: keychain::Keychain + 'static, { - let mut wallet = Box::new(DefaultWalletImpl::<'static, C>::new(node_client.clone()).unwrap()) + let mut wallet = Box::new(DefaultWalletImpl::::new(node_client.clone()).unwrap()) as Box>; let lc = wallet.lc_provider().unwrap(); let _ = lc.set_top_level_directory(&config.data_file_dir); @@ -287,6 +302,17 @@ fn parse_u64_or_none(arg: Option<&str>) -> Option { } } +// Parse a value expressed in Grin (e.g. "1.5") into nanograms. +fn parse_grin_amount(arg: &str, name: &str) -> Result { + // Exact decimal parsing (integer nanogrin), not f64. + core::core::amount_from_hr_string(arg).map_err(|e| { + ParseError::ArgumentError(format!( + "Could not parse {} '{}' as a number. e={}", + name, arg, e + )) + }) +} + pub fn parse_global_args( config: &WalletConfig, args: &ArgMatches, @@ -309,7 +335,7 @@ pub fn parse_global_args( let key = match config.tls_certificate_key.clone() { Some(k) => k, None => { - let msg = format!("Private key for certificate is not set"); + let msg = "Private key for certificate is not set".to_string(); return Err(ParseError::ArgumentError(msg)); } }; @@ -319,11 +345,11 @@ pub fn parse_global_args( Ok(command::GlobalArgs { account: account.to_owned(), - show_spent: show_spent, - api_secret: api_secret, - node_api_secret: node_api_secret, - password: password, - tls_conf: tls_conf, + show_spent, + api_secret, + node_api_secret, + password, + tls_conf, }) } @@ -333,9 +359,9 @@ pub fn parse_init_args( g_args: &command::GlobalArgs, args: &ArgMatches, _test_mode: bool, -) -> Result +) -> Result where - DefaultWalletImpl<'static, C>: WalletInst<'static, L, C, K>, + DefaultWalletImpl: WalletInst<'static, L, C, K>, L: WalletLCProvider<'static, C, K>, C: NodeClient + 'static, K: keychain::Keychain + 'static, @@ -345,7 +371,9 @@ where true => 16, }; let recovery_phrase = match args.is_present("recover") { - true => Some(prompt_recovery_phrase(wallet)?), + true => { + Some(prompt_recovery_phrase(wallet).map_err(|e| Error::GenericError(e.to_string()))?) + } false => None, }; @@ -355,46 +383,26 @@ where println!("Please enter a password for your new wallet"); } - let password = match g_args.password.clone() { - Some(p) => p, - None => prompt_password_confirm(), + let password = if let Some(p) = g_args.password.clone() { + p + } else { + prompt_password_confirm()? }; Ok(command::InitArgs { - list_length: list_length, - password: password, + list_length, + password, config: config.clone(), - recovery_phrase: recovery_phrase, + recovery_phrase, restore: false, }) } -pub fn parse_recover_args( - g_args: &command::GlobalArgs, -) -> Result +pub fn parse_recover_args(g_args: &command::GlobalArgs) -> Result where { - let passphrase = prompt_password(&g_args.password); - Ok(command::RecoverArgs { - passphrase: passphrase, - }) -} - -pub fn parse_listen_args( - config: &mut WalletConfig, - tor_config: &mut TorConfig, - args: &ArgMatches, -) -> Result { - if let Some(port) = args.value_of("port") { - config.api_listen_port = port.parse().unwrap(); - } - if let Some(bridge) = args.value_of("bridge") { - tor_config.bridge.bridge_line = Some(bridge.into()); - } - if args.is_present("no_tor") { - tor_config.use_tor_listener = false; - } - Ok(command::ListenArgs {}) + let passphrase = prompt_password(&g_args.password)?; + Ok(command::RecoverArgs { passphrase }) } pub fn parse_owner_api_args( @@ -417,7 +425,7 @@ pub fn parse_scan_rewind_hash_args( let start_height = parse_u64_or_none(args.value_of("start_height")); let backwards_from_tip = parse_u64_or_none(args.value_of("backwards_from_tip")); if backwards_from_tip.is_some() && start_height.is_some() { - let msg = format!("backwards_from tip and start_height cannot both be present"); + let msg = "backwards_from tip and start_height cannot both be present".to_string(); return Err(ParseError::ArgumentError(msg)); } Ok(command::ViewWalletScanArgs { @@ -432,7 +440,7 @@ pub fn parse_account_args(account_args: &ArgMatches) -> Result None, Some(s) => Some(s.to_owned()), }; - Ok(command::AccountArgs { create: create }) + Ok(command::AccountArgs { create }) } pub fn parse_send_args(args: &ArgMatches) -> Result { @@ -468,9 +476,10 @@ pub fn parse_send_args(args: &ArgMatches) -> Result d, - None => "default", + let dest = if let Some(dest) = args.value_of("dest") { + Some(dest.to_owned()) + } else { + None }; // change_outputs @@ -498,22 +507,27 @@ pub fn parse_send_args(args: &ArgMatches) -> Result match SlatepackAddress::try_from(dest) { - Ok(a) => Some(a), - Err(_) => { - if !estimate_selection_strategies { - println!("No recipient Slatepack address or provided address invalid. No payment proof will be requested."); - } - None - } - }, - true => None, + if let Some(a) = dest.clone() { + match args.is_present("no_payment_proof") { + false => Some(a), + true => None, + } + } else { + if !estimate_selection_strategies { + println!("No recipient Slatepack address or provided address invalid. No payment proof will be requested."); + } + None } }; let outfile = parse_optional(args, "outfile")?; + let skip_tor = if args.is_present("manual") { + Some(true) + } else { + None + }; + let bridge = match args.value_of("bridge") { Some(b) => Some(b.to_string()), None => None, @@ -522,24 +536,24 @@ pub fn parse_send_args(args: &ArgMatches) -> Result Result Result Result Result Result d, - None => "default", + let dest = if let Some(dest) = args.value_of("dest") { + Some(dest.to_owned()) + } else { + None }; let outfile = parse_optional(args, "outfile")?; @@ -689,14 +716,14 @@ pub fn parse_issue_invoice_args( let slatepack_qr = args.is_present("slatepack_qr"); Ok(command::IssueInvoiceArgs { - dest: dest.into(), + dest, issue_args: IssueInvoiceTxArgs { dest_acct_name: None, amount, target_slate_version, }, outfile, - slatepack_qr: slatepack_qr, + slatepack_qr, }) } @@ -772,10 +799,22 @@ pub fn parse_process_invoice_args( let outfile = parse_optional(args, "outfile")?; + let skip_tor = if args.is_present("manual") { + Some(true) + } else { + None + }; + let bridge = parse_optional(args, "bridge")?; let slatepack_qr = args.is_present("slatepack_qr"); + let ret_address = if let Some(a) = ret_address { + Some(a.to_string()) + } else { + None + }; + Ok(command::ProcessInvoiceArgs { minimum_confirmations: min_c, selection_strategy: selection_strategy.to_owned(), @@ -784,10 +823,10 @@ pub fn parse_process_invoice_args( slate, max_outputs, ttl_blocks, - skip_tor: args.is_present("manual"), + skip_tor, outfile, bridge, - slatepack_qr: slatepack_qr, + slatepack_qr, }) } @@ -805,7 +844,7 @@ pub fn parse_check_args(args: &ArgMatches) -> Result Result }, }; if tx_id.is_some() && tx_slate_id.is_some() { - let msg = format!("At most one of 'id' (-i) or 'txid' (-t) may be provided."); + let msg = "At most one of 'id' (-i) or 'txid' (-t) may be provided.".to_string(); return Err(ParseError::ArgumentError(msg)); } let count = match args.value_of("count") { @@ -840,8 +879,8 @@ pub fn parse_txs_args(args: &ArgMatches) -> Result }; Ok(command::TxsArgs { id: tx_id, - tx_slate_id: tx_slate_id, - count: count, + tx_slate_id, + count, }) } @@ -888,8 +927,8 @@ pub fn parse_repost_args(args: &ArgMatches) -> Result Result Result Result Result { + // Optional: without --encrypt-for the contract slatepack is produced unencrypted. + let counterparty_addr = args.value_of("encrypt-for").map(String::from); + + // Parse receive and send params and convert them to nano grin + let receive = match args.value_of("receive") { + Some(g) => Some(parse_grin_amount(g, "receive")?), + None => None, + }; + let send = match args.value_of("send") { + Some(g) => Some(parse_grin_amount(g, "send")?), + None => None, + }; + if receive.is_some() && send.is_some() { + return Err(ParseError::ArgumentError(String::from( + "You can only specify receive or send, not both.", + ))); + }; + if receive.is_none() && send.is_none() { + return Err(ParseError::ArgumentError(String::from( + "You must specify either --receive or --send.", + ))); + }; + let src_acct_name = Some(String::from(account)); + let add_outputs = args.is_present("add-outputs"); + let as_json = args.is_present("as-json"); + let no_payjoin = args.is_present("no-payjoin"); + let use_inputs = match args.value_of("use-inputs") { + Some(v) => { + if no_payjoin { + return Err(ParseError::ArgumentError(String::from( + "Can't use --no-payjoin with --use-inputs.", + ))); + } + Some(String::from(v)) + } + None => { + if no_payjoin { + None + } else { + // Some("any") means pick 1 random input to contribute (payjoin) + Some(String::from("any")) + } + } + }; + let make_outputs = match args.value_of("make-outputs") { + Some(v) => { + // Parse each comma-separated grin amount into nanogrin at the CLI boundary, + // so the API and Context carry explicit u64 amounts. + let mut amounts = Vec::new(); + for amt in v.split(',') { + amounts.push(parse_grin_amount(amt, "make-outputs")?); + } + Some(amounts) + } + None => None, + }; + + let num_participants = match args.value_of("num-participants") { + Some(v) => v.parse::().map_err(|e| { + ParseError::ArgumentError(format!("Could not parse num-participants '{}'. e={}", v, e)) + })?, + None => 2, + }; + + Ok(command::ContractNewArgs { + counterparty_addr: counterparty_addr, + receive: receive, + send: send, + src_acct_name: src_acct_name, + num_participants: num_participants, + as_json: as_json, + add_outputs: add_outputs, + use_inputs: use_inputs, + make_outputs: make_outputs, + // Not yet wired to CLI flags: + fee_rate: None, + outfile: None, + }) +} + +pub fn parse_contract_setup_args( + args: &ArgMatches, +) -> Result { + let counterparty_addr = match args.value_of("encrypt-for") { + Some(v) => Some(String::from(v)), + None => None, + }; + // Parse receive and send params and convert them to nano grin + let receive = match args.value_of("receive") { + Some(g) => Some(parse_grin_amount(g, "receive")?), + None => None, + }; + let send = match args.value_of("send") { + Some(g) => Some(parse_grin_amount(g, "send")?), + None => None, + }; + if receive.is_some() && send.is_some() { + return Err(ParseError::ArgumentError(String::from( + "You can only specify receive or send, not both.", + ))); + }; + let as_json = args.is_present("as-json"); + let no_payjoin = args.is_present("no-payjoin"); + let use_inputs = match args.value_of("use-inputs") { + Some(v) => { + if no_payjoin { + return Err(ParseError::ArgumentError(String::from( + "Can't use --no-payjoin with --use-inputs.", + ))); + } + Some(String::from(v)) + } + None => { + if no_payjoin { + None + } else { + // Some("any") means pick 1 random input to contribute (payjoin) + Some(String::from("any")) + } + } + }; + let make_outputs = match args.value_of("make-outputs") { + Some(v) => { + // Parse each comma-separated grin amount into nanogrin at the CLI boundary, + // so the API and Context carry explicit u64 amounts. + let mut amounts = Vec::new(); + for amt in v.split(',') { + amounts.push(parse_grin_amount(amt, "make-outputs")?); + } + Some(amounts) + } + None => None, + }; + // FUTURE: detect a reversed --receive/--send from the slate state (S1 -> receive, I1 -> send). + + Ok(command::ContractSetupArgs { + counterparty_addr: counterparty_addr, + receive: receive, + send: send, + as_json: as_json, + add_outputs: false, + use_inputs: use_inputs, + make_outputs: make_outputs, + // Not yet wired to CLI flags: + fee_rate: None, + outfile: None, + }) +} + +pub fn parse_contract_view_args( + args: &ArgMatches, +) -> Result { + let input_file = match args.is_present("input") { + true => { + let file = parse_required(args, "input")?.to_owned(); + if !Path::new(&file).is_file() { + return Err(ParseError::ArgumentError(format!( + "File {} not found.", + &file + ))); + } + Some(file) + } + false => None, + }; + + // As for receive, prompt for the slatepack when no file was given + let mut input_slatepack_message = None; + if input_file.is_none() { + input_slatepack_message = Some(prompt_slatepack()?); + } + + Ok(command::ContractViewArgs { + input_file, + input_slatepack_message, + }) +} + +pub fn parse_contract_revoke_args( + args: &ArgMatches, +) -> Result { + let tx_id_str = parse_required(args, "tx-id")?; + let tx_id = tx_id_str.parse::().map_err(|e| { + ParseError::ArgumentError(format!("Could not parse tx-id '{}'. e={}", tx_id_str, e)) + })?; + + Ok(command::ContractRevokeArgs { tx_id: tx_id }) +} + pub fn wallet_command( wallet_args: &ArgMatches, - mut wallet_config: WalletConfig, - tor_config: Option, + config: GlobalWalletConfig, mut node_client: C, test_mode: bool, wallet_inst_cb: F, @@ -977,7 +1208,7 @@ where Box< dyn WalletInst< 'static, - DefaultLCProvider<'static, C, keychain::ExtKeychain>, + DefaultLCProvider, C, keychain::ExtKeychain, >, @@ -986,6 +1217,7 @@ where >, ), { + let mut wallet_config = config.members.wallet.clone(); if let Some(dir) = wallet_args.value_of("top_level_dir") { wallet_config.data_file_dir = dir.to_string().clone(); } @@ -1008,13 +1240,6 @@ where wallet_config.data_file_dir = top_level_wallet_dir.to_str().unwrap().into(); } - // for backwards compatibility: If tor config doesn't exist in the file, assume - // the top level directory for data - let tor_config = tor_config.unwrap_or_else(|| TorConfig { - send_config_dir: wallet_config.data_file_dir.clone(), - ..Default::default() - }); - // Instantiate wallet (doesn't open the wallet) let wallet = inst_wallet::, C, keychain::ExtKeychain>( @@ -1028,12 +1253,12 @@ where { let mut wallet_lock = wallet.lock(); - let lc = wallet_lock.lc_provider().unwrap(); + let lc = wallet_lock.lc_provider()?; let _ = lc.set_top_level_directory(&wallet_config.data_file_dir); } // provide wallet instance back to the caller (handy for testing with - // local wallet proxy, etc) + // local wallet proxy, etc.) wallet_inst_cb(wallet.clone()); // don't open wallet for certain lifecycle commands @@ -1045,7 +1270,7 @@ where ("owner_api", _) => { // If wallet exists and password is present then open it. Otherwise, that's fine too. let mut wallet_lock = wallet.lock(); - let lc = wallet_lock.lc_provider().unwrap(); + let lc = wallet_lock.lc_provider()?; open_wallet = wallet_args.is_present("pass") && lc.wallet_exists(None)?; } _ => {} @@ -1054,10 +1279,10 @@ where let keychain_mask = match open_wallet { true => { let mut wallet_lock = wallet.lock(); - let lc = wallet_lock.lc_provider().unwrap(); + let lc = wallet_lock.lc_provider()?; let mask = lc.open_wallet( None, - prompt_password(&global_wallet_args.password), + prompt_password(&global_wallet_args.password)?, false, false, )?; @@ -1072,20 +1297,20 @@ where let res = match wallet_args.subcommand() { ("cli", Some(_)) => command_loop( + config, wallet, keychain_mask, - &wallet_config, - &tor_config, &global_wallet_args, test_mode, ), _ => { - let mut owner_api = Owner::new(wallet, None); + let tor_config = config.tor_config(); + let mut owner_api = Owner::new(wallet, None, config.config_file_path); parse_and_execute( &mut owner_api, keychain_mask, &wallet_config, - &tor_config, + tor_config, &global_wallet_args, &wallet_args, test_mode, @@ -1105,14 +1330,14 @@ pub fn parse_and_execute( owner_api: &mut Owner, keychain_mask: Option, wallet_config: &WalletConfig, - tor_config: &TorConfig, + tor_config: TorConfig, global_wallet_args: &command::GlobalArgs, wallet_args: &ArgMatches, test_mode: bool, cli_mode: bool, ) -> Result<(), Error> where - DefaultWalletImpl<'static, C>: WalletInst<'static, L, C, K>, + DefaultWalletImpl: WalletInst<'static, L, C, K>, L: WalletLCProvider<'static, C, K> + 'static, C: NodeClient + 'static, K: keychain::Keychain + 'static, @@ -1141,14 +1366,25 @@ where } ("listen", Some(args)) => { let mut c = wallet_config.clone(); - let mut t = tor_config.clone(); - let a = arg_parse!(parse_listen_args(&mut c, &mut t, &args)); + if let Some(port) = args.value_of("port") { + c.api_listen_port = port.parse().unwrap(); + } + let bridge = if let Some(bridge) = args.value_of("bridge") { + Some(bridge.into()) + } else { + None + }; + let use_tor = if args.is_present("no_tor") { + Some(false) + } else { + None + }; command::listen( owner_api, Arc::new(Mutex::new(keychain_mask)), - &c, - &t, - &a, + c, + bridge, + use_tor, &global_wallet_args.clone(), cli_mode, test_mode, @@ -1159,13 +1395,12 @@ where let mut g = global_wallet_args.clone(); g.tls_conf = None; arg_parse!(parse_owner_api_args(&mut c, &args)); - command::owner_api(owner_api, keychain_mask, &c, &tor_config, &g, test_mode) + command::owner_api(owner_api, keychain_mask, &c, &g, test_mode) } ("web", Some(_)) => command::owner_api( owner_api, keychain_mask, wallet_config, - tor_config, global_wallet_args, test_mode, ), @@ -1187,22 +1422,15 @@ where command::send( owner_api, km, - Some(tor_config.clone()), a, + tor_config, wallet_config.dark_background_color_scheme.unwrap_or(true), test_mode, ) } ("receive", Some(args)) => { let a = arg_parse!(parse_receive_args(&args)); - command::receive( - owner_api, - km, - &global_wallet_args, - a, - Some(tor_config.clone()), - test_mode, - ) + command::receive(owner_api, km, &global_wallet_args, a, tor_config, test_mode) } ("unpack", Some(args)) => { let a = arg_parse!(parse_unpack_args(&args)); @@ -1226,7 +1454,7 @@ where command::process_invoice( owner_api, km, - Some(tor_config.clone()), + tor_config, a, wallet_config.dark_background_color_scheme.unwrap_or(true), test_mode, @@ -1291,9 +1519,31 @@ where // for CLI mode only, should be handled externally Ok(()) } + ("contract", Some(args)) => match args.subcommand() { + ("new", Some(new_args)) => { + let account = &global_wallet_args.account; + let a = arg_parse!(parse_contract_new_args(&new_args, account)); + command::contract_new(owner_api, km, a) + } + ("sign", Some(sign_args)) => { + // Sign command takes setup_args so we use the same parser + let setup_args = arg_parse!(parse_contract_setup_args(&sign_args)); + let broadcast_tx = !sign_args.is_present("no-broadcast"); + command::contract_sign(owner_api, km, setup_args, broadcast_tx) + } + ("view", Some(view_args)) => { + let a = arg_parse!(parse_contract_view_args(&view_args)); + command::contract_view(owner_api, km, a) + } + ("revoke", Some(revoke_args)) => { + let a = arg_parse!(parse_contract_revoke_args(&revoke_args)); + command::contract_revoke(owner_api, km, a) + } + _ => Err(Error::ArgumentError(String::from("Unknown contract subcommand.")).into()), + }, _ => { - let msg = format!("Unknown wallet command, use 'grin-wallet help' for details"); - return Err(Error::ArgumentError(msg)); + let msg = "Unknown wallet command, use 'grin-wallet help' for details".to_string(); + Err(Error::ArgumentError(msg)) } } } diff --git a/tests/cmd_line_basic.rs b/tests/cmd_line_basic.rs index bce4a8d03..019e6cf13 100644 --- a/tests/cmd_line_basic.rs +++ b/tests/cmd_line_basic.rs @@ -21,6 +21,7 @@ extern crate log; extern crate grin_wallet; use grin_wallet_impls::test_framework::{self, LocalWalletClient, WalletProxy}; +use std::path::PathBuf; use clap::App; use std::thread; @@ -60,7 +61,7 @@ fn command_line_test_impl(test_dir: &str) -> Result<(), grin_wallet_controller:: // add wallet to proxy //let wallet1 = test_framework::create_wallet(&format!("{}/wallet1", test_dir), client1.clone()); let config1 = initial_setup_wallet(test_dir, "wallet1"); - let wallet_config1 = config1.clone().members.unwrap().wallet; + let wallet_config1 = config1.clone().members.wallet; let (wallet1, mask1_i) = instantiate_wallet( wallet_config1.clone(), client1.clone(), @@ -80,7 +81,7 @@ fn command_line_test_impl(test_dir: &str) -> Result<(), grin_wallet_controller:: execute_command(&app, test_dir, "wallet2", &client2, arg_vec.clone())?; let config2 = initial_setup_wallet(test_dir, "wallet2"); - let wallet_config2 = config2.clone().members.unwrap().wallet; + let wallet_config2 = config2.clone().members.wallet; let (wallet2, mask2_i) = instantiate_wallet( wallet_config2.clone(), client2.clone(), @@ -148,14 +149,14 @@ fn command_line_test_impl(test_dir: &str) -> Result<(), grin_wallet_controller:: // Mine a bit into wallet 1 so we have something to send // (TODO: Be able to stop listeners so we can test this better) - let wallet_config1 = config1.clone().members.unwrap().wallet; + let wallet_config1 = config1.clone().members.wallet; let (wallet1, mask1_i) = instantiate_wallet(wallet_config1, client1.clone(), "password1", "default")?; let mask1 = (&mask1_i).as_ref(); grin_wallet_controller::controller::owner_single_use( - Some(wallet1.clone()), + wallet1.clone(), mask1, - None, + PathBuf::from(test_dir), |api, m| { api.set_active_account(m, "mining")?; Ok(()) @@ -222,7 +223,7 @@ fn command_line_test_impl(test_dir: &str) -> Result<(), grin_wallet_controller:: execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; bh += 1; - let wallet_config1 = config1.clone().members.unwrap().wallet; + let wallet_config1 = config1.clone().members.wallet; let (wallet1, mask1_i) = instantiate_wallet( wallet_config1.clone(), client1.clone(), @@ -233,9 +234,9 @@ fn command_line_test_impl(test_dir: &str) -> Result<(), grin_wallet_controller:: // Check our transaction log, should have 10 entries grin_wallet_controller::controller::owner_single_use( - Some(wallet1.clone()), + wallet1.clone(), mask1, - None, + PathBuf::from(test_dir), |api, m| { api.set_active_account(m, "mining")?; let (refreshed, txs) = api.retrieve_txs(m, true, None, None, None)?; @@ -259,7 +260,7 @@ fn command_line_test_impl(test_dir: &str) -> Result<(), grin_wallet_controller:: execute_command(&app, test_dir, "wallet2", &client1, arg_vec)?; // check results in wallet 2 - let wallet_config2 = config2.clone().members.unwrap().wallet; + let wallet_config2 = config2.clone().members.wallet; let (wallet2, mask2_i) = instantiate_wallet( wallet_config2.clone(), client2.clone(), @@ -269,9 +270,9 @@ fn command_line_test_impl(test_dir: &str) -> Result<(), grin_wallet_controller:: let mask2 = (&mask2_i).as_ref(); grin_wallet_controller::controller::owner_single_use( - Some(wallet2.clone()), + wallet2.clone(), mask2, - None, + PathBuf::from(test_dir), |api, m| { api.set_active_account(m, "account_1")?; let (_, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; @@ -284,9 +285,9 @@ fn command_line_test_impl(test_dir: &str) -> Result<(), grin_wallet_controller:: // Send to wallet 2 with --amount_includes_fee let mut old_balance = 0; grin_wallet_controller::controller::owner_single_use( - Some(wallet1.clone()), + wallet1.clone(), mask1, - None, + PathBuf::from(test_dir), |api, m| { api.set_active_account(m, "mining")?; let (_, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; @@ -344,9 +345,9 @@ fn command_line_test_impl(test_dir: &str) -> Result<(), grin_wallet_controller:: // Check the new balance of wallet 1 reduced by EXACTLY the tx amount (instead of amount + fee) // This confirms that the TX amount was correctly computed to allow for the fee grin_wallet_controller::controller::owner_single_use( - Some(wallet1.clone()), + wallet1.clone(), mask1, - None, + PathBuf::from(test_dir), |api, m| { api.set_active_account(m, "mining")?; let (_, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; @@ -421,7 +422,7 @@ fn command_line_test_impl(test_dir: &str) -> Result<(), grin_wallet_controller:: bh += 1; // Check our transaction log, should have bh entries - let wallet_config1 = config1.clone().members.unwrap().wallet; + let wallet_config1 = config1.clone().members.wallet; let (wallet1, mask1_i) = instantiate_wallet( wallet_config1.clone(), client1.clone(), @@ -431,9 +432,9 @@ fn command_line_test_impl(test_dir: &str) -> Result<(), grin_wallet_controller:: let mask1 = (&mask1_i).as_ref(); grin_wallet_controller::controller::owner_single_use( - Some(wallet1.clone()), + wallet1.clone(), mask1, - None, + PathBuf::from(test_dir), |api, m| { api.set_active_account(m, "mining")?; let (refreshed, txs) = api.retrieve_txs(m, true, None, None, None)?; @@ -494,7 +495,7 @@ fn command_line_test_impl(test_dir: &str) -> Result<(), grin_wallet_controller:: bh += 1; // Check our transaction log, should have bh entries + 1 for self-seld - let wallet_config1 = config1.clone().members.unwrap().wallet; + let wallet_config1 = config1.clone().members.wallet; let (wallet1, mask1_i) = instantiate_wallet( wallet_config1.clone(), client1.clone(), @@ -504,9 +505,9 @@ fn command_line_test_impl(test_dir: &str) -> Result<(), grin_wallet_controller:: let mask1 = (&mask1_i).as_ref(); grin_wallet_controller::controller::owner_single_use( - Some(wallet1.clone()), + wallet1.clone(), mask1, - None, + PathBuf::from(test_dir), |api, m| { api.set_active_account(m, "mining")?; let (refreshed, txs) = api.retrieve_txs(m, true, None, None, None)?; @@ -629,15 +630,15 @@ fn command_line_test_impl(test_dir: &str) -> Result<(), grin_wallet_controller:: // get tx output via -tx parameter let mut tx_id = "".to_string(); grin_wallet_controller::controller::owner_single_use( - Some(wallet2.clone()), + wallet2.clone(), mask2, - None, + PathBuf::from(test_dir), |api, m| { api.set_active_account(m, "default")?; let (_, txs) = api.retrieve_txs(m, true, None, None, None)?; let some_tx_id = txs[0].tx_slate_id.clone(); assert!(some_tx_id.is_some()); - tx_id = some_tx_id.unwrap().to_hyphenated().to_string().clone(); + tx_id = some_tx_id.unwrap().to_string().clone(); Ok(()) }, )?; @@ -696,9 +697,9 @@ fn command_line_test_impl(test_dir: &str) -> Result<(), grin_wallet_controller:: // and recently matured coinbase outputs, which were not mature at time of spending. // This confirms that the TX amount was correctly computed to allow for the fee grin_wallet_controller::controller::owner_single_use( - Some(wallet1.clone()), + wallet1.clone(), mask1, - None, + PathBuf::from(test_dir), |api, m| { api.set_active_account(m, "mining")?; let (_, wallet1_info) = api.retrieve_summary_info(m, true, 10)?; diff --git a/tests/cmd_line_contract.rs b/tests/cmd_line_contract.rs new file mode 100644 index 000000000..67c5cafa3 --- /dev/null +++ b/tests/cmd_line_contract.rs @@ -0,0 +1,167 @@ +// 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. + +//! Test the contract command line works as expected +#[macro_use] +extern crate clap; + +#[macro_use] +extern crate log; + +extern crate grin_wallet; + +use grin_wallet_impls::test_framework::{self, LocalWalletClient, WalletProxy}; + +use clap::App; +use std::thread; +use std::time::Duration; + +use grin_keychain::ExtKeychain; +use grin_wallet_impls::DefaultLCProvider; + +mod common; +use common::{clean_output_dir, execute_command, initial_setup_wallet, instantiate_wallet, setup}; + +/// Return the single slatepack file a wallet has written +fn only_slatepack(test_dir: &str, wallet_name: &str) -> String { + let dir = format!("{}/{}/slatepack", test_dir, wallet_name); + let mut found: Vec = std::fs::read_dir(&dir) + .unwrap_or_else(|e| panic!("no slatepack dir {}: {}", dir, e)) + .filter_map(|e| e.ok()) + .map(|e| e.path().to_string_lossy().to_string()) + .filter(|p| p.ends_with(".slatepack")) + .collect(); + assert_eq!(found.len(), 1, "expected one slatepack in {}", dir); + found.pop().unwrap() +} + +fn contract_command_test_impl(test_dir: &str) -> Result<(), grin_wallet_controller::Error> { + setup(test_dir); + let mut wallet_proxy: WalletProxy< + DefaultLCProvider, + LocalWalletClient, + ExtKeychain, + > = WalletProxy::new(test_dir); + let chain = wallet_proxy.chain.clone(); + + let yml = load_yaml!("../src/bin/grin-wallet.yml"); + let app = App::from_yaml(yml); + + // Two wallets, as a contract has two parties + let arg_vec = vec!["grin-wallet", "-p", "password1", "init", "-h"]; + let client1 = LocalWalletClient::new("wallet1", wallet_proxy.tx.clone()); + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + let config1 = initial_setup_wallet(test_dir, "wallet1"); + let (wallet1, mask1_i) = instantiate_wallet( + config1.clone().members.wallet, + client1.clone(), + "password1", + "default", + )?; + wallet_proxy.add_wallet( + "wallet1", + client1.get_send_instance(), + wallet1.clone(), + mask1_i.clone(), + ); + + let arg_vec = vec!["grin-wallet", "-p", "password2", "init", "-h"]; + let client2 = LocalWalletClient::new("wallet2", wallet_proxy.tx.clone()); + execute_command(&app, test_dir, "wallet2", &client2, arg_vec)?; + let config2 = initial_setup_wallet(test_dir, "wallet2"); + let (wallet2, mask2_i) = instantiate_wallet( + config2.clone().members.wallet, + client2.clone(), + "password2", + "default", + )?; + wallet_proxy.add_wallet( + "wallet2", + client2.get_send_instance(), + wallet2.clone(), + mask2_i.clone(), + ); + + thread::spawn(move || { + if let Err(e) = wallet_proxy.run() { + error!("Wallet Proxy error: {}", e); + } + }); + + // Mine into wallet 1 so it has something to send + let mask1 = (&mask1_i).as_ref(); + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 5, false); + + // Wallet 1 opens a contract sending 1 grin, leaving an unencrypted slatepack because + // no --encrypt-for was given + let arg_vec = vec![ + "grin-wallet", + "-p", + "password1", + "contract", + "new", + "--send", + "1", + ]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + // Wallet 2 views the contract before signing it + let file_name = only_slatepack(test_dir, "wallet1"); + let arg_vec = vec![ + "grin-wallet", + "-p", + "password2", + "contract", + "view", + "-i", + &file_name, + ]; + execute_command(&app, test_dir, "wallet2", &client2, arg_vec)?; + + // Wallet 1 can view its own contract too + let arg_vec = vec![ + "grin-wallet", + "-p", + "password1", + "contract", + "view", + "-i", + &file_name, + ]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + // A file that isn't there is reported, not panicked on + let arg_vec = vec![ + "grin-wallet", + "-p", + "password1", + "contract", + "view", + "-i", + "no/such/file.slatepack", + ]; + assert!(execute_command(&app, test_dir, "wallet1", &client1, arg_vec).is_err()); + + // let logging finish + thread::sleep(Duration::from_millis(200)); + Ok(()) +} + +#[test] +fn wallet_contract_command_line() { + let test_dir = "target/test_output/contract_command_line"; + if let Err(e) = contract_command_test_impl(test_dir) { + panic!("Libwallet Error: {}", e); + } + clean_output_dir(test_dir); +} diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 211645734..7833061f0 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -36,6 +36,7 @@ use util::secp::key::{PublicKey, SecretKey}; use grin_api as api; use grin_wallet::cmd::wallet_args; +use grin_wallet_config::config::reload_global_config; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; @@ -72,7 +73,7 @@ macro_rules! setup_proxy { // add wallet to proxy let config1 = initial_setup_wallet($test_dir, "wallet1"); - let wallet_config1 = config1.clone().members.unwrap().wallet; + let wallet_config1 = config1.clone().members.wallet; //config1.owner_api_listen_port = Some(13420); let ($wallet1, mask1_i) = instantiate_wallet( wallet_config1.clone(), @@ -97,7 +98,7 @@ macro_rules! setup_proxy { } let config2 = initial_setup_wallet($test_dir, "wallet2"); - let wallet_config2 = config2.clone().members.unwrap().wallet; + let wallet_config2 = config2.clone().members.wallet; //config2.api_listen_port = 23415; let ($wallet2, mask2_i) = instantiate_wallet( wallet_config2.clone(), @@ -124,14 +125,14 @@ macro_rules! setup_proxy { #[allow(dead_code)] pub fn clean_output_dir(test_dir: &str) { - let _ = remove_dir_all::remove_dir_all(test_dir); + let _ = fs::remove_dir_all(test_dir); } #[allow(dead_code)] pub fn setup(test_dir: &str) { util::init_test_logger(); clean_output_dir(test_dir); - global::set_local_chain_type(global::ChainTypes::AutomatedTesting); + global::set_local_chain_type(ChainTypes::AutomatedTesting); } /// Some tests require the global chain_type to be configured. @@ -141,7 +142,7 @@ pub fn setup(test_dir: &str) { /// 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); + global::init_global_chain_type(ChainTypes::AutomatedTesting); } /// Create a wallet config file in the given current directory @@ -150,7 +151,6 @@ pub fn config_command_wallet( wallet_name: &str, ) -> Result<(), grin_wallet_controller::Error> { let mut current_dir; - let mut default_config = GlobalWalletConfig::default(); current_dir = env::current_dir().unwrap_or_else(|e| { panic!("Error creating config file: {}", e); }); @@ -165,6 +165,9 @@ pub fn config_command_wallet( .to_owned(), ))?; } + + let mut default_config = + GlobalWalletConfig::for_chain(&ChainTypes::AutomatedTesting, &config_file_name); default_config.update_paths(¤t_dir, ¤t_dir); default_config .write_to_file(config_file_name.to_str().unwrap(), false, None, None) @@ -191,7 +194,7 @@ pub fn initial_setup_wallet(dir_name: &str, wallet_name: &str) -> GlobalWalletCo let _ = fs::create_dir_all(current_dir.clone()); let mut config_file_name = current_dir.clone(); config_file_name.push("grin-wallet.toml"); - GlobalWalletConfig::new(config_file_name.to_str().unwrap()).unwrap() + reload_global_config(&config_file_name).unwrap() } fn get_wallet_subcommand<'a>( @@ -211,8 +214,8 @@ fn get_wallet_subcommand<'a>( _ => ArgMatches::new(), } } -// -// Helper to create an instance of the LMDB wallet + +/// Helper to create an instance of the LMDB wallet #[allow(dead_code)] pub fn instantiate_wallet( mut wallet_config: WalletConfig, @@ -226,7 +229,7 @@ pub fn instantiate_wallet( Box< dyn WalletInst< 'static, - DefaultLCProvider<'static, LocalWalletClient, ExtKeychain>, + DefaultLCProvider, LocalWalletClient, ExtKeychain, >, @@ -241,7 +244,7 @@ pub fn instantiate_wallet( let mut wallet = Box::new(DefaultWalletImpl::::new(node_client).unwrap()) as Box< dyn WalletInst< - DefaultLCProvider<'static, LocalWalletClient, ExtKeychain>, + DefaultLCProvider, LocalWalletClient, ExtKeychain, >, @@ -275,21 +278,10 @@ pub fn execute_command( let args = app.clone().get_matches_from(arg_vec); let _ = get_wallet_subcommand(test_dir, wallet_name, args.clone()); let config = initial_setup_wallet(test_dir, wallet_name); - let mut wallet_config = config.clone().members.unwrap().wallet; - let tor_config = config.clone().members.unwrap().tor; - //unset chain type so it doesn't get reset - wallet_config.chain_type = None; - wallet_args::wallet_command( - &args, - wallet_config.clone(), - tor_config, - client.clone(), - true, - |_| {}, - ) + wallet_args::wallet_command(&args, config, client.clone(), true, |_| {}) } -// as above, but without necessarily setting up the wallet +/// As above, but without necessarily setting up the wallet #[allow(dead_code)] pub fn execute_command_no_setup( app: &App, @@ -302,29 +294,19 @@ pub fn execute_command_no_setup( where C: NodeClient + 'static + Clone, F: FnOnce( - Arc< - Mutex< - Box< - dyn WalletInst< - 'static, - DefaultLCProvider<'static, C, ExtKeychain>, - C, - ExtKeychain, - >, - >, - >, - >, + Arc, C, ExtKeychain>>>>, ), { let args = app.clone().get_matches_from(arg_vec); let _ = get_wallet_subcommand(test_dir, wallet_name, args.clone()); - let config = config::initial_setup_wallet(&ChainTypes::AutomatedTesting, None, true).unwrap(); - let mut wallet_config = config.clone().members.unwrap().wallet; + let mut config = + config::initial_setup_wallet(&ChainTypes::AutomatedTesting, None, true).unwrap(); + let mut wallet_config = config.clone().members.wallet; wallet_config.chain_type = None; wallet_config.api_secret_path = None; wallet_config.node_api_secret_path = None; - let tor_config = config.members.unwrap().tor.clone(); - wallet_args::wallet_command(&args, wallet_config, tor_config, client.clone(), true, f) + config.members.wallet = wallet_config; + wallet_args::wallet_command(&args, config, client.clone(), true, f) } pub fn post(url: &Url, api_secret: Option, input: &IN) -> Result diff --git a/tests/owner_v3_lifecycle.rs b/tests/owner_v3_lifecycle.rs index 42295d50c..997bdb342 100644 --- a/tests/owner_v3_lifecycle.rs +++ b/tests/owner_v3_lifecycle.rs @@ -27,6 +27,7 @@ use std::thread; use std::time::Duration; use grin_keychain::ExtKeychain; +use grin_wallet_config::GlobalWalletConfig; use grin_wallet_impls::DefaultLCProvider; use grin_wallet_libwallet::{InitTxArgs, Slate, SlateVersion, VersionedSlate}; use serde_json; @@ -57,7 +58,7 @@ fn owner_v3_lifecycle() -> Result<(), grin_wallet_controller::Error> { let wallet_proxy_a: Arc< Mutex< WalletProxy< - DefaultLCProvider<'static, LocalWalletClient, ExtKeychain>, + DefaultLCProvider, LocalWalletClient, ExtKeychain, >, @@ -74,7 +75,7 @@ fn owner_v3_lifecycle() -> Result<(), grin_wallet_controller::Error> { execute_command(&app, test_dir, "wallet2", &client2, arg_vec.clone())?; let config2 = initial_setup_wallet(test_dir, "wallet2"); - let wallet_config2 = config2.clone().members.unwrap().wallet; + let wallet_config2 = config2.clone().members.wallet; //config2.api_listen_port = 23415; let (wallet2, mask2_i) = instantiate_wallet( wallet_config2.clone(), @@ -193,6 +194,26 @@ fn owner_v3_lifecycle() -> Result<(), grin_wallet_controller::Error> { let pb = PathBuf::from(format!("{}/wallet1/grin-wallet.toml", test_dir)); assert!(pb.exists()); + let req = serde_json::json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "set_tor_config", + "params": { + "tor_config": null + } + }); + let res = send_request_enc::<()>( + &JsonId::StrId(String::from("1")), + 1, + "http://127.0.0.1:43420/v3/owner", + &req.to_string(), + &shared_key, + )?; + assert!(res.is_ok()); + let tor = GlobalWalletConfig::new(pb.clone()).unwrap().tor_config(); + assert!(!tor.use_tor_listener); + assert_eq!(tor.skip_send_attempt, Some(true)); + // 5) Try and perform an operation without having a wallet open let req = include_str!("data/v3_reqs/retrieve_info.req.json"); let res = send_request_enc::( @@ -382,9 +403,9 @@ fn owner_v3_lifecycle() -> Result<(), grin_wallet_controller::Error> { // give this slate over to wallet 2 manually grin_wallet_controller::controller::owner_single_use( - Some(wallet2.clone()), + wallet2.clone(), mask2, - None, + PathBuf::from(test_dir), |api, m| { let args = InitTxArgs { src_acct_name: None, diff --git a/util/Cargo.toml b/util/Cargo.toml index be2005733..7552d896a 100644 --- a/util/Cargo.toml +++ b/util/Cargo.toml @@ -10,31 +10,15 @@ workspace = ".." edition = "2021" [dependencies] -rand = "0.6" serde = "1" serde_derive = "1" -ed25519-dalek = "1.0.0-pre.4" +ed25519-dalek = "2" data-encoding = "2" sha3 = "0.8" thiserror = "1" ##### Grin Imports -# For Release -grin_util = "5.4.1" +grin_util = { path = "../grin/util"} -# For beta release - -# grin_util = { git = "https://github.com/mimblewimble/grin", tag = "v5.2.0-beta.3" } - -# For bleeding edge -# grin_util = { git = "https://github.com/mimblewimble/grin", branch = "master" } - -# For local testing - -# grin_util = { path = "../../grin/util"} - -##### - -[dev-dependencies] -pretty_assertions = "0.6" +###### diff --git a/util/src/ov3.rs b/util/src/ov3.rs index 8c16b3d2b..924523f10 100644 --- a/util/src/ov3.rs +++ b/util/src/ov3.rs @@ -13,8 +13,8 @@ // limitations under the License. use data_encoding::BASE32; -use ed25519_dalek::PublicKey as DalekPublicKey; -use ed25519_dalek::SecretKey as DalekSecretKey; +use ed25519_dalek::SigningKey as DalekSecretKey; +use ed25519_dalek::VerifyingKey as DalekPublicKey; use grin_util::from_hex; use sha3::{Digest, Sha3_256}; use std::convert::TryFrom; @@ -49,15 +49,7 @@ impl OnionV3Address { /// populate from a private key pub fn from_private(key: &[u8; 32]) -> Result { - let d_skey = match DalekSecretKey::from_bytes(key) { - Ok(k) => k, - Err(e) => { - return Err(OnionV3Error::InvalidPrivateKey(format!( - "Unable to create public key: {}", - e - ))); - } - }; + let d_skey = DalekSecretKey::from_bytes(key); let d_pub_key: DalekPublicKey = (&d_skey).into(); Ok(OnionV3Address(*d_pub_key.as_bytes())) }