From 2e12b17663c4d4f53b77b10de55448d92ef52a53 Mon Sep 17 00:00:00 2001 From: ardocrat Date: Fri, 28 Jul 2023 00:00:57 +0300 Subject: [PATCH] build + wallet + ui: fix dependencies connection setup, mnemonic validation, wallet creation, methods to work with wallet, update translations --- Cargo.lock | 749 ++++++++---------- Cargo.toml | 33 +- locales/en.yml | 1 + locales/ru.yml | 1 + src/gui/views/views.rs | 2 +- src/gui/views/wallets/creation/creation.rs | 141 ++-- src/gui/views/wallets/creation/mnemonic.rs | 8 + src/gui/views/wallets/creation/types.rs | 12 +- src/gui/views/wallets/setup/connection.rs | 13 +- src/gui/views/wallets/wallets.rs | 43 +- src/lib.rs | 14 +- src/wallet/config.rs | 43 +- src/wallet/keys.rs | 128 ++++ src/wallet/list.rs | 44 +- src/wallet/mod.rs | 5 + src/wallet/selection.rs | 714 ++++++++++++++++++ src/wallet/tx.rs | 547 ++++++++++++++ src/wallet/updater.rs | 837 +++++++++++++++++++++ src/wallet/wallet.rs | 498 +++++++++++- 19 files changed, 3243 insertions(+), 590 deletions(-) create mode 100644 src/wallet/keys.rs create mode 100644 src/wallet/selection.rs create mode 100644 src/wallet/tx.rs create mode 100644 src/wallet/updater.rs diff --git a/Cargo.lock b/Cargo.lock index a57b1c1..ace9415 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -254,9 +254,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.71" +version = "1.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" +checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854" [[package]] name = "arboard" @@ -358,7 +358,7 @@ dependencies = [ "async-lock", "async-task", "concurrent-queue", - "fastrand", + "fastrand 1.9.0", "futures-lite", "slab", ] @@ -428,9 +428,9 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba" dependencies = [ - "proc-macro2 1.0.64", - "quote 1.0.29", - "syn 2.0.25", + "proc-macro2 1.0.66", + "quote 1.0.32", + "syn 2.0.27", ] [[package]] @@ -441,13 +441,13 @@ checksum = "ecc7ab41815b3c653ccd2978ec3255c81349336702dfdf62ee6f7069b12a3aae" [[package]] name = "async-trait" -version = "0.1.71" +version = "0.1.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a564d521dd56509c4c47480d00b80ee55f7e385ae48db5744c67ad50c92d2ebf" +checksum = "cc6dde6e4ed435a4c1ee4e73592f5ba9da2151af10076cc04858746af9352d09" dependencies = [ - "proc-macro2 1.0.64", - "quote 1.0.29", - "syn 2.0.25", + "proc-macro2 1.0.66", + "quote 1.0.32", + "syn 2.0.27", ] [[package]] @@ -485,7 +485,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb4870a32c0eaa17e35bca0e6b16020635157121fb7d45593d242c295bc768" dependencies = [ - "quote 1.0.29", + "quote 1.0.32", "syn 1.0.109", ] @@ -591,8 +591,8 @@ dependencies = [ "lazycell", "log", "peeking_take_while", - "proc-macro2 1.0.64", - "quote 1.0.29", + "proc-macro2 1.0.66", + "quote 1.0.32", "regex", "rustc-hash", "shlex", @@ -716,7 +716,7 @@ dependencies = [ "async-lock", "async-task", "atomic-waker", - "fastrand", + "fastrand 1.9.0", "futures-lite", "log", ] @@ -774,9 +774,9 @@ version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdde5c9cd29ebd706ce1b35600920a33550e402fc998a2e53ad3b42c3c47a192" dependencies = [ - "proc-macro2 1.0.64", - "quote 1.0.29", - "syn 2.0.25", + "proc-macro2 1.0.66", + "quote 1.0.32", + "syn 2.0.27", ] [[package]] @@ -918,7 +918,7 @@ dependencies = [ "android-tzdata", "iana-time-zone", "js-sys", - "num-traits 0.2.15", + "num-traits 0.2.16", "serde", "time", "wasm-bindgen", @@ -1294,8 +1294,8 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" dependencies = [ - "proc-macro2 1.0.64", - "quote 1.0.29", + "proc-macro2 1.0.66", + "quote 1.0.32", "syn 1.0.109", ] @@ -1382,9 +1382,9 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ - "proc-macro2 1.0.64", - "quote 1.0.29", - "syn 2.0.25", + "proc-macro2 1.0.66", + "quote 1.0.32", + "syn 2.0.27", ] [[package]] @@ -1569,9 +1569,9 @@ dependencies = [ [[package]] name = "either" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" [[package]] name = "emath" @@ -1616,9 +1616,9 @@ version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e9a1f9f7d83e59740248a6e14ecf93929ade55027844dfcea78beafccc15745" dependencies = [ - "proc-macro2 1.0.64", - "quote 1.0.29", - "syn 2.0.25", + "proc-macro2 1.0.66", + "quote 1.0.32", + "syn 2.0.27", ] [[package]] @@ -1722,6 +1722,12 @@ dependencies = [ "instant", ] +[[package]] +name = "fastrand" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" + [[package]] name = "fdeflate" version = "0.3.0" @@ -1916,7 +1922,7 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" dependencies = [ - "fastrand", + "fastrand 1.9.0", "futures-core", "futures-io", "memchr", @@ -1931,9 +1937,9 @@ version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ - "proc-macro2 1.0.64", - "quote 1.0.29", - "syn 2.0.25", + "proc-macro2 1.0.66", + "quote 1.0.32", + "syn 2.0.27", ] [[package]] @@ -2085,9 +2091,9 @@ dependencies = [ [[package]] name = "glow" -version = "0.12.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "807edf58b70c0b5b2181dd39fe1839dbdb3ba02645630dc5f753e23da307f762" +checksum = "ca0fe580e4b60a8ab24a868bc08e2f03cbcb20d3d676601fa909386713333728" dependencies = [ "js-sys", "slotmap", @@ -2217,27 +2223,34 @@ version = "0.1.0" dependencies = [ "android_logger", "built", + "byteorder", "chrono", "dirs 5.0.1", + "ed25519-dalek", "eframe", "egui", "egui_extras", "env_logger 0.10.0", "futures 0.3.28", - "grin_api 5.2.0-beta.2", - "grin_chain 5.2.0-beta.2", + "grin_api", + "grin_chain", "grin_config", - "grin_core 5.2.0-beta.2", - "grin_keychain 5.2.0-beta.2", - "grin_p2p 5.2.0-beta.2", + "grin_core", + "grin_keychain", + "grin_p2p", "grin_servers", - "grin_util 5.2.0-beta.2", + "grin_util", + "grin_wallet_api", "grin_wallet_impls", + "grin_wallet_libwallet", + "grin_wallet_util", "jni", "lazy_static", "log", + "num-bigint 0.4.3", "once_cell", "openssl-sys", + "parking_lot 0.10.2", "pnet", "pollster", "rand 0.8.5", @@ -2250,6 +2263,7 @@ dependencies = [ "tokio-util 0.7.8", "toml 0.7.6", "url", + "uuid", "wgpu", "winit", "zeroize", @@ -2258,48 +2272,17 @@ dependencies = [ [[package]] name = "grin_api" version = "5.2.0-beta.2" +source = "git+https://github.com/mimblewimble/grin?branch=master#399fb19c3014a4a5c3f0575dd222e7df6fda8c83" dependencies = [ "bytes 0.5.6", "easy-jsonrpc-mw", "futures 0.3.28", - "grin_chain 5.2.0-beta.2", - "grin_core 5.2.0-beta.2", - "grin_p2p 5.2.0-beta.2", - "grin_pool 5.2.0-beta.2", - "grin_store 5.2.0-beta.2", - "grin_util 5.2.0-beta.2", - "http", - "hyper", - "hyper-rustls 0.20.0", - "hyper-timeout", - "lazy_static", - "log", - "regex", - "ring", - "rustls 0.17.0", - "serde", - "serde_derive", - "serde_json", - "thiserror", - "tokio 0.2.25", - "tokio-rustls 0.13.1", - "url", -] - -[[package]] -name = "grin_api" -version = "5.2.0-beta.2" -source = "git+https://github.com/mimblewimble/grin?branch=master#b69f18d0a206c398e604b7841e2cb89332cfddf9" -dependencies = [ - "bytes 0.5.6", - "easy-jsonrpc-mw", - "futures 0.3.28", - "grin_chain 5.2.0-beta.2 (git+https://github.com/mimblewimble/grin?branch=master)", - "grin_core 5.2.0-beta.2 (git+https://github.com/mimblewimble/grin?branch=master)", - "grin_p2p 5.2.0-beta.2 (git+https://github.com/mimblewimble/grin?branch=master)", - "grin_pool 5.2.0-beta.2 (git+https://github.com/mimblewimble/grin?branch=master)", - "grin_store 5.2.0-beta.2 (git+https://github.com/mimblewimble/grin?branch=master)", - "grin_util 5.2.0-beta.2 (git+https://github.com/mimblewimble/grin?branch=master)", + "grin_chain", + "grin_core", + "grin_p2p", + "grin_pool", + "grin_store", + "grin_util", "http", "hyper", "hyper-rustls 0.20.0", @@ -2321,6 +2304,7 @@ dependencies = [ [[package]] name = "grin_chain" version = "5.2.0-beta.2" +source = "git+https://github.com/mimblewimble/grin?branch=master#399fb19c3014a4a5c3f0575dd222e7df6fda8c83" dependencies = [ "bit-vec", "bitflags 1.3.2", @@ -2328,33 +2312,10 @@ dependencies = [ "chrono", "croaring", "enum_primitive", - "grin_core 5.2.0-beta.2", - "grin_keychain 5.2.0-beta.2", - "grin_store 5.2.0-beta.2", - "grin_util 5.2.0-beta.2", - "lazy_static", - "log", - "lru-cache", - "serde", - "serde_derive", - "thiserror", -] - -[[package]] -name = "grin_chain" -version = "5.2.0-beta.2" -source = "git+https://github.com/mimblewimble/grin?branch=master#b69f18d0a206c398e604b7841e2cb89332cfddf9" -dependencies = [ - "bit-vec", - "bitflags 1.3.2", - "byteorder", - "chrono", - "croaring", - "enum_primitive", - "grin_core 5.2.0-beta.2 (git+https://github.com/mimblewimble/grin?branch=master)", - "grin_keychain 5.2.0-beta.2 (git+https://github.com/mimblewimble/grin?branch=master)", - "grin_store 5.2.0-beta.2 (git+https://github.com/mimblewimble/grin?branch=master)", - "grin_util 5.2.0-beta.2 (git+https://github.com/mimblewimble/grin?branch=master)", + "grin_core", + "grin_keychain", + "grin_store", + "grin_util", "lazy_static", "log", "lru-cache", @@ -2366,12 +2327,13 @@ dependencies = [ [[package]] name = "grin_config" version = "5.2.0-beta.2" +source = "git+https://github.com/mimblewimble/grin?branch=master#399fb19c3014a4a5c3f0575dd222e7df6fda8c83" dependencies = [ "dirs 2.0.2", - "grin_core 5.2.0-beta.2", - "grin_p2p 5.2.0-beta.2", + "grin_core", + "grin_p2p", "grin_servers", - "grin_util 5.2.0-beta.2", + "grin_util", "rand 0.6.5", "serde", "serde_derive", @@ -2381,6 +2343,7 @@ dependencies = [ [[package]] name = "grin_core" version = "5.2.0-beta.2" +source = "git+https://github.com/mimblewimble/grin?branch=master#399fb19c3014a4a5c3f0575dd222e7df6fda8c83" dependencies = [ "blake2-rfc", "byteorder", @@ -2388,39 +2351,13 @@ dependencies = [ "chrono", "croaring", "enum_primitive", - "grin_keychain 5.2.0-beta.2", - "grin_util 5.2.0-beta.2", + "grin_keychain", + "grin_util", "lazy_static", "log", "lru-cache", "num", - "num-bigint", - "rand 0.6.5", - "serde", - "serde_derive", - "siphasher", - "thiserror", - "zeroize", -] - -[[package]] -name = "grin_core" -version = "5.2.0-beta.2" -source = "git+https://github.com/mimblewimble/grin?branch=master#b69f18d0a206c398e604b7841e2cb89332cfddf9" -dependencies = [ - "blake2-rfc", - "byteorder", - "bytes 0.5.6", - "chrono", - "croaring", - "enum_primitive", - "grin_keychain 5.2.0-beta.2 (git+https://github.com/mimblewimble/grin?branch=master)", - "grin_util 5.2.0-beta.2 (git+https://github.com/mimblewimble/grin?branch=master)", - "lazy_static", - "log", - "lru-cache", - "num", - "num-bigint", + "num-bigint 0.2.6", "rand 0.6.5", "serde", "serde_derive", @@ -2432,33 +2369,12 @@ dependencies = [ [[package]] name = "grin_keychain" version = "5.2.0-beta.2" +source = "git+https://github.com/mimblewimble/grin?branch=master#399fb19c3014a4a5c3f0575dd222e7df6fda8c83" dependencies = [ "blake2-rfc", "byteorder", "digest 0.9.0", - "grin_util 5.2.0-beta.2", - "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_keychain" -version = "5.2.0-beta.2" -source = "git+https://github.com/mimblewimble/grin?branch=master#b69f18d0a206c398e604b7841e2cb89332cfddf9" -dependencies = [ - "blake2-rfc", - "byteorder", - "digest 0.9.0", - "grin_util 5.2.0-beta.2 (git+https://github.com/mimblewimble/grin?branch=master)", + "grin_util", "hmac 0.11.0", "lazy_static", "log", @@ -2475,37 +2391,16 @@ dependencies = [ [[package]] name = "grin_p2p" version = "5.2.0-beta.2" +source = "git+https://github.com/mimblewimble/grin?branch=master#399fb19c3014a4a5c3f0575dd222e7df6fda8c83" dependencies = [ "bitflags 1.3.2", "bytes 0.5.6", "chrono", "enum_primitive", - "grin_chain 5.2.0-beta.2", - "grin_core 5.2.0-beta.2", - "grin_store 5.2.0-beta.2", - "grin_util 5.2.0-beta.2", - "log", - "lru-cache", - "num", - "rand 0.6.5", - "serde", - "serde_derive", - "tempfile", -] - -[[package]] -name = "grin_p2p" -version = "5.2.0-beta.2" -source = "git+https://github.com/mimblewimble/grin?branch=master#b69f18d0a206c398e604b7841e2cb89332cfddf9" -dependencies = [ - "bitflags 1.3.2", - "bytes 0.5.6", - "chrono", - "enum_primitive", - "grin_chain 5.2.0-beta.2 (git+https://github.com/mimblewimble/grin?branch=master)", - "grin_core 5.2.0-beta.2 (git+https://github.com/mimblewimble/grin?branch=master)", - "grin_store 5.2.0-beta.2 (git+https://github.com/mimblewimble/grin?branch=master)", - "grin_util 5.2.0-beta.2 (git+https://github.com/mimblewimble/grin?branch=master)", + "grin_chain", + "grin_core", + "grin_store", + "grin_util", "log", "lru-cache", "num", @@ -2518,29 +2413,13 @@ dependencies = [ [[package]] name = "grin_pool" version = "5.2.0-beta.2" +source = "git+https://github.com/mimblewimble/grin?branch=master#399fb19c3014a4a5c3f0575dd222e7df6fda8c83" dependencies = [ "blake2-rfc", "chrono", - "grin_core 5.2.0-beta.2", - "grin_keychain 5.2.0-beta.2", - "grin_util 5.2.0-beta.2", - "log", - "rand 0.6.5", - "serde", - "serde_derive", - "thiserror", -] - -[[package]] -name = "grin_pool" -version = "5.2.0-beta.2" -source = "git+https://github.com/mimblewimble/grin?branch=master#b69f18d0a206c398e604b7841e2cb89332cfddf9" -dependencies = [ - "blake2-rfc", - "chrono", - "grin_core 5.2.0-beta.2 (git+https://github.com/mimblewimble/grin?branch=master)", - "grin_keychain 5.2.0-beta.2 (git+https://github.com/mimblewimble/grin?branch=master)", - "grin_util 5.2.0-beta.2 (git+https://github.com/mimblewimble/grin?branch=master)", + "grin_core", + "grin_keychain", + "grin_util", "log", "rand 0.6.5", "serde", @@ -2567,18 +2446,19 @@ dependencies = [ [[package]] name = "grin_servers" version = "5.2.0-beta.2" +source = "git+https://github.com/mimblewimble/grin?branch=master#399fb19c3014a4a5c3f0575dd222e7df6fda8c83" dependencies = [ "chrono", "fs2", "futures 0.3.28", - "grin_api 5.2.0-beta.2", - "grin_chain 5.2.0-beta.2", - "grin_core 5.2.0-beta.2", - "grin_keychain 5.2.0-beta.2", - "grin_p2p 5.2.0-beta.2", - "grin_pool 5.2.0-beta.2", - "grin_store 5.2.0-beta.2", - "grin_util 5.2.0-beta.2", + "grin_api", + "grin_chain", + "grin_core", + "grin_keychain", + "grin_p2p", + "grin_pool", + "grin_store", + "grin_util", "http", "hyper", "hyper-rustls 0.20.0", @@ -2596,30 +2476,12 @@ dependencies = [ [[package]] name = "grin_store" version = "5.2.0-beta.2" +source = "git+https://github.com/mimblewimble/grin?branch=master#399fb19c3014a4a5c3f0575dd222e7df6fda8c83" dependencies = [ "byteorder", "croaring", - "grin_core 5.2.0-beta.2", - "grin_util 5.2.0-beta.2", - "libc", - "lmdb-zero", - "log", - "memmap", - "serde", - "serde_derive", - "tempfile", - "thiserror", -] - -[[package]] -name = "grin_store" -version = "5.2.0-beta.2" -source = "git+https://github.com/mimblewimble/grin?branch=master#b69f18d0a206c398e604b7841e2cb89332cfddf9" -dependencies = [ - "byteorder", - "croaring", - "grin_core 5.2.0-beta.2 (git+https://github.com/mimblewimble/grin?branch=master)", - "grin_util 5.2.0-beta.2 (git+https://github.com/mimblewimble/grin?branch=master)", + "grin_core", + "grin_util", "libc", "lmdb-zero", "log", @@ -2633,6 +2495,7 @@ dependencies = [ [[package]] name = "grin_util" version = "5.2.0-beta.2" +source = "git+https://github.com/mimblewimble/grin?branch=master#399fb19c3014a4a5c3f0575dd222e7df6fda8c83" dependencies = [ "backtrace", "base64 0.12.3", @@ -2651,33 +2514,38 @@ dependencies = [ ] [[package]] -name = "grin_util" -version = "5.2.0-beta.2" -source = "git+https://github.com/mimblewimble/grin?branch=master#b69f18d0a206c398e604b7841e2cb89332cfddf9" +name = "grin_wallet_api" +version = "5.2.0-alpha.1" +source = "git+https://github.com/mimblewimble/grin-wallet?branch=master#0b491fea0fd5aa21fd28c171ed775e70f0877661" dependencies = [ - "backtrace", "base64 0.12.3", - "byteorder", - "grin_secp256k1zkp", - "lazy_static", + "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", - "log4rs", - "parking_lot 0.10.2", "rand 0.6.5", + "ring", "serde", "serde_derive", - "walkdir", - "zeroize", - "zip", + "serde_json", + "uuid", ] [[package]] name = "grin_wallet_config" version = "5.2.0-alpha.1" +source = "git+https://github.com/mimblewimble/grin-wallet?branch=master#0b491fea0fd5aa21fd28c171ed775e70f0877661" dependencies = [ "dirs 2.0.2", - "grin_core 5.2.0-beta.2 (git+https://github.com/mimblewimble/grin?branch=master)", - "grin_util 5.2.0-beta.2 (git+https://github.com/mimblewimble/grin?branch=master)", + "grin_core", + "grin_util", "grin_wallet_util", "rand 0.6.5", "serde", @@ -2688,6 +2556,7 @@ dependencies = [ [[package]] name = "grin_wallet_impls" version = "5.2.0-alpha.1" +source = "git+https://github.com/mimblewimble/grin-wallet?branch=master#0b491fea0fd5aa21fd28c171ed775e70f0877661" dependencies = [ "base64 0.12.3", "blake2-rfc", @@ -2696,12 +2565,12 @@ dependencies = [ "data-encoding", "ed25519-dalek", "futures 0.3.28", - "grin_api 5.2.0-beta.2 (git+https://github.com/mimblewimble/grin?branch=master)", - "grin_chain 5.2.0-beta.2 (git+https://github.com/mimblewimble/grin?branch=master)", - "grin_core 5.2.0-beta.2 (git+https://github.com/mimblewimble/grin?branch=master)", - "grin_keychain 5.2.0-beta.2 (git+https://github.com/mimblewimble/grin?branch=master)", - "grin_store 5.2.0-beta.2 (git+https://github.com/mimblewimble/grin?branch=master)", - "grin_util 5.2.0-beta.2 (git+https://github.com/mimblewimble/grin?branch=master)", + "grin_api", + "grin_chain", + "grin_core", + "grin_keychain", + "grin_store", + "grin_util", "grin_wallet_config", "grin_wallet_libwallet", "grin_wallet_util", @@ -2726,6 +2595,7 @@ dependencies = [ [[package]] name = "grin_wallet_libwallet" version = "5.2.0-alpha.1" +source = "git+https://github.com/mimblewimble/grin-wallet?branch=master#0b491fea0fd5aa21fd28c171ed775e70f0877661" dependencies = [ "age", "base64 0.9.3", @@ -2736,15 +2606,15 @@ dependencies = [ "chrono", "curve25519-dalek 2.1.3", "ed25519-dalek", - "grin_core 5.2.0-beta.2 (git+https://github.com/mimblewimble/grin?branch=master)", - "grin_keychain 5.2.0-beta.2 (git+https://github.com/mimblewimble/grin?branch=master)", - "grin_store 5.2.0-beta.2 (git+https://github.com/mimblewimble/grin?branch=master)", - "grin_util 5.2.0-beta.2 (git+https://github.com/mimblewimble/grin?branch=master)", + "grin_core", + "grin_keychain", + "grin_store", + "grin_util", "grin_wallet_config", "grin_wallet_util", "lazy_static", "log", - "num-bigint", + "num-bigint 0.2.6", "rand 0.6.5", "regex", "secrecy 0.6.0", @@ -2762,10 +2632,11 @@ dependencies = [ [[package]] name = "grin_wallet_util" version = "5.2.0-alpha.1" +source = "git+https://github.com/mimblewimble/grin-wallet?branch=master#0b491fea0fd5aa21fd28c171ed775e70f0877661" dependencies = [ "data-encoding", "ed25519-dalek", - "grin_util 5.2.0-beta.2 (git+https://github.com/mimblewimble/grin?branch=master)", + "grin_util", "rand 0.6.5", "serde", "serde_derive", @@ -2905,7 +2776,7 @@ checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" dependencies = [ "bytes 1.4.0", "fnv", - "itoa 1.0.8", + "itoa 1.0.9", ] [[package]] @@ -3077,10 +2948,10 @@ dependencies = [ "i18n-embed", "lazy_static", "proc-macro-error", - "proc-macro2 1.0.64", - "quote 1.0.29", + "proc-macro2 1.0.66", + "quote 1.0.32", "strsim 0.10.0", - "syn 2.0.25", + "syn 2.0.27", "unic-langid", ] @@ -3092,8 +2963,8 @@ checksum = "e9a95d065e6be4499e50159172395559a388d20cf13c84c77e4a1e341786f219" dependencies = [ "find-crate", "i18n-config", - "proc-macro2 1.0.64", - "quote 1.0.29", + "proc-macro2 1.0.66", + "quote 1.0.32", "syn 1.0.109", ] @@ -3157,7 +3028,7 @@ dependencies = [ "byteorder", "color_quant", "num-rational 0.4.1", - "num-traits 0.2.15", + "num-traits 0.2.16", "png", ] @@ -3275,9 +3146,9 @@ checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" [[package]] name = "itoa" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b02a5381cc465bd3041d84623d0fa3b66738b52b8e2fc3bab8ad63ab032f4a" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "jni" @@ -3430,9 +3301,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56ee889ecc9568871456d42f603d6a0ce59ff328d291063a45cbdf0036baf6db" +checksum = "24e6ab01971eb092ffe6a7d42f49f9ff42662f17604681e2843ad65077ba47dc" dependencies = [ "cc", "libc", @@ -3732,7 +3603,7 @@ dependencies = [ "hexf-parse", "indexmap 1.9.3", "log", - "num-traits 0.2.15", + "num-traits 0.2.16", "rustc-hash", "spirv", "termcolor", @@ -3753,8 +3624,8 @@ dependencies = [ "openssl-probe", "openssl-sys", "schannel", - "security-framework 2.9.1", - "security-framework-sys 2.9.0", + "security-framework 2.9.2", + "security-framework-sys 2.9.1", "tempfile", ] @@ -3889,12 +3760,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8536030f9fea7127f841b45bb6243b27255787fb4eb83958aa1ef9d2fdc0c36" dependencies = [ - "num-bigint", + "num-bigint 0.2.6", "num-complex", "num-integer", "num-iter", "num-rational 0.2.4", - "num-traits 0.2.15", + "num-traits 0.2.16", ] [[package]] @@ -3905,7 +3776,18 @@ checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304" dependencies = [ "autocfg 1.1.0", "num-integer", - "num-traits 0.2.15", + "num-traits 0.2.16", +] + +[[package]] +name = "num-bigint" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +dependencies = [ + "autocfg 1.1.0", + "num-integer", + "num-traits 0.2.16", ] [[package]] @@ -3915,7 +3797,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6b19411a9719e753aff12e5187b74d60d3dc449ec3f4dc21e3989c3f554bc95" dependencies = [ "autocfg 1.1.0", - "num-traits 0.2.15", + "num-traits 0.2.16", ] [[package]] @@ -3925,7 +3807,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" dependencies = [ "autocfg 1.1.0", - "num-traits 0.2.15", + "num-traits 0.2.16", ] [[package]] @@ -3936,7 +3818,7 @@ checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" dependencies = [ "autocfg 1.1.0", "num-integer", - "num-traits 0.2.15", + "num-traits 0.2.16", ] [[package]] @@ -3946,9 +3828,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c000134b5dbf44adc5cb772486d335293351644b801551abe8f75c84cfa4aef" dependencies = [ "autocfg 1.1.0", - "num-bigint", + "num-bigint 0.2.6", "num-integer", - "num-traits 0.2.15", + "num-traits 0.2.16", ] [[package]] @@ -3959,7 +3841,7 @@ checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" dependencies = [ "autocfg 1.1.0", "num-integer", - "num-traits 0.2.15", + "num-traits 0.2.16", ] [[package]] @@ -3968,14 +3850,14 @@ version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31" dependencies = [ - "num-traits 0.2.15", + "num-traits 0.2.16", ] [[package]] name = "num-traits" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" dependencies = [ "autocfg 1.1.0", ] @@ -4015,8 +3897,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" dependencies = [ "proc-macro-crate", - "proc-macro2 1.0.64", - "quote 1.0.29", + "proc-macro2 1.0.66", + "quote 1.0.32", "syn 1.0.109", ] @@ -4027,9 +3909,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96667db765a921f7b295ffee8b60472b686a51d4f21c2ee4ffdb94c7013b65a6" dependencies = [ "proc-macro-crate", - "proc-macro2 1.0.64", - "quote 1.0.29", - "syn 2.0.25", + "proc-macro2 1.0.66", + "quote 1.0.32", + "syn 2.0.27", ] [[package]] @@ -4151,9 +4033,9 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ - "proc-macro2 1.0.64", - "quote 1.0.29", - "syn 2.0.25", + "proc-macro2 1.0.66", + "quote 1.0.32", + "syn 2.0.27", ] [[package]] @@ -4205,7 +4087,7 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3305af35278dd29f46fcdd139e0b1fbfae2153f0e5928b39b035542dd31e37b7" dependencies = [ - "num-traits 0.2.15", + "num-traits 0.2.16", ] [[package]] @@ -4293,9 +4175,9 @@ dependencies = [ [[package]] name = "paste" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4b27ab7be369122c218afc2079489cdcb4b517c0a3fc386ff11e1fedfcc2b35" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" [[package]] name = "pbkdf2" @@ -4346,9 +4228,9 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c" dependencies = [ - "proc-macro2 1.0.64", - "quote 1.0.29", - "syn 2.0.25", + "proc-macro2 1.0.66", + "quote 1.0.32", + "syn 2.0.27", ] [[package]] @@ -4377,9 +4259,9 @@ checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" [[package]] name = "pnet" -version = "0.33.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd959a8268165518e2bf5546ba84c7b3222744435616381df3c456fe8d983576" +checksum = "130c5b738eeda2dc5796fe2671e49027e6935e817ab51b930a36ec9e6a206a64" dependencies = [ "ipnetwork", "pnet_base", @@ -4391,18 +4273,18 @@ dependencies = [ [[package]] name = "pnet_base" -version = "0.33.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "872e46346144ebf35219ccaa64b1dffacd9c6f188cd7d012bd6977a2a838f42e" +checksum = "fe4cf6fb3ab38b68d01ab2aea03ed3d1132b4868fa4e06285f29f16da01c5f4c" dependencies = [ "no-std-net", ] [[package]] name = "pnet_datalink" -version = "0.33.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c302da22118d2793c312a35fb3da6846cb0fab6c3ad53fd67e37809b06cdafce" +checksum = "ad5854abf0067ebbd3967f7d45ebc8976ff577ff0c7bd101c4973ae3c70f98fe" dependencies = [ "ipnetwork", "libc", @@ -4413,30 +4295,30 @@ dependencies = [ [[package]] name = "pnet_macros" -version = "0.33.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a780e80005c2e463ec25a6e9f928630049a10b43945fea83207207d4a7606f4" +checksum = "688b17499eee04a0408aca0aa5cba5fc86401d7216de8a63fdf7a4c227871804" dependencies = [ - "proc-macro2 1.0.64", - "quote 1.0.29", + "proc-macro2 1.0.66", + "quote 1.0.32", "regex", - "syn 1.0.109", + "syn 2.0.27", ] [[package]] name = "pnet_macros_support" -version = "0.33.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d932134f32efd7834eb8b16d42418dac87086347d1bc7d142370ef078582bc" +checksum = "eea925b72f4bd37f8eab0f221bbe4c78b63498350c983ffa9dd4bcde7e030f56" dependencies = [ "pnet_base", ] [[package]] name = "pnet_packet" -version = "0.33.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bde678bbd85cb1c2d99dc9fc596e57f03aa725f84f3168b0eaf33eeccb41706" +checksum = "a9a005825396b7fe7a38a8e288dbc342d5034dac80c15212436424fef8ea90ba" dependencies = [ "glob", "pnet_base", @@ -4446,9 +4328,9 @@ dependencies = [ [[package]] name = "pnet_sys" -version = "0.33.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faf7a58b2803d818a374be9278a1fe8f88fce14b936afbe225000cfcd9c73f16" +checksum = "417c0becd1b573f6d544f73671070b039051e5ad819cc64aa96377b536128d00" dependencies = [ "libc", "winapi 0.3.9", @@ -4456,9 +4338,9 @@ dependencies = [ [[package]] name = "pnet_transport" -version = "0.33.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "813d1c0e4defbe7ee22f6fe1755f122b77bfb5abe77145b1b5baaf463cab9249" +checksum = "2637e14d7de974ee2f74393afccbc8704f3e54e6eb31488715e72481d1662cc3" dependencies = [ "libc", "pnet_base", @@ -4535,8 +4417,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" dependencies = [ "proc-macro-error-attr", - "proc-macro2 1.0.64", - "quote 1.0.29", + "proc-macro2 1.0.66", + "quote 1.0.32", "syn 1.0.109", "version_check", ] @@ -4547,8 +4429,8 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ - "proc-macro2 1.0.64", - "quote 1.0.29", + "proc-macro2 1.0.66", + "quote 1.0.32", "version_check", ] @@ -4563,9 +4445,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.64" +version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78803b62cbf1f46fde80d7c0e803111524b9877184cfe7c3033659490ac7a7da" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" dependencies = [ "unicode-ident", ] @@ -4593,11 +4475,11 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.29" +version = "1.0.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105" +checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" dependencies = [ - "proc-macro2 1.0.64", + "proc-macro2 1.0.66", ] [[package]] @@ -4986,10 +4868,10 @@ version = "6.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49b94b81e5b2c284684141a2fb9e2a31be90638caf040bf9afbc5a0416afe1ac" dependencies = [ - "proc-macro2 1.0.64", - "quote 1.0.29", + "proc-macro2 1.0.66", + "quote 1.0.32", "rust-embed-utils", - "syn 2.0.25", + "syn 2.0.27", "walkdir", ] @@ -5005,23 +4887,23 @@ dependencies = [ [[package]] name = "rust-i18n" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a516a7ceb61ddcdad9cf723de82b86f6ed8c78ebe25255c5686a061bf7318a6" +checksum = "074e2507aedea43bdeb742cb55fc339a0704625050c9532a226c7ddbd1a05f62" dependencies = [ "anyhow", "clap", "globwalk", "itertools", "once_cell", - "quote 1.0.29", + "quote 1.0.32", "regex", "rust-i18n-extract", "rust-i18n-macro", "rust-i18n-support", "serde", "serde_derive", - "toml 0.5.11", + "toml 0.7.6", ] [[package]] @@ -5032,8 +4914,8 @@ checksum = "e89ac25fb50c8d0893ee6436056fb4a0cc6f6e1df99239d7c104421d007d445e" dependencies = [ "anyhow", "ignore", - "proc-macro2 1.0.64", - "quote 1.0.29", + "proc-macro2 1.0.66", + "quote 1.0.32", "regex", "rust-i18n-support", "serde", @@ -5050,8 +4932,8 @@ checksum = "e09ef5c1e310112eea3c19c4e18e3e62968b002eb535ff5b242ca1200742f996" dependencies = [ "glob", "once_cell", - "proc-macro2 1.0.64", - "quote 1.0.29", + "proc-macro2 1.0.66", + "quote 1.0.32", "rust-i18n-support", "serde", "serde_json", @@ -5067,7 +4949,7 @@ checksum = "14eb094cd0072c5f09f333eea36fcd8c64961f9eb61dbd09e82eff51c58e8414" dependencies = [ "globwalk", "once_cell", - "proc-macro2 1.0.64", + "proc-macro2 1.0.66", "serde", "serde_json", "serde_yaml", @@ -5159,9 +5041,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe232bdf6be8c8de797b22184ee71118d63780ea42ac85b61d1baa6d3b782ae9" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" [[package]] name = "safemem" @@ -5204,9 +5086,9 @@ checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" [[package]] name = "scopeguard" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "scrypt" @@ -5276,15 +5158,15 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.9.1" +version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc758eb7bffce5b308734e9b0c1468893cae9ff70ebf13e7090be8dcbcc83a8" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" dependencies = [ "bitflags 1.3.2", "core-foundation 0.9.3", "core-foundation-sys 0.8.4", "libc", - "security-framework-sys 2.9.0", + "security-framework-sys 2.9.1", ] [[package]] @@ -5299,9 +5181,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.9.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f51d0c0d83bec45f16480d0ce0058397a69e48fcdc52d1dc8855fb68acbd31a7" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" dependencies = [ "core-foundation-sys 0.8.4", "libc", @@ -5315,18 +5197,18 @@ checksum = "1ef965a420fe14fdac7dd018862966a4c14094f900e1650bbc71ddd7d580c8af" [[package]] name = "semver" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" +checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" dependencies = [ "serde", ] [[package]] name = "serde" -version = "1.0.171" +version = "1.0.175" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30e27d1e4fd7659406c492fd6cfaf2066ba8773de45ca75e855590f856dc34a9" +checksum = "5d25439cd7397d044e2748a6fe2432b5e85db703d6d097bd014b3c0ad1ebff0b" dependencies = [ "serde_derive", ] @@ -5343,35 +5225,35 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.171" +version = "1.0.175" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389894603bd18c46fa56231694f8d827779c0951a667087194cf9de94ed24682" +checksum = "b23f7ade6f110613c0d63858ddb8b94c1041f550eab58a16b371bdf2c9c80ab4" dependencies = [ - "proc-macro2 1.0.64", - "quote 1.0.29", - "syn 2.0.25", + "proc-macro2 1.0.66", + "quote 1.0.32", + "syn 2.0.27", ] [[package]] name = "serde_json" -version = "1.0.102" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5062a995d481b2308b6064e9af76011f2921c35f97b0468811ed9f6cd91dfed" +checksum = "d03b412469450d4404fe8499a268edd7f8b79fecb074b0d812ad64ca21f4031b" dependencies = [ - "itoa 1.0.8", + "itoa 1.0.9", "ryu", "serde", ] [[package]] name = "serde_repr" -version = "0.1.14" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d89a8107374290037607734c0b73a85db7ed80cae314b3c5791f192a496e731" +checksum = "e168eaaf71e8f9bd6037feb05190485708e019f4fd87d161b3c0a0d37daf85e5" dependencies = [ - "proc-macro2 1.0.64", - "quote 1.0.29", - "syn 2.0.25", + "proc-macro2 1.0.66", + "quote 1.0.32", + "syn 2.0.27", ] [[package]] @@ -5390,7 +5272,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", - "itoa 1.0.8", + "itoa 1.0.9", "ryu", "serde", ] @@ -5475,9 +5357,9 @@ checksum = "7fdf1b9db47230893d76faad238fd6097fd6d6a9245cd7a4d90dbd639536bbd2" [[package]] name = "signal-hook" -version = "0.3.15" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "732768f1176d21d09e076c23a93123d40bba92d50c4058da34d45c8de8e682b9" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" dependencies = [ "libc", "signal-hook-registry", @@ -5500,9 +5382,9 @@ checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" [[package]] name = "simd-adler32" -version = "0.3.5" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "238abfbb77c1915110ad968465608b68e869e0772622c9656714e73e5a1a522f" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" [[package]] name = "siphasher" @@ -5597,7 +5479,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "246bfa38fe3db3f1dfc8ca5a2cdeb7348c78be2112740cc0ec8ef18b6d94f830" dependencies = [ "bitflags 1.3.2", - "num-traits 0.2.15", + "num-traits 0.2.16", ] [[package]] @@ -5643,8 +5525,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87c85aa3f8ea653bfd3ddf25f7ee357ee4d204731f6aa9ad04002306f6e2774c" dependencies = [ "heck", - "proc-macro2 1.0.64", - "quote 1.0.29", + "proc-macro2 1.0.66", + "quote 1.0.32", "syn 1.0.109", ] @@ -5677,19 +5559,19 @@ version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ - "proc-macro2 1.0.64", - "quote 1.0.29", + "proc-macro2 1.0.66", + "quote 1.0.32", "unicode-ident", ] [[package]] name = "syn" -version = "2.0.25" +version = "2.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15e3fc8c0c74267e2df136e5e5fb656a464158aa57624053375eb9c8c6e25ae2" +checksum = "b60f673f44a8255b9c8c657daf66a596d435f2da81a555b06dc644d080ba45e0" dependencies = [ - "proc-macro2 1.0.64", - "quote 1.0.29", + "proc-macro2 1.0.66", + "quote 1.0.32", "unicode-ident", ] @@ -5720,15 +5602,14 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.6.0" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6" +checksum = "5486094ee78b2e5038a6382ed7645bc084dc2ec433426ca4c3cb61e2007b8998" dependencies = [ - "autocfg 1.1.0", "cfg-if 1.0.0", - "fastrand", + "fastrand 2.0.0", "redox_syscall 0.3.5", - "rustix 0.37.23", + "rustix 0.38.4", "windows-sys 0.48.0", ] @@ -5752,22 +5633,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.43" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a35fc5b8971143ca348fa6df4f024d4d55264f3468c71ad1c2f365b0a4d58c42" +checksum = "611040a08a0439f8248d1990b111c95baa9c704c805fa1f62104b39655fd7f90" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.43" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "463fe12d7993d3b327787537ce8dd4dfa058de32fc2b195ef3cde03dc4771e8f" +checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96" dependencies = [ - "proc-macro2 1.0.64", - "quote 1.0.29", - "syn 2.0.25", + "proc-macro2 1.0.66", + "quote 1.0.32", + "syn 2.0.27", ] [[package]] @@ -5920,8 +5801,8 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e44da00bfc73a25f814cd8d7e57a68a5c31b74b3152a0a1d1f590c97ed06265a" dependencies = [ - "proc-macro2 1.0.64", - "quote 1.0.29", + "proc-macro2 1.0.66", + "quote 1.0.32", "syn 1.0.109", ] @@ -5931,9 +5812,9 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ - "proc-macro2 1.0.64", - "quote 1.0.29", - "syn 2.0.25", + "proc-macro2 1.0.66", + "quote 1.0.32", + "syn 2.0.27", ] [[package]] @@ -6057,9 +5938,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.19.12" +version = "0.19.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c500344a19072298cd05a7224b3c0c629348b78692bf48466c5238656e315a78" +checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a" dependencies = [ "indexmap 2.0.0", "serde", @@ -6093,9 +5974,9 @@ version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ - "proc-macro2 1.0.64", - "quote 1.0.29", - "syn 2.0.25", + "proc-macro2 1.0.66", + "quote 1.0.32", + "syn 2.0.27", ] [[package]] @@ -6214,9 +6095,9 @@ checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22049a19f4a68748a168c0fc439f9516686aa045927ff767eca0a85101fb6e73" +checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" [[package]] name = "unicode-normalization" @@ -6379,9 +6260,9 @@ dependencies = [ "bumpalo", "log", "once_cell", - "proc-macro2 1.0.64", - "quote 1.0.29", - "syn 2.0.25", + "proc-macro2 1.0.66", + "quote 1.0.32", + "syn 2.0.27", "wasm-bindgen-shared", ] @@ -6403,7 +6284,7 @@ version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" dependencies = [ - "quote 1.0.29", + "quote 1.0.32", "wasm-bindgen-macro-support", ] @@ -6413,9 +6294,9 @@ version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ - "proc-macro2 1.0.64", - "quote 1.0.29", - "syn 2.0.25", + "proc-macro2 1.0.66", + "quote 1.0.32", + "syn 2.0.27", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -6483,8 +6364,8 @@ version = "0.29.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f4303d8fa22ab852f789e75a967f0a2cdc430a607751c0499bada3e451cbd53" dependencies = [ - "proc-macro2 1.0.64", - "quote 1.0.29", + "proc-macro2 1.0.66", + "quote 1.0.32", "xml-rs", ] @@ -6559,9 +6440,9 @@ dependencies = [ [[package]] name = "wgpu" -version = "0.16.2" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8aa4361a426ff9f028520da01e8fda28ab9bdb029e2a76901f1f88317e2796e9" +checksum = "480c965c9306872eb6255fa55e4b4953be55a8b64d57e61d7ff840d3dcc051cd" dependencies = [ "arrayvec 0.7.4", "cfg-if 1.0.0", @@ -6750,8 +6631,8 @@ version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce87ca8e3417b02dc2a8a22769306658670ec92d78f1bd420d6310a67c245c6" dependencies = [ - "proc-macro2 1.0.64", - "quote 1.0.29", + "proc-macro2 1.0.66", + "quote 1.0.32", "syn 1.0.109", ] @@ -6761,8 +6642,8 @@ version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "853f69a591ecd4f810d29f17e902d40e349fb05b0b11fff63b08b826bfe39c7f" dependencies = [ - "proc-macro2 1.0.64", - "quote 1.0.29", + "proc-macro2 1.0.66", + "quote 1.0.32", "syn 1.0.109", ] @@ -6935,9 +6816,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.4.9" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81a2094c43cc94775293eaa0e499fbc30048a6d824ac82c0351a8c0bf9112529" +checksum = "25b5872fa2e10bd067ae946f927e726d7d603eaeb6e02fa6a350e0722d2b8c11" dependencies = [ "memchr", ] @@ -7037,9 +6918,9 @@ dependencies = [ [[package]] name = "xml-rs" -version = "0.8.15" +version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a56c84a8ccd4258aed21c92f70c0f6dea75356b6892ae27c24139da456f9336" +checksum = "47430998a7b5d499ccee752b41567bc3afc57e1327dc855b1a2aa44ce29b5fa1" [[package]] name = "yaml-rust" @@ -7098,8 +6979,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41d1794a946878c0e807f55a397187c11fc7a038ba5d868e7db4f3bd7760bc9d" dependencies = [ "proc-macro-crate", - "proc-macro2 1.0.64", - "quote 1.0.29", + "proc-macro2 1.0.66", + "quote 1.0.32", "regex", "syn 1.0.109", "zvariant_utils", @@ -7131,9 +7012,9 @@ version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ - "proc-macro2 1.0.64", - "quote 1.0.29", - "syn 2.0.25", + "proc-macro2 1.0.66", + "quote 1.0.32", + "syn 2.0.27", ] [[package]] @@ -7168,8 +7049,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "934d7a7dfc310d6ee06c87ffe88ef4eca7d3e37bb251dece2ef93da8f17d8ecd" dependencies = [ "proc-macro-crate", - "proc-macro2 1.0.64", - "quote 1.0.29", + "proc-macro2 1.0.66", + "quote 1.0.32", "syn 1.0.109", "zvariant_utils", ] @@ -7180,7 +7061,7 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7234f0d811589db492d16893e3f21e8e2fd282e6d01b0cddee310322062cc200" dependencies = [ - "proc-macro2 1.0.64", - "quote 1.0.29", + "proc-macro2 1.0.66", + "quote 1.0.32", "syn 1.0.109", ] diff --git a/Cargo.toml b/Cargo.toml index f927c1c..bbf1e06 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,42 +16,47 @@ log = "0.4" ## node openssl-sys = { version = "0.9.82", features = ["vendored"] } -grin_api = { path = "../grin/node/api" } -grin_chain = { path = "../grin/node/chain" } -grin_config = { path = "../grin/node/config" } -grin_core = { path = "../grin/node/core" } -grin_keychain = { path = "../grin/node/keychain" } -grin_p2p = { path = "../grin/node/p2p" } -grin_servers = { path = "../grin/node/servers" } -grin_util = { path = "../grin/node/util" } +grin_api = { git = "https://github.com/mimblewimble/grin", branch = "master" } +grin_chain = { git = "https://github.com/mimblewimble/grin", branch = "master" } +grin_config = { git = "https://github.com/mimblewimble/grin", branch = "master" } +grin_core = { git = "https://github.com/mimblewimble/grin", branch = "master" } +grin_p2p = { git = "https://github.com/mimblewimble/grin", branch = "master" } +grin_servers = { 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" } ## wallet -grin_wallet_impls = { path = "../grin/wallet/impls" } -#grin_wallet_api = "5.1.0" -#grin_wallet_libwallet = "5.1.0" +grin_wallet_impls = { git = "https://github.com/mimblewimble/grin-wallet", branch = "master" } +grin_wallet_api = { git = "https://github.com/mimblewimble/grin-wallet", branch = "master" } +grin_wallet_libwallet = { git = "https://github.com/mimblewimble/grin-wallet", branch = "master" } +grin_wallet_util = { git = "https://github.com/mimblewimble/grin-wallet", branch = "master" } #grin_wallet_controller = "5.1.0" #grin_wallet_config = "5.1.0" -#grin_wallet_util = "5.1.0" ## ui pollster = "0.3.0" wgpu = "0.16.1" egui = { version = "0.22.0", default-features = false } egui_extras = { version = "0.22.0", features = ["image"] } +rust-i18n = "2.1.0" ## other futures = "0.3" dirs = "5.0.1" once_cell = "1.10.0" -rust-i18n = "2.0.0" sys-locale = "0.3.0" chrono = "0.4.23" lazy_static = "1.4.0" toml = "0.7.4" serde = "1" -pnet = "0.33.0" +pnet = "0.34.0" zeroize = "1.6.0" url = "2.4.0" +parking_lot = "0.10.2" +uuid = { version = "0.8.2", features = ["serde", "v4"] } +num-bigint = "0.4.3" +byteorder = "1.3" +ed25519-dalek = "1.0.0-pre.4" # stratum server serde_derive = "1" diff --git a/locales/en.yml b/locales/en.yml index 101459c..62ef71d 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -16,6 +16,7 @@ wallets: words_count: 'Words count:' enter_word: 'Enter word #%{number}:' not_valid_word: Entered word is not valid + not_valid_phrase: Entered phrase is not valid create_phrase_desc: Safely write down and save your recovery phrase. restore_phrase_desc: Enter words from your saved recovery phrase. setup_conn_desc: Choose how your wallet connects to the network. diff --git a/locales/ru.yml b/locales/ru.yml index 0aedb39..62e62c0 100644 --- a/locales/ru.yml +++ b/locales/ru.yml @@ -16,6 +16,7 @@ wallets: words_count: 'Количество слов:' enter_word: 'Введите слово #%{number}:' not_valid_word: Введено недопустимое слово + not_valid_phrase: Введена недопустимая фраза восстановления create_phrase_desc: Безопасно запишите и сохраните вашу фразу восстановления. restore_phrase_desc: Введите слова из вашей сохранённой фразы восстановления. setup_conn_desc: Выберите способ подключения вашего кошелька к сети. diff --git a/src/gui/views/views.rs b/src/gui/views/views.rs index 645f273..be2cfd3 100644 --- a/src/gui/views/views.rs +++ b/src/gui/views/views.rs @@ -168,7 +168,7 @@ impl View { let size = egui::Vec2::splat(2.0 * r + 5.0); let (rect, br) = ui.allocate_at_least(size, Sense::click_and_drag()); - let mut icon_size = 22.0; + let mut icon_size = 24.0; let mut icon_color = Colors::TEXT_BUTTON; // Increase radius and change icon size and color on-hover. diff --git a/src/gui/views/wallets/creation/creation.rs b/src/gui/views/wallets/creation/creation.rs index 5f32558..9e6485d 100644 --- a/src/gui/views/wallets/creation/creation.rs +++ b/src/gui/views/wallets/creation/creation.rs @@ -23,6 +23,7 @@ use crate::gui::views::{Modal, ModalPosition, View}; use crate::gui::views::wallets::creation::MnemonicSetup; use crate::gui::views::wallets::creation::types::{PhraseMode, Step}; use crate::gui::views::wallets::setup::ConnectionSetup; +use crate::wallet::WalletList; /// Wallet creation content. pub struct WalletCreation { @@ -70,11 +71,12 @@ impl WalletCreation { pub const NAME_PASS_MODAL: &'static str = "name_pass_modal"; pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { - // Show wallet creation step description and confirmation bottom panel. + // Show wallet creation step description and confirmation panel. if self.step.is_some() { egui::TopBottomPanel::bottom("wallet_creation_step_panel") .frame(egui::Frame { stroke: View::DEFAULT_STROKE, + fill: Colors::FILL_DARK, inner_margin: Margin { left: View::far_left_inset_margin(ui) + 4.0, right: View::get_right_inset() + 4.0, @@ -85,65 +87,15 @@ impl WalletCreation { }) .show_inside(ui, |ui| { ui.vertical_centered(|ui| { - if let Some(step) = &self.step { - // Setup step description text and availability. - let (step_text, step_available) = match step { - Step::EnterMnemonic => { - let mode = &self.mnemonic_setup.mnemonic.mode; - let text = if mode == &PhraseMode::Generate { - t!("wallets.create_phrase_desc") - } else { - t!("wallets.restore_phrase_desc") - }; - let available = !self - .mnemonic_setup - .mnemonic - .words - .contains(&String::from("")); - (text, available) - } - Step::ConfirmMnemonic => { - let text = t!("wallets.restore_phrase_desc"); - let available = !self - .mnemonic_setup - .mnemonic - .confirm_words - .contains(&String::from("")); - (text, available) - }, - Step::SetupConnection => (t!("wallets.setup_conn_desc"), true) - }; - // Show step description. - ui.label(RichText::new(step_text).size(16.0).color(Colors::GRAY)); - - // Show next step button if there are no empty words. - - if step_available { - // Setup button text. - let (next_text, color) = if step == &Step::SetupConnection { - (format!("{} {}", CHECK, t!("complete")), Colors::GOLD) - } else { - let text = format!("{} {}", SHARE_FAT, t!("continue")); - (text, Colors::WHITE) - }; - - ui.add_space(4.0); - // Show button. - View::button(ui, next_text.to_uppercase(), color, || { - self.forward(); - }); - ui.add_space(4.0); - } - } + self.step_control_ui(ui); }); }); } - // Show wallet creation step content. + // Show wallet creation step content panel. egui::CentralPanel::default() .frame(egui::Frame { stroke: View::DEFAULT_STROKE, - fill: if self.step.is_none() { Colors::FILL_DARK } else { Colors::WHITE }, inner_margin: Margin { left: View::far_left_inset_margin(ui) + 4.0, right: View::get_right_inset() + 4.0, @@ -153,12 +105,74 @@ impl WalletCreation { ..Default::default() }) .show_inside(ui, |ui| { - self.step_ui(ui, cb); + self.step_content_ui(ui, cb); }); } + /// Draw [`Step`] description and confirmation control. + fn step_control_ui(&mut self, ui: &mut egui::Ui) { + if let Some(step) = &self.step { + // Setup step description text and availability. + let (step_text, mut step_available) = match step { + Step::EnterMnemonic => { + let mode = &self.mnemonic_setup.mnemonic.mode; + let text = if mode == &PhraseMode::Generate { + t!("wallets.create_phrase_desc") + } else { + t!("wallets.restore_phrase_desc") + }; + let available = !self + .mnemonic_setup + .mnemonic + .words + .contains(&String::from("")); + (text, available) + } + Step::ConfirmMnemonic => { + let text = t!("wallets.restore_phrase_desc"); + let available = !self + .mnemonic_setup + .mnemonic + .confirm_words + .contains(&String::from("")); + (text, available) + }, + Step::SetupConnection => (t!("wallets.setup_conn_desc"), true) + }; + // Show step description. + ui.label(RichText::new(step_text).size(16.0).color(Colors::GRAY)); + + // Show error if entered phrase is not valid. + if !self.mnemonic_setup.valid_phrase { + step_available = false; + ui.label(RichText::new(t!("wallets.not_valid_phrase")) + .size(16.0) + .color(Colors::RED)); + ui.add_space(2.0); + } + + // Show next step button if there are no empty words. + if step_available { + // Setup button text. + let (next_text, color) = if step == &Step::SetupConnection { + (format!("{} {}", CHECK, t!("complete")), Colors::GOLD) + } else { + let text = format!("{} {}", SHARE_FAT, t!("continue")); + (text, Colors::WHITE) + }; + + ui.add_space(4.0); + // Show button. + View::button(ui, next_text.to_uppercase(), color, || { + self.forward(); + }); + ui.add_space(4.0); + } + } + } + /// Draw wallet creation [`Step`] content. - fn step_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { + fn step_content_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { match &self.step { None => { // Show wallet creation message if step is empty. @@ -233,12 +247,23 @@ impl WalletCreation { if self.mnemonic_setup.mnemonic.mode == PhraseMode::Generate { Some(Step::ConfirmMnemonic) } else { - Some(Step::SetupConnection) + // Check if entered phrase was valid. + if self.mnemonic_setup.valid_phrase { + Some(Step::SetupConnection) + } else { + Some(Step::EnterMnemonic) + } } } Step::ConfirmMnemonic => Some(Step::SetupConnection), Step::SetupConnection => { - //TODO: Confirm mnemonic + // Create wallet at last step. + WalletList::create_wallet( + self.name_edit.clone(), + self.pass_edit.clone(), + self.mnemonic_setup.mnemonic.get_phrase(), + self.network_setup.get_ext_conn_url() + ).unwrap(); None } } @@ -249,7 +274,7 @@ impl WalletCreation { /// Start wallet creation from showing [`Modal`] to enter name and password. pub fn show_name_pass_modal(&mut self) { // Reset modal values. - self.hide_pass = false; + self.hide_pass = true; self.modal_just_opened = true; self.name_edit = String::from(""); self.pass_edit = String::from(""); diff --git a/src/gui/views/wallets/creation/mnemonic.rs b/src/gui/views/wallets/creation/mnemonic.rs index 82b4ea0..d1c7629 100644 --- a/src/gui/views/wallets/creation/mnemonic.rs +++ b/src/gui/views/wallets/creation/mnemonic.rs @@ -25,6 +25,9 @@ pub struct MnemonicSetup { /// Current mnemonic phrase. pub(crate) mnemonic: Mnemonic, + /// Flag to check if entered phrase was valid. + pub(crate) valid_phrase: bool, + /// Current word number to edit at [`Modal`]. word_num_edit: usize, /// Entered word value for [`Modal`]. @@ -37,6 +40,7 @@ impl Default for MnemonicSetup { fn default() -> Self { Self { mnemonic: Mnemonic::default(), + valid_phrase: true, word_num_edit: 0, word_edit: String::from(""), valid_word_edit: true @@ -290,6 +294,10 @@ impl MnemonicSetup { let close_modal = words.len() == self.word_num_edit || !words.get(self.word_num_edit).unwrap().is_empty(); if close_modal { + // Check if entered phrase was valid when all words were entered. + if !self.mnemonic.words.contains(&String::from("")) { + self.valid_phrase = self.mnemonic.is_valid_phrase(); + } cb.hide_keyboard(); modal.close(); } else { diff --git a/src/gui/views/wallets/creation/types.rs b/src/gui/views/wallets/creation/types.rs index 212ef0d..0551516 100644 --- a/src/gui/views/wallets/creation/types.rs +++ b/src/gui/views/wallets/creation/types.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use grin_keychain::mnemonic::{from_entropy, search}; +use grin_keychain::mnemonic::{from_entropy, search, to_entropy}; use rand::{Rng, thread_rng}; use zeroize::{Zeroize, ZeroizeOnDrop}; @@ -124,6 +124,16 @@ impl Mnemonic { valid && equal } + /// Check if current phrase is valid. + pub fn is_valid_phrase(&self) -> bool { + to_entropy(self.get_phrase().as_str()).is_ok() + } + + /// Get phrase from words. + pub fn get_phrase(&self) -> String { + self.words.iter().map(|x| x.to_string() + " ").collect::() + } + /// Generate list of words based on provided [`PhraseMode`] and [`PhraseSize`]. fn generate_words(mode: &PhraseMode, size: &PhraseSize) -> Vec { match mode { diff --git a/src/gui/views/wallets/setup/connection.rs b/src/gui/views/wallets/setup/connection.rs index 4d339be..5e0124e 100644 --- a/src/gui/views/wallets/setup/connection.rs +++ b/src/gui/views/wallets/setup/connection.rs @@ -52,6 +52,14 @@ impl ConnectionSetup { // Self { method: ConnectionMethod::Integrated } // } + /// Get external node connection URL. + pub fn get_ext_conn_url(&self) -> Option { + match &self.method { + ConnectionMethod::Integrated => None, + ConnectionMethod::External(url) => Some(url.clone()) + } + } + pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { ScrollArea::vertical() .id_source("wallet_connection_setup") @@ -99,11 +107,6 @@ impl ConnectionSetup { }); } - /// Draw external connections setup. - fn external_conn_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { - - } - /// Draw modal content. pub fn modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) { ui.add_space(6.0); diff --git a/src/gui/views/wallets/wallets.rs b/src/gui/views/wallets/wallets.rs index 724016d..a15598e 100644 --- a/src/gui/views/wallets/wallets.rs +++ b/src/gui/views/wallets/wallets.rs @@ -23,13 +23,10 @@ use crate::gui::views::{Modal, ModalContainer, Root, TitlePanel, TitleType, View use crate::gui::views::wallets::creation::{MnemonicSetup, WalletCreation}; use crate::gui::views::wallets::setup::ConnectionSetup; use crate::gui::views::wallets::wallet::WalletContent; -use crate::wallet::{Wallet, WalletList}; +use crate::wallet::WalletList; /// Wallets content. pub struct WalletsContent { - /// List of wallets. - list: Vec, - /// Selected list item content. item_content: Option, /// Wallet creation content. @@ -42,7 +39,6 @@ pub struct WalletsContent { impl Default for WalletsContent { fn default() -> Self { Self { - list: WalletList::list(), item_content: None, creation_content: WalletCreation::default(), modal_ids: vec![ @@ -83,14 +79,22 @@ impl WalletsContent { // Show title panel. self.title_ui(ui, frame); - let is_wallet_panel_open = Self::is_dual_panel_mode(ui, frame) || self.list.is_empty(); + let wallets = WalletList::list(); + + let is_dual_panel = Self::is_dual_panel_mode(ui, frame); + let is_wallet_creation = self.creation_content.can_go_back(); + let is_wallet_panel_open = is_dual_panel || is_wallet_creation || wallets.is_empty(); let wallet_panel_width = self.wallet_panel_width(ui, frame); // Show wallet content. egui::SidePanel::right("wallet_panel") .resizable(false) .min_width(wallet_panel_width) .frame(egui::Frame { - fill: if self.list.is_empty() { Colors::FILL_DARK } else { Colors::WHITE }, + fill: if !wallets.is_empty() || is_wallet_creation { + Colors::WHITE + } else { + Colors::FILL_DARK + }, ..Default::default() }) .show_animated_inside(ui, is_wallet_panel_open, |ui| { @@ -98,7 +102,7 @@ impl WalletsContent { }); // Show list of wallets. - if !self.list.is_empty() { + if !is_wallet_creation && !wallets.is_empty() { egui::CentralPanel::default() .frame(egui::Frame { stroke: View::DEFAULT_STROKE, @@ -113,6 +117,12 @@ impl WalletsContent { }) .show_inside(ui, |ui| { //TODO: wallets list + for w in WalletList::list() { + ui.label(w.config.get_name()); + View::button(ui, "get info".to_string(), Colors::GOLD, || { + println!("12345 amount {}", w.get_txs_info(10).unwrap().2.total); + }); + } }); // Show wallet creation button if wallet panel is not open. if !is_wallet_panel_open { @@ -154,7 +164,7 @@ impl WalletsContent { ui: &mut egui::Ui, frame: &mut eframe::Frame, cb: &dyn PlatformCallbacks) { - if self.list.is_empty() || self.item_content.is_none() { + if WalletList::list().is_empty() || self.item_content.is_none() { self.creation_content.ui(ui, cb) } else { self.item_content.as_mut().unwrap().ui(ui, frame, cb); @@ -163,18 +173,19 @@ impl WalletsContent { /// Get [`WalletContent`] panel width. fn wallet_panel_width(&self, ui: &mut egui::Ui, frame: &mut eframe::Frame) -> f32 { + let is_wallet_creation = self.creation_content.can_go_back(); + let available_width = if WalletList::list().is_empty() || is_wallet_creation { + ui.available_width() + } else { + ui.available_width() - Root::SIDE_PANEL_MIN_WIDTH + }; if Self::is_dual_panel_mode(ui, frame) { let min_width = (Root::SIDE_PANEL_MIN_WIDTH + View::get_right_inset()) as i64; - let available_width = if self.list.is_empty() { - ui.available_width() - } else { - ui.available_width() - Root::SIDE_PANEL_MIN_WIDTH - } as i64; - max(min_width, available_width) as f32 + max(min_width, available_width as i64) as f32 } else { let dual_panel_root = Root::is_dual_panel_mode(frame); if dual_panel_root { - ui.available_width() + available_width } else { frame.info().window_info.size.x } diff --git a/src/lib.rs b/src/lib.rs index 74d00e9..7d58829 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,17 +27,17 @@ use crate::node::Node; i18n!("locales"); +mod node; +mod wallet; +mod settings; + +pub mod gui; + // Include build information. pub mod built_info { include!(concat!(env!("OUT_DIR"), "/built.rs")); } -mod node; -mod wallet; -pub mod gui; - -mod settings; - #[allow(dead_code)] #[cfg(target_os = "android")] #[no_mangle] @@ -100,7 +100,7 @@ pub fn start(mut options: eframe::NativeOptions, app_creator: eframe::AppCreator Node::start(); } // Launch graphical interface. - let _ = eframe::run_native("Grim", options, app_creator); + eframe::run_native("Grim", options, app_creator).unwrap(); } /// Setup application [`egui::Style`] and [`egui::Visuals`]. diff --git a/src/wallet/config.rs b/src/wallet/config.rs index aa17d79..db1a580 100644 --- a/src/wallet/config.rs +++ b/src/wallet/config.rs @@ -12,8 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::ffi::OsString; +use std::fs; use std::path::PathBuf; +use grin_core::global::ChainTypes; use serde_derive::{Deserialize, Serialize}; use crate::{AppConfig, Settings}; @@ -22,8 +23,10 @@ use crate::wallet::WalletList; /// Wallet configuration. #[derive(Serialize, Deserialize, Clone)] pub struct WalletConfig { + /// Chain type for current wallet. + chain_type: ChainTypes, /// Identifier for a wallet. - id: OsString, + id: i64, /// Readable wallet name. name: String, /// External node connection URL. @@ -35,13 +38,12 @@ const CONFIG_FILE_NAME: &'static str = "grim-wallet.toml"; impl WalletConfig { /// Create wallet config. - pub fn create(id: OsString, name: String) -> WalletConfig { - let config_path = Self::get_config_path(&id); - let config = WalletConfig { - id, - name, - external_node_url: None, - }; + pub fn create(name: String, external_node_url: Option) -> WalletConfig { + let id = chrono::Utc::now().timestamp(); + let chain_type = AppConfig::chain_type(); + let config_path = Self::get_config_file_path(&chain_type, id); + + let config = WalletConfig { chain_type, id, name, external_node_url }; Settings::write_to_file(&config, config_path); config } @@ -56,18 +58,29 @@ impl WalletConfig { None } - /// Get config file path for provided wallet identifier. - fn get_config_path(id: &OsString) -> PathBuf { - let chain_type = AppConfig::chain_type(); - let mut config_path = WalletList::get_wallets_base_dir(&chain_type); - config_path.push(id); + /// Get config file path for provided [`ChainTypes`] and wallet identifier. + fn get_config_file_path(chain_type: &ChainTypes, id: i64) -> PathBuf { + let mut config_path = WalletList::get_base_path(chain_type); + config_path.push(id.to_string()); + // Create if the config path doesn't exist. + if !config_path.exists() { + let _ = fs::create_dir_all(config_path.clone()); + } config_path.push(CONFIG_FILE_NAME); config_path } + /// Get current wallet data path. + pub fn get_data_path(&self) -> String { + let chain_type = AppConfig::chain_type(); + let mut config_path = WalletList::get_base_path(&chain_type); + config_path.push(self.id.to_string()); + config_path.to_str().unwrap().to_string() + } + /// Save wallet config. fn save(&self) { - let config_path = Self::get_config_path(&self.id); + let config_path = Self::get_config_file_path(&self.chain_type, self.id); Settings::write_to_file(self, config_path); } diff --git a/src/wallet/keys.rs b/src/wallet/keys.rs new file mode 100644 index 0000000..9bb0a46 --- /dev/null +++ b/src/wallet/keys.rs @@ -0,0 +1,128 @@ +// Copyright 2023 The Grim 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 grin_keychain::{ChildNumber, ExtKeychain, Identifier, Keychain}; +use grin_util::secp::key::SecretKey; +use grin_wallet_libwallet::{AcctPathMapping, NodeClient, WalletBackend}; +use grin_wallet_libwallet::Error; + +/// Get next available key in the wallet for a given parent +pub fn next_available_key<'a, T: ?Sized, C, K>( + wallet: &mut T, + keychain_mask: Option<&SecretKey>, +) -> Result + where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let child = wallet.next_child(keychain_mask)?; + Ok(child) +} + +/// Retrieve an existing key from a wallet +pub fn retrieve_existing_key<'a, T: ?Sized, C, K>( + wallet: &T, + key_id: Identifier, + mmr_index: Option, +) -> Result<(Identifier, u32), Error> + where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let existing = wallet.get(&key_id, &mmr_index)?; + let key_id = existing.key_id.clone(); + let derivation = existing.n_child; + Ok((key_id, derivation)) +} + +/// Returns a list of account to BIP32 path mappings +pub fn accounts<'a, T: ?Sized, C, K>(wallet: &mut T) -> Result, Error> + where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + Ok(wallet.acct_path_iter().collect()) +} + +/// Adds an new parent account path with a given label +pub fn new_acct_path<'a, T: ?Sized, C, K>( + wallet: &mut T, + keychain_mask: Option<&SecretKey>, + label: &str, +) -> Result + where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let label = label.to_owned(); + if wallet.acct_path_iter().any(|l| l.label == label) { + return Err(Error::AccountLabelAlreadyExists(label)); + } + + // We're always using paths at m/k/0 for parent keys for output derivations + // so find the highest of those, then increment (to conform with external/internal + // derivation chains in BIP32 spec) + + let highest_entry = wallet.acct_path_iter().max_by(|a, b| { + ::from(a.path.to_path().path[0]).cmp(&::from(b.path.to_path().path[0])) + }); + + let return_id = { + if let Some(e) = highest_entry { + let mut p = e.path.to_path(); + p.path[0] = ChildNumber::from(::from(p.path[0]) + 1); + p.to_identifier() + } else { + ExtKeychain::derive_key_id(2, 0, 0, 0, 0) + } + }; + + let save_path = AcctPathMapping { + label, + path: return_id.clone(), + }; + + let mut batch = wallet.batch(keychain_mask)?; + batch.save_acct_path(save_path)?; + batch.commit()?; + Ok(return_id) +} + +/// Adds/sets a particular account path with a given label +pub fn set_acct_path<'a, T: ?Sized, C, K>( + wallet: &mut T, + keychain_mask: Option<&SecretKey>, + label: &str, + path: &Identifier, +) -> Result<(), Error> + where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let label = label.to_owned(); + let save_path = AcctPathMapping { + label, + path: path.clone(), + }; + + let mut batch = wallet.batch(keychain_mask)?; + batch.save_acct_path(save_path)?; + batch.commit()?; + Ok(()) +} \ No newline at end of file diff --git a/src/wallet/list.rs b/src/wallet/list.rs index 5dc115a..9e8b40c 100644 --- a/src/wallet/list.rs +++ b/src/wallet/list.rs @@ -16,6 +16,7 @@ use std::fs; use std::path::PathBuf; use std::sync::{Arc, RwLock}; use grin_core::global::ChainTypes; +use grin_wallet_libwallet::Error; use lazy_static::lazy_static; @@ -24,43 +25,58 @@ use crate::wallet::Wallet; lazy_static! { /// Global wallets state. - static ref WALLETS_STATE: Arc> = Arc::new(RwLock::new(WalletList::load())); + static ref WALLETS_STATE: Arc> = Arc::new(RwLock::new(WalletList::init())); } -/// List of created wallets. +/// Wallets manager. pub struct WalletList { - list: Vec + pub(crate) list: Vec } /// Base wallets directory name. pub const BASE_DIR_NAME: &'static str = "wallets"; impl WalletList { - /// Load list of wallets. - fn load() -> Self { + /// Initialize manager by loading list of wallets into state. + fn init() -> Self { Self { list: Self::load_wallets(&AppConfig::chain_type()) } } + /// Create new wallet and add it to state. + pub fn create_wallet( + name: String, + password: String, + mnemonic: String, + external_node_url: Option + )-> Result<(), Error> { + let wallet = Wallet::create(name, password, mnemonic, external_node_url)?; + let mut w_state = WALLETS_STATE.write().unwrap(); + w_state.list.push(wallet); + Ok(()) + } + /// Load wallets for provided [`ChainType`]. fn load_wallets(chain_type: &ChainTypes) -> Vec { let mut wallets = Vec::new(); - let wallets_dir = Self::get_wallets_base_dir(chain_type); - // Load wallets from directory. + let wallets_dir = Self::get_base_path(chain_type); + // Load wallets from base directory. for dir in wallets_dir.read_dir().unwrap() { - let wallet = Wallet::load(dir.unwrap().path()); - if let Some(w) = wallet { - wallets.push(w); + let wallet_dir = dir.unwrap().path(); + if wallet_dir.is_dir() { + let wallet = Wallet::init(wallet_dir); + if let Some(w) = wallet { + wallets.push(w); + } } - continue; } wallets } - /// Get wallets base directory for provided [`ChainTypes`]. - pub fn get_wallets_base_dir(chain_type: &ChainTypes) -> PathBuf { + /// Get wallets base directory path for provided [`ChainTypes`]. + pub fn get_base_path(chain_type: &ChainTypes) -> PathBuf { let mut wallets_path = Settings::get_base_path(Some(chain_type.shortname())); wallets_path.push(BASE_DIR_NAME); - // Create wallets directory if it doesn't exist. + // Create wallets base directory if it doesn't exist. if !wallets_path.exists() { let _ = fs::create_dir_all(wallets_path.clone()); } diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index ed2f38b..48c4f56 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -12,6 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. +pub mod updater; +pub mod selection; +pub mod tx; +pub mod keys; + mod wallet; pub use wallet::Wallet; diff --git a/src/wallet/selection.rs b/src/wallet/selection.rs new file mode 100644 index 0000000..9ec7120 --- /dev/null +++ b/src/wallet/selection.rs @@ -0,0 +1,714 @@ +// Copyright 2023 The Grim 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::collections::HashMap; +use std::convert::TryInto; + +use grin_core::core::amount_to_hr_string; +use grin_core::libtx::{ + build, + proof::{ProofBuild, ProofBuilder}, + tx_fee, +}; +use grin_keychain::{Identifier, Keychain}; +use grin_util::secp::key::SecretKey; +use grin_util::secp::pedersen; +use grin_wallet_libwallet::{address, Context, NodeClient, OutputData, OutputStatus, StoredProofInfo, TxLogEntry, TxLogEntryType, WalletBackend}; +use grin_wallet_libwallet::Error; +use grin_wallet_libwallet::Slate; +use grin_wallet_util::OnionV3Address; +use log::debug; + +use crate::wallet::keys::next_available_key; + +/// Initialize a transaction on the sender side, returns a corresponding +/// libwallet transaction slate with the appropriate inputs selected, +/// and saves the private wallet identifiers of our selected outputs +/// into our transaction context + +pub fn build_send_tx<'a, T: ?Sized, C, K>( + wallet: &mut T, + keychain: &K, + keychain_mask: Option<&SecretKey>, + slate: &mut Slate, + current_height: u64, + minimum_confirmations: u64, + max_outputs: usize, + change_outputs: usize, + selection_strategy_is_use_all: bool, + fixed_fee: Option, + parent_key_id: Identifier, + use_test_nonce: bool, + is_initiator: bool, + amount_includes_fee: bool, +) -> Result + where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let (elems, inputs, change_amounts_derivations, fee) = select_send_tx( + wallet, + keychain_mask, + slate.amount, + amount_includes_fee, + current_height, + minimum_confirmations, + max_outputs, + change_outputs, + selection_strategy_is_use_all, + &parent_key_id, + false, + )?; + if amount_includes_fee { + slate.amount = slate.amount.checked_sub(fee).ok_or(Error::GenericError( + format!("Transaction amount is too small to include fee").into(), + ))?; + }; + + if fixed_fee.map(|f| fee != f).unwrap_or(false) { + return Err(Error::Fee( + "The initially selected fee is not sufficient".into(), + )); + } + + // Update the fee on the slate so we account for this when building the tx. + slate.fee_fields = fee.try_into().unwrap(); + slate.add_transaction_elements(keychain, &ProofBuilder::new(keychain), elems)?; + + // Create our own private context + let mut context = Context::new( + keychain.secp(), + &parent_key_id, + use_test_nonce, + is_initiator, + ); + + context.fee = Some(slate.fee_fields); + context.amount = slate.amount; + + // Store our private identifiers for each input + for input in inputs { + context.add_input(&input.key_id, &input.mmr_index, input.value); + } + + let mut commits: HashMap> = HashMap::new(); + + // Store change output(s) and cached commits + for (change_amount, id, mmr_index) in &change_amounts_derivations { + context.add_output(&id, &mmr_index, *change_amount); + commits.insert( + id.clone(), + wallet.calc_commit_for_cache(keychain_mask, *change_amount, &id)?, + ); + } + + Ok(context) +} + +/// Locks all corresponding outputs in the context, creates +/// change outputs and tx log entry +pub fn lock_tx_context<'a, T: ?Sized, C, K>( + wallet: &mut T, + keychain_mask: Option<&SecretKey>, + slate: &Slate, + current_height: u64, + context: &Context, + excess_override: Option, +) -> Result<(), Error> + where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let mut output_commits: HashMap, u64)> = HashMap::new(); + // Store cached commits before locking wallet + let mut total_change = 0; + for (id, _, change_amount) in &context.get_outputs() { + output_commits.insert( + id.clone(), + ( + wallet.calc_commit_for_cache(keychain_mask, *change_amount, &id)?, + *change_amount, + ), + ); + total_change += change_amount; + } + + debug!("Change amount is: {}", total_change); + + let keychain = wallet.keychain(keychain_mask)?; + + let tx_entry = { + let lock_inputs = context.get_inputs(); + let slate_id = slate.id; + let height = current_height; + let parent_key_id = context.parent_key_id.clone(); + let mut batch = wallet.batch(keychain_mask)?; + let log_id = batch.next_tx_log_id(&parent_key_id)?; + let mut t = TxLogEntry::new(parent_key_id.clone(), TxLogEntryType::TxSent, log_id); + t.tx_slate_id = Some(slate_id); + let filename = format!("{}.grintx", slate_id); + t.stored_tx = Some(filename); + t.fee = context.fee; + t.ttl_cutoff_height = match slate.ttl_cutoff_height { + 0 => None, + n => Some(n), + }; + + if let Ok(e) = slate.calc_excess(keychain.secp()) { + t.kernel_excess = Some(e) + } + if let Some(e) = excess_override { + t.kernel_excess = Some(e) + } + t.kernel_lookup_min_height = Some(current_height); + + let mut amount_debited = 0; + t.num_inputs = lock_inputs.len(); + for id in lock_inputs { + let mut coin = batch.get(&id.0, &id.1).unwrap(); + coin.tx_log_entry = Some(log_id); + amount_debited += coin.value; + batch.lock_output(&mut coin)?; + } + + t.amount_debited = amount_debited; + + // store extra payment proof info, if required + if let Some(ref p) = slate.payment_proof { + let sender_address_path = match context.payment_proof_derivation_index { + Some(p) => p, + None => { + return Err(Error::PaymentProof( + "Payment proof derivation index required".to_owned(), + ) + .into()); + } + }; + let sender_key = address::address_from_derivation_path( + &keychain, + &parent_key_id, + sender_address_path, + )?; + let sender_address = OnionV3Address::from_private(&sender_key.0)?; + t.payment_proof = Some(StoredProofInfo { + receiver_address: p.receiver_address, + receiver_signature: p.receiver_signature, + sender_address: sender_address.to_ed25519()?, + sender_address_path, + sender_signature: None, + }); + }; + + // write the output representing our change + for (id, _, _) in &context.get_outputs() { + t.num_outputs += 1; + let (commit, change_amount) = output_commits.get(&id).unwrap().clone(); + t.amount_credited += change_amount; + batch.save(OutputData { + root_key_id: parent_key_id.clone(), + key_id: id.clone(), + n_child: id.to_path().last_path_index(), + commit, + mmr_index: None, + value: change_amount, + status: OutputStatus::Unconfirmed, + height, + lock_height: 0, + is_coinbase: false, + tx_log_entry: Some(log_id), + })?; + } + batch.save_tx_log_entry(t.clone(), &parent_key_id)?; + batch.commit()?; + t + }; + wallet.store_tx( + &format!("{}", tx_entry.tx_slate_id.unwrap()), + slate.tx_or_err()?, + )?; + Ok(()) +} + +/// Creates a new output in the wallet for the recipient, +/// returning the key of the fresh output +/// Also creates a new transaction containing the output +pub fn build_recipient_output<'a, T: ?Sized, C, K>( + wallet: &mut T, + keychain_mask: Option<&SecretKey>, + slate: &mut Slate, + current_height: u64, + parent_key_id: Identifier, + use_test_rng: bool, + is_initiator: bool, +) -> Result<(Identifier, Context, TxLogEntry), Error> + where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + // Create a potential output for this transaction + let key_id = next_available_key(wallet, keychain_mask).unwrap(); + let keychain = wallet.keychain(keychain_mask)?; + let key_id_inner = key_id.clone(); + let amount = slate.amount; + let height = current_height; + + let slate_id = slate.id; + slate.add_transaction_elements( + &keychain, + &ProofBuilder::new(&keychain), + vec![build::output(amount, key_id.clone())], + )?; + + // Add blinding sum to our context + let mut context = Context::new(keychain.secp(), &parent_key_id, use_test_rng, is_initiator); + + context.add_output(&key_id, &None, amount); + context.amount = amount; + context.fee = slate.fee_fields.as_opt(); + let commit = wallet.calc_commit_for_cache(keychain_mask, amount, &key_id_inner)?; + let mut batch = wallet.batch(keychain_mask)?; + let log_id = batch.next_tx_log_id(&parent_key_id)?; + let mut t = TxLogEntry::new(parent_key_id.clone(), TxLogEntryType::TxReceived, log_id); + t.tx_slate_id = Some(slate_id); + t.amount_credited = amount; + t.num_outputs = 1; + t.ttl_cutoff_height = match slate.ttl_cutoff_height { + 0 => None, + n => Some(n), + }; + // when invoicing, this will be invalid + if let Ok(e) = slate.calc_excess(keychain.secp()) { + t.kernel_excess = Some(e) + } + t.kernel_lookup_min_height = Some(current_height); + batch.save(OutputData { + root_key_id: parent_key_id.clone(), + key_id: key_id_inner.clone(), + mmr_index: None, + n_child: key_id_inner.to_path().last_path_index(), + commit, + value: amount, + status: OutputStatus::Unconfirmed, + height, + lock_height: 0, + is_coinbase: false, + tx_log_entry: Some(log_id), + })?; + batch.save_tx_log_entry(t.clone(), &parent_key_id)?; + batch.commit()?; + + Ok((key_id, context, t)) +} + +/// Builds a transaction to send to someone from the HD seed associated with the +/// wallet and the amount to send. Handles reading through the wallet data file, +/// selecting outputs to spend and building the change. +pub fn select_send_tx<'a, T: ?Sized, C, K, B>( + wallet: &mut T, + keychain_mask: Option<&SecretKey>, + amount: u64, + amount_includes_fee: bool, + current_height: u64, + minimum_confirmations: u64, + max_outputs: usize, + change_outputs: usize, + selection_strategy_is_use_all: bool, + parent_key_id: &Identifier, + include_inputs_in_sum: bool, +) -> Result< + ( + Vec>>, + Vec, + Vec<(u64, Identifier, Option)>, // change amounts and derivations + u64, // fee + ), + Error, +> + where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, + B: ProofBuild, +{ + let (coins, _total, amount, fee) = select_coins_and_fee( + wallet, + amount, + amount_includes_fee, + current_height, + minimum_confirmations, + max_outputs, + change_outputs, + selection_strategy_is_use_all, + &parent_key_id, + )?; + + // build transaction skeleton with inputs and change + let (parts, change_amounts_derivations) = inputs_and_change( + &coins, + wallet, + keychain_mask, + amount, + fee, + change_outputs, + include_inputs_in_sum, + )?; + + Ok((parts, coins, change_amounts_derivations, fee)) +} + +/// Select outputs and calculating fee. +pub fn select_coins_and_fee<'a, T: ?Sized, C, K>( + wallet: &mut T, + amount: u64, + amount_includes_fee: bool, + current_height: u64, + minimum_confirmations: u64, + max_outputs: usize, + change_outputs: usize, + selection_strategy_is_use_all: bool, + parent_key_id: &Identifier, +) -> Result< + ( + Vec, + u64, // total + u64, // amount + u64, // fee + ), + Error, +> + where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + // select some spendable coins from the wallet + let (max_outputs, mut coins) = select_coins( + wallet, + amount, + current_height, + minimum_confirmations, + max_outputs, + selection_strategy_is_use_all, + parent_key_id, + ); + + // sender is responsible for setting the fee on the partial tx + // recipient should double check the fee calculation and not blindly trust the + // sender + + // First attempt to spend without change + let mut fee = tx_fee(coins.len(), 1, 1); + let mut total: u64 = coins.iter().map(|c| c.value).sum(); + let mut amount_with_fee = match amount_includes_fee { + true => amount, + false => amount + fee, + }; + + if total == 0 { + return Err(Error::NotEnoughFunds { + available: 0, + available_disp: amount_to_hr_string(0, false), + needed: amount_with_fee, + needed_disp: amount_to_hr_string(amount_with_fee, false), + }); + } + + // The amount with fee is more than the total values of our max outputs + if total < amount_with_fee && coins.len() == max_outputs { + return Err(Error::NotEnoughFunds { + available: total, + available_disp: amount_to_hr_string(total, false), + needed: amount_with_fee, + needed_disp: amount_to_hr_string(amount_with_fee, false), + }); + } + + let num_outputs = change_outputs + 1; + + // We need to add a change address or amount with fee is more than total + if total != amount_with_fee { + fee = tx_fee(coins.len(), num_outputs, 1); + amount_with_fee = match amount_includes_fee { + true => amount, + false => amount + fee, + }; + + // Here check if we have enough outputs for the amount including fee otherwise + // look for other outputs and check again + while total < amount_with_fee { + // End the loop if we have selected all the outputs and still not enough funds + if coins.len() == max_outputs { + return Err(Error::NotEnoughFunds { + available: total, + available_disp: amount_to_hr_string(total, false), + needed: amount_with_fee, + needed_disp: amount_to_hr_string(amount_with_fee, false), + }); + } + + // select some spendable coins from the wallet + coins = select_coins( + wallet, + amount_with_fee, + current_height, + minimum_confirmations, + max_outputs, + selection_strategy_is_use_all, + parent_key_id, + ) + .1; + fee = tx_fee(coins.len(), num_outputs, 1); + total = coins.iter().map(|c| c.value).sum(); + amount_with_fee = match amount_includes_fee { + true => amount, + false => amount + fee, + }; + } + } + // If original amount includes fee, the new amount should + // be reduced, to accommodate the fee. + let new_amount = match amount_includes_fee { + true => amount.checked_sub(fee).ok_or(Error::GenericError( + format!("Transaction amount is too small to include fee").into(), + ))?, + false => amount, + }; + Ok((coins, total, new_amount, fee)) +} + +/// Selects inputs and change for a transaction +pub fn inputs_and_change<'a, T: ?Sized, C, K, B>( + coins: &[OutputData], + wallet: &mut T, + keychain_mask: Option<&SecretKey>, + amount: u64, + fee: u64, + num_change_outputs: usize, + include_inputs_in_sum: bool, +) -> Result< + ( + Vec>>, + Vec<(u64, Identifier, Option)>, + ), + Error, +> + where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, + B: ProofBuild, +{ + let mut parts = vec![]; + + // calculate the total across all inputs, and how much is left + let total: u64 = coins.iter().map(|c| c.value).sum(); + + // if we are spending 10,000 coins to send 1,000 then our change will be 9,000 + // if the fee is 80 then the recipient will receive 1000 and our change will be + // 8,920 + let change = total - amount - fee; + + // build inputs using the appropriate derived key_ids + if include_inputs_in_sum { + for coin in coins { + if coin.is_coinbase { + parts.push(build::coinbase_input(coin.value, coin.key_id.clone())); + } else { + parts.push(build::input(coin.value, coin.key_id.clone())); + } + } + } + + let mut change_amounts_derivations = vec![]; + + if change == 0 { + debug!("No change (sending exactly amount + fee), no change outputs to build"); + } else { + debug!( + "Building change outputs: total change: {} ({} outputs)", + change, num_change_outputs + ); + + let part_change = change / num_change_outputs as u64; + let remainder_change = change % part_change; + + for x in 0..num_change_outputs { + // n-1 equal change_outputs and a final one accounting for any remainder + let change_amount = if x == (num_change_outputs - 1) { + part_change + remainder_change + } else { + part_change + }; + + let change_key = wallet.next_child(keychain_mask).unwrap(); + + change_amounts_derivations.push((change_amount, change_key.clone(), None)); + parts.push(build::output(change_amount, change_key)); + } + } + + Ok((parts, change_amounts_derivations)) +} + +/// Select spendable coins from a wallet. +/// Default strategy is to spend the maximum number of outputs (up to +/// max_outputs). Alternative strategy is to spend smallest outputs first +/// but only as many as necessary. When we introduce additional strategies +/// we should pass something other than a bool in. +pub fn select_coins<'a, T: ?Sized, C, K>( + wallet: &mut T, + amount: u64, + current_height: u64, + minimum_confirmations: u64, + max_outputs: usize, + select_all: bool, + parent_key_id: &Identifier, +) -> (usize, Vec) +// max_outputs_available, Outputs + where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + // first find all eligible outputs based on number of confirmations + let mut eligible = wallet + .iter() + .filter(|out| { + out.root_key_id == *parent_key_id + && out.eligible_to_spend(current_height, minimum_confirmations) + }) + .collect::>(); + + let max_available = eligible.len(); + + // sort eligible outputs by increasing value + eligible.sort_by_key(|out| out.value); + + // use a sliding window to identify potential sets of possible outputs to spend + // Case of amount > total amount of max_outputs(500): + // The limit exists because by default, we always select as many inputs as + // possible in a transaction, to reduce both the Output set and the fees. + // But that only makes sense up to a point, hence the limit to avoid being too + // greedy. But if max_outputs(500) is actually not enough to cover the whole + // amount, the wallet should allow going over it to satisfy what the user + // wants to send. So the wallet considers max_outputs more of a soft limit. + if eligible.len() > max_outputs { + for window in eligible.windows(max_outputs) { + let windowed_eligibles = window.to_vec(); + if let Some(outputs) = select_from(amount, select_all, windowed_eligibles) { + return (max_available, outputs); + } + } + // Not exist in any window of which total amount >= amount. + // Then take coins from the smallest one up to the total amount of selected + // coins = the amount. + if let Some(outputs) = select_from(amount, false, eligible.clone()) { + debug!( + "Extending maximum number of outputs. {} outputs selected.", + outputs.len() + ); + return (max_available, outputs); + } + } else if let Some(outputs) = select_from(amount, select_all, eligible.clone()) { + return (max_available, outputs); + } + + // we failed to find a suitable set of outputs to spend, + // so return the largest amount we can so we can provide guidance on what is + // possible + eligible.reverse(); + ( + max_available, + eligible.iter().take(max_outputs).cloned().collect(), + ) +} + +fn select_from(amount: u64, select_all: bool, outputs: Vec) -> Option> { + let total = outputs.iter().fold(0, |acc, x| acc + x.value); + if total >= amount { + if select_all { + Some(outputs.to_vec()) + } else { + let mut selected_amount = 0; + Some( + outputs + .iter() + .take_while(|out| { + let res = selected_amount < amount; + selected_amount += out.value; + res + }) + .cloned() + .collect(), + ) + } + } else { + None + } +} + +/// Repopulates output in the slate's tranacstion +/// with outputs from the stored context +/// change outputs and tx log entry +/// Remove the explicitly stored excess +pub fn repopulate_tx<'a, T: ?Sized, C, K>( + wallet: &mut T, + keychain_mask: Option<&SecretKey>, + slate: &mut Slate, + context: &Context, + update_fee: bool, +) -> Result<(), Error> + where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + // restore the original amount, fee + slate.amount = context.amount; + if update_fee { + slate.fee_fields = context + .fee + .ok_or_else(|| Error::Fee("Missing fee fields".into()))?; + } + + let keychain = wallet.keychain(keychain_mask)?; + + // restore my signature data + slate.add_participant_info(&keychain, &context, None)?; + + let mut parts = vec![]; + for (id, _, value) in &context.get_inputs() { + let input = wallet.iter().find(|out| out.key_id == *id); + if let Some(i) = input { + if i.is_coinbase { + parts.push(build::coinbase_input(*value, i.key_id.clone())); + } else { + parts.push(build::input(*value, i.key_id.clone())); + } + } + } + for (id, _, value) in &context.get_outputs() { + let output = wallet.iter().find(|out| out.key_id == *id); + if let Some(i) = output { + parts.push(build::output(*value, i.key_id.clone())); + } + } + let _ = slate.add_transaction_elements(&keychain, &ProofBuilder::new(&keychain), parts)?; + // restore the original offset + slate.tx_or_err_mut()?.offset = slate.offset.clone(); + Ok(()) +} \ No newline at end of file diff --git a/src/wallet/tx.rs b/src/wallet/tx.rs new file mode 100644 index 0000000..28af396 --- /dev/null +++ b/src/wallet/tx.rs @@ -0,0 +1,547 @@ +// Copyright 2023 The Grim 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::io::Cursor; + +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; +use ed25519_dalek::{Signer, Verifier}; +use ed25519_dalek::Keypair as DalekKeypair; +use ed25519_dalek::PublicKey as DalekPublicKey; +use ed25519_dalek::SecretKey as DalekSecretKey; +use ed25519_dalek::Signature as DalekSignature; +use grin_core::consensus::valid_header_version; +use grin_core::core::FeeFields; +use grin_core::core::HeaderVersion; +use grin_keychain::{Identifier, Keychain}; +use grin_util::Mutex; +use grin_util::secp::key::SecretKey; +use grin_util::secp::pedersen; +use grin_wallet_libwallet::{Context, NodeClient, StoredProofInfo, TxLogEntryType, WalletBackend}; +use grin_wallet_libwallet::{address, Error}; +use grin_wallet_libwallet::InitTxArgs; +use grin_wallet_libwallet::Slate; +use grin_wallet_util::OnionV3Address; +use lazy_static::lazy_static; +use log::trace; +use uuid::Uuid; + +use crate::wallet::selection::{build_recipient_output, build_send_tx, select_coins_and_fee}; +use crate::wallet::updater::{cancel_tx_and_outputs, refresh_outputs, retrieve_outputs, retrieve_txs}; + +/// Static value to increment UUIDs of slates. +lazy_static! { + static ref SLATE_COUNTER: Mutex = Mutex::new(0); +} + +/// Creates a new slate for a transaction, can be called by anyone involved in +/// the transaction (sender(s), receiver(s)). +pub fn new_tx_slate<'a, T: ?Sized, C, K>( + wallet: &mut T, + amount: u64, + is_invoice: bool, + num_participants: u8, + use_test_rng: bool, + ttl_blocks: Option, +) -> Result + where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let current_height = wallet.w2n_client().get_chain_tip()?.0; + let mut slate = Slate::blank(num_participants, is_invoice); + if let Some(b) = ttl_blocks { + slate.ttl_cutoff_height = current_height + b; + } + if use_test_rng { + { + let sc = SLATE_COUNTER.lock(); + let bytes = [4, 54, 67, 12, 43, 2, 98, 76, 32, 50, 87, 5, 1, 33, 43, *sc]; + slate.id = Uuid::from_slice(&bytes).unwrap(); + } + *SLATE_COUNTER.lock() += 1; + } + slate.amount = amount; + + if valid_header_version(current_height, HeaderVersion(1)) { + slate.version_info.block_header_version = 1; + } + + if valid_header_version(current_height, HeaderVersion(2)) { + slate.version_info.block_header_version = 2; + } + + if valid_header_version(current_height, HeaderVersion(3)) { + slate.version_info.block_header_version = 3; + } + + // Set the features explicitly to 0 here. + // This will generate a Plain kernel (rather than a HeightLocked kernel). + slate.kernel_features = 0; + + Ok(slate) +} + +/// Add inputs to the slate (effectively becoming the sender). +pub fn add_inputs_to_slate<'a, T: ?Sized, C, K>( + wallet: &mut T, + keychain_mask: Option<&SecretKey>, + slate: &mut Slate, + current_height: u64, + minimum_confirmations: u64, + max_outputs: usize, + num_change_outputs: usize, + selection_strategy_is_use_all: bool, + parent_key_id: &Identifier, + is_initiator: bool, + use_test_rng: bool, + amount_includes_fee: bool, +) -> Result + where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + // sender should always refresh outputs + refresh_outputs(wallet, keychain_mask, parent_key_id, false)?; + + // Sender selects outputs into a new slate and save our corresponding keys in + // a transaction context. The secret key in our transaction context will be + // randomly selected. This returns the public slate, and a closure that locks + // our inputs and outputs once we're convinced the transaction exchange went + // according to plan + // This function is just a big helper to do all of that, in theory + // this process can be split up in any way + let mut context = build_send_tx( + wallet, + &wallet.keychain(keychain_mask)?, + keychain_mask, + slate, + current_height, + minimum_confirmations, + max_outputs, + num_change_outputs, + selection_strategy_is_use_all, + None, + parent_key_id.clone(), + use_test_rng, + is_initiator, + amount_includes_fee, + )?; + + // Generate a kernel offset and subtract from our context's secret key. Store + // the offset in the slate's transaction kernel, and adds our public key + // information to the slate + slate.fill_round_1(&wallet.keychain(keychain_mask)?, &mut context)?; + + context.initial_sec_key = context.sec_key.clone(); + + if !is_initiator { + // perform partial sig + slate.fill_round_2( + &wallet.keychain(keychain_mask)?, + &context.sec_key, + &context.sec_nonce, + )?; + } + + Ok(context) +} + +/// Add receiver output to the slate. +pub fn add_output_to_slate<'a, T: ?Sized, C, K>( + wallet: &mut T, + keychain_mask: Option<&SecretKey>, + slate: &mut Slate, + current_height: u64, + parent_key_id: &Identifier, + is_initiator: bool, + use_test_rng: bool, +) -> Result + where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let keychain = wallet.keychain(keychain_mask)?; + // create an output using the amount in the slate + let (_, mut context, mut tx) = build_recipient_output( + wallet, + keychain_mask, + slate, + current_height, + parent_key_id.clone(), + use_test_rng, + is_initiator, + )?; + + // fill public keys + slate.fill_round_1(&keychain, &mut context)?; + + context.initial_sec_key = context.sec_key.clone(); + + if !is_initiator { + // perform partial sig + slate.fill_round_2(&keychain, &context.sec_key, &context.sec_nonce)?; + // update excess in stored transaction + let mut batch = wallet.batch(keychain_mask)?; + tx.kernel_excess = Some(slate.calc_excess(keychain.secp())?); + batch.save_tx_log_entry(tx.clone(), &parent_key_id)?; + batch.commit()?; + } + + Ok(context) +} + +/// Create context, without adding inputs to slate. +pub fn create_late_lock_context<'a, T: ?Sized, C, K>( + wallet: &mut T, + keychain_mask: Option<&SecretKey>, + slate: &mut Slate, + current_height: u64, + init_tx_args: &InitTxArgs, + parent_key_id: &Identifier, + use_test_rng: bool, +) -> Result + where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + // sender should always refresh outputs + refresh_outputs(wallet, keychain_mask, parent_key_id, false)?; + + // we're just going to run a selection to get the potential fee, + // but this won't be locked + let (_coins, _total, _amount, fee) = select_coins_and_fee( + wallet, + init_tx_args.amount, + init_tx_args.amount_includes_fee.unwrap_or(false), + current_height, + init_tx_args.minimum_confirmations, + init_tx_args.max_outputs as usize, + init_tx_args.num_change_outputs as usize, + init_tx_args.selection_strategy_is_use_all, + &parent_key_id, + )?; + slate.fee_fields = FeeFields::new(0, fee)?; + + let keychain = wallet.keychain(keychain_mask)?; + + // Create our own private context + let mut context = Context::new(keychain.secp(), &parent_key_id, use_test_rng, true); + context.fee = Some(slate.fee_fields); + context.amount = slate.amount; + context.late_lock_args = Some(init_tx_args.clone()); + + // Generate a blinding factor for the tx and add + // public key info to the slate + slate.fill_round_1(&wallet.keychain(keychain_mask)?, &mut context)?; + + Ok(context) +} + +/// Complete a transaction. +pub fn complete_tx<'a, T: ?Sized, C, K>( + wallet: &mut T, + keychain_mask: Option<&SecretKey>, + slate: &mut Slate, + context: &Context, +) -> Result<(), Error> + where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + // when self sending invoice tx, use initiator nonce to finalize + let (sec_key, sec_nonce) = { + if context.initial_sec_key != context.sec_key + && context.initial_sec_nonce != context.sec_nonce + { + ( + context.initial_sec_key.clone(), + context.initial_sec_nonce.clone(), + ) + } else { + (context.sec_key.clone(), context.sec_nonce.clone()) + } + }; + slate.fill_round_2(&wallet.keychain(keychain_mask)?, &sec_key, &sec_nonce)?; + + // Final transaction can be built by anyone at this stage + trace!("Slate to finalize is: {}", slate); + slate.finalize(&wallet.keychain(keychain_mask)?)?; + Ok(()) +} + +/// Rollback outputs associated with a transaction in the wallet. +pub fn cancel_tx<'a, T: ?Sized, C, K>( + wallet: &mut T, + keychain_mask: Option<&SecretKey>, + parent_key_id: &Identifier, + tx_id: Option, + tx_slate_id: Option, +) -> Result<(), Error> + where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let mut tx_id_string = String::new(); + if let Some(tx_id) = tx_id { + tx_id_string = tx_id.to_string(); + } else if let Some(tx_slate_id) = tx_slate_id { + tx_id_string = tx_slate_id.to_string(); + } + let tx_vec = retrieve_txs( + wallet, + tx_id, + tx_slate_id, + None, + Some(&parent_key_id), + false, + )?; + if tx_vec.len() != 1 { + return Err(Error::TransactionDoesntExist(tx_id_string)); + } + let tx = tx_vec[0].clone(); + match tx.tx_type { + TxLogEntryType::TxSent | TxLogEntryType::TxReceived | TxLogEntryType::TxReverted => {} + _ => return Err(Error::TransactionNotCancellable(tx_id_string)), + } + if tx.confirmed { + return Err(Error::TransactionNotCancellable(tx_id_string)); + } + // get outputs associated with tx + let res = retrieve_outputs( + wallet, + keychain_mask, + false, + Some(tx.id), + Some(&parent_key_id), + )?; + let outputs = res.iter().map(|m| m.output.clone()).collect(); + cancel_tx_and_outputs(wallet, keychain_mask, tx, outputs, parent_key_id)?; + Ok(()) +} + +/// Update the stored transaction (this update needs to happen when the TX is finalised). +pub fn update_stored_tx<'a, T: ?Sized, C, K>( + wallet: &mut T, + keychain_mask: Option<&SecretKey>, + context: &Context, + slate: &Slate, + is_invoiced: bool, +) -> Result<(), Error> + where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + // finalize command + let tx_vec = retrieve_txs(wallet, None, Some(slate.id), None, None, false)?; + let mut tx = None; + // don't want to assume this is the right tx, in case of self-sending + for t in tx_vec { + if t.tx_type == TxLogEntryType::TxSent && !is_invoiced { + tx = Some(t); + break; + } + if t.tx_type == TxLogEntryType::TxReceived && is_invoiced { + tx = Some(t); + break; + } + } + let mut tx = match tx { + Some(t) => t, + None => return Err(Error::TransactionDoesntExist(slate.id.to_string())), + }; + let parent_key = tx.parent_key_id.clone(); + { + let keychain = wallet.keychain(keychain_mask)?; + tx.kernel_excess = Some(slate.calc_excess(keychain.secp())?); + } + + if let Some(ref p) = slate.clone().payment_proof { + let derivation_index = match context.payment_proof_derivation_index { + Some(i) => i, + None => 0, + }; + let keychain = wallet.keychain(keychain_mask)?; + let parent_key_id = wallet.parent_key_id(); + let excess = slate.calc_excess(keychain.secp())?; + let sender_key = + address::address_from_derivation_path(&keychain, &parent_key_id, derivation_index)?; + let sender_address = OnionV3Address::from_private(&sender_key.0)?; + let sig = + create_payment_proof_signature(slate.amount, &excess, p.sender_address, sender_key)?; + tx.payment_proof = Some(StoredProofInfo { + receiver_address: p.receiver_address, + receiver_signature: p.receiver_signature, + sender_address_path: derivation_index, + sender_address: sender_address.to_ed25519()?, + sender_signature: Some(sig), + }) + } + + wallet.store_tx(&format!("{}", tx.tx_slate_id.unwrap()), slate.tx_or_err()?)?; + + let mut batch = wallet.batch(keychain_mask)?; + batch.save_tx_log_entry(tx, &parent_key)?; + batch.commit()?; + Ok(()) +} + +pub fn payment_proof_message( + amount: u64, + kernel_commitment: &pedersen::Commitment, + sender_address: DalekPublicKey, +) -> Result, Error> { + let mut msg = Vec::new(); + msg.write_u64::(amount)?; + msg.append(&mut kernel_commitment.0.to_vec()); + msg.append(&mut sender_address.to_bytes().to_vec()); + Ok(msg) +} + +pub fn _decode_payment_proof_message( + msg: &[u8], +) -> Result<(u64, pedersen::Commitment, DalekPublicKey), Error> { + let mut rdr = Cursor::new(msg); + let amount = rdr.read_u64::()?; + let mut commit_bytes = [0u8; 33]; + for i in 0..33 { + commit_bytes[i] = rdr.read_u8()?; + } + let mut sender_address_bytes = [0u8; 32]; + for i in 0..32 { + sender_address_bytes[i] = rdr.read_u8()?; + } + + Ok(( + amount, + pedersen::Commitment::from_vec(commit_bytes.to_vec()), + DalekPublicKey::from_bytes(&sender_address_bytes).unwrap(), + )) +} + +/// Create a payment proof. +pub fn create_payment_proof_signature( + amount: u64, + kernel_commitment: &pedersen::Commitment, + sender_address: DalekPublicKey, + sec_key: SecretKey, +) -> Result { + let msg = payment_proof_message(amount, kernel_commitment, sender_address)?; + let d_skey = match DalekSecretKey::from_bytes(&sec_key.0) { + Ok(k) => k, + Err(e) => { + return Err(Error::ED25519Key(format!("{}", e))); + } + }; + let pub_key: DalekPublicKey = (&d_skey).into(); + let keypair = DalekKeypair { + public: pub_key, + secret: d_skey, + }; + Ok(keypair.sign(&msg)) +} + +/// Verify all aspects of a completed payment proof on the current slate. +pub fn verify_slate_payment_proof<'a, T: ?Sized, C, K>( + wallet: &mut T, + keychain_mask: Option<&SecretKey>, + parent_key_id: &Identifier, + context: &Context, + slate: &Slate, +) -> Result<(), Error> + where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let tx_vec = retrieve_txs( + wallet, + None, + Some(slate.id), + None, + Some(parent_key_id), + false, + )?; + if tx_vec.is_empty() { + return Err(Error::PaymentProof( + "TxLogEntry with original proof info not found (is account correct?)".to_owned(), + )); + } + + let orig_proof_info = tx_vec[0].clone().payment_proof; + + if orig_proof_info.is_some() && slate.payment_proof.is_none() { + return Err(Error::PaymentProof( + "Expected Payment Proof for this Transaction is not present".to_owned(), + )); + } + + if let Some(ref p) = slate.clone().payment_proof { + let orig_proof_info = match orig_proof_info { + Some(p) => p.clone(), + None => { + return Err(Error::PaymentProof( + "Original proof info not stored in tx".to_owned(), + )); + } + }; + let keychain = wallet.keychain(keychain_mask)?; + let index = match context.payment_proof_derivation_index { + Some(i) => i, + None => { + return Err(Error::PaymentProof( + "Payment proof derivation index required".to_owned(), + )); + } + }; + let orig_sender_sk = + address::address_from_derivation_path(&keychain, parent_key_id, index)?; + let orig_sender_address = OnionV3Address::from_private(&orig_sender_sk.0)?; + if p.sender_address != orig_sender_address.to_ed25519()? { + return Err(Error::PaymentProof( + "Sender address on slate does not match original sender address".to_owned(), + )); + } + + if orig_proof_info.receiver_address != p.receiver_address { + return Err(Error::PaymentProof( + "Recipient address on slate does not match original recipient address".to_owned(), + )); + } + let msg = payment_proof_message( + slate.amount, + &slate.calc_excess(&keychain.secp())?, + orig_sender_address.to_ed25519()?, + )?; + let sig = match p.receiver_signature { + Some(s) => s, + None => { + return Err(Error::PaymentProof( + "Recipient did not provide requested proof signature".to_owned(), + )); + } + }; + + if p.receiver_address.verify(&msg, &sig).is_err() { + return Err(Error::PaymentProof("Invalid proof signature".to_owned())); + }; + } + Ok(()) +} \ No newline at end of file diff --git a/src/wallet/updater.rs b/src/wallet/updater.rs new file mode 100644 index 0000000..da199e5 --- /dev/null +++ b/src/wallet/updater.rs @@ -0,0 +1,837 @@ +// Copyright 2023 The Grim 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::collections::{HashMap, HashSet}; + +use grin_keychain::{Identifier, Keychain, SwitchCommitmentType}; +use grin_util as util; +use grin_util::secp::key::SecretKey; +use grin_util::secp::pedersen; +use grin_util::static_secp_instance; +use grin_wallet_libwallet::{ + NodeClient, OutputData, OutputStatus, TxLogEntry, TxLogEntryType, WalletBackend, WalletInfo, +}; +use grin_wallet_libwallet::{ + OutputCommitMapping, RetrieveTxQueryArgs, RetrieveTxQuerySortField, + RetrieveTxQuerySortOrder, +}; +use grin_wallet_libwallet::Error; +use log::{debug, warn}; +use num_bigint::BigInt; +use uuid::Uuid; + +/// Retrieve all of the outputs (doesn't attempt to update from node) +pub fn retrieve_outputs<'a, T: ?Sized, C, K>( + wallet: &mut T, + keychain_mask: Option<&SecretKey>, + show_spent: bool, + tx_id: Option, + parent_key_id: Option<&Identifier>, +) -> Result, Error> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + // just read the wallet here, no need for a write lock + let mut outputs = wallet + .iter() + .filter(|out| show_spent || out.status != OutputStatus::Spent) + .collect::>(); + + // only include outputs with a given tx_id if provided + if let Some(id) = tx_id { + outputs = outputs + .into_iter() + .filter(|out| out.tx_log_entry == Some(id)) + .collect::>(); + } + + if let Some(k) = parent_key_id { + outputs = outputs + .iter() + .filter(|o| o.root_key_id == *k) + .cloned() + .collect() + } + + outputs.sort_by_key(|out| out.n_child); + let keychain = wallet.keychain(keychain_mask)?; + + let res = outputs + .into_iter() + .map(|output| { + let commit = match output.commit.clone() { + Some(c) => pedersen::Commitment::from_vec(util::from_hex(&c).unwrap()), + None => keychain + .commit(output.value, &output.key_id, SwitchCommitmentType::Regular) + .unwrap(), // TODO: proper support for different switch commitment schemes + }; + OutputCommitMapping { output, commit } + }) + .collect(); + Ok(res) +} + +/// Apply advanced filtering to resultset from retrieve_txs below +pub fn apply_advanced_tx_list_filtering<'a, T: ?Sized, C, K>( + wallet: &mut T, + query_args: &RetrieveTxQueryArgs, +) -> Vec +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + // Apply simple bool, GTE or LTE fields + let txs_iter: Box> = Box::new( + wallet + .tx_log_iter() + .filter(|tx_entry| { + if let Some(v) = query_args.exclude_cancelled { + if v { + tx_entry.tx_type != TxLogEntryType::TxReceivedCancelled + && tx_entry.tx_type != TxLogEntryType::TxSentCancelled + } else { + true + } + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.include_outstanding_only { + if v { + !tx_entry.confirmed + } else { + true + } + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.include_confirmed_only { + if v { + tx_entry.confirmed + } else { + true + } + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.include_sent_only { + if v { + tx_entry.tx_type == TxLogEntryType::TxSent + || tx_entry.tx_type == TxLogEntryType::TxSentCancelled + } else { + true + } + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.include_received_only { + if v { + tx_entry.tx_type == TxLogEntryType::TxReceived + || tx_entry.tx_type == TxLogEntryType::TxReceivedCancelled + } else { + true + } + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.include_coinbase_only { + if v { + tx_entry.tx_type == TxLogEntryType::ConfirmedCoinbase + } else { + true + } + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.include_reverted_only { + if v { + tx_entry.tx_type == TxLogEntryType::TxReverted + } else { + true + } + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.min_id { + tx_entry.id >= v + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.max_id { + tx_entry.id <= v + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.min_amount { + if tx_entry.tx_type == TxLogEntryType::TxSent + || tx_entry.tx_type == TxLogEntryType::TxSentCancelled + { + BigInt::from(tx_entry.amount_debited) + - BigInt::from(tx_entry.amount_credited) + >= BigInt::from(v) + } else { + BigInt::from(tx_entry.amount_credited) + - BigInt::from(tx_entry.amount_debited) + >= BigInt::from(v) + } + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.max_amount { + if tx_entry.tx_type == TxLogEntryType::TxSent + || tx_entry.tx_type == TxLogEntryType::TxSentCancelled + { + BigInt::from(tx_entry.amount_debited) + - BigInt::from(tx_entry.amount_credited) + <= BigInt::from(v) + } else { + BigInt::from(tx_entry.amount_credited) + - BigInt::from(tx_entry.amount_debited) + <= BigInt::from(v) + } + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.min_creation_timestamp { + tx_entry.creation_ts >= v + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.min_confirmed_timestamp { + tx_entry.creation_ts <= v + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.min_confirmed_timestamp { + if let Some(t) = tx_entry.confirmation_ts { + t >= v + } else { + true + } + } else { + true + } + }) + .filter(|tx_entry| { + if let Some(v) = query_args.max_confirmed_timestamp { + if let Some(t) = tx_entry.confirmation_ts { + t <= v + } else { + true + } + } else { + true + } + }), + ); + + let mut return_txs: Vec = txs_iter.collect(); + + // Now apply requested sorting + if let Some(ref s) = query_args.sort_field { + match s { + RetrieveTxQuerySortField::Id => { + return_txs.sort_by_key(|tx| tx.id); + } + RetrieveTxQuerySortField::CreationTimestamp => { + return_txs.sort_by_key(|tx| tx.creation_ts); + } + RetrieveTxQuerySortField::ConfirmationTimestamp => { + return_txs.sort_by_key(|tx| tx.confirmation_ts); + } + RetrieveTxQuerySortField::TotalAmount => { + return_txs.sort_by_key(|tx| { + if tx.tx_type == TxLogEntryType::TxSent + || tx.tx_type == TxLogEntryType::TxSentCancelled + { + BigInt::from(tx.amount_debited) - BigInt::from(tx.amount_credited) + } else { + BigInt::from(tx.amount_credited) - BigInt::from(tx.amount_debited) + } + }); + } + RetrieveTxQuerySortField::AmountCredited => { + return_txs.sort_by_key(|tx| tx.amount_credited); + } + RetrieveTxQuerySortField::AmountDebited => { + return_txs.sort_by_key(|tx| tx.amount_debited); + } + } + } else { + return_txs.sort_by_key(|tx| tx.id); + } + + if let Some(ref s) = query_args.sort_order { + match s { + RetrieveTxQuerySortOrder::Desc => return_txs.reverse(), + _ => {} + } + } + + // Apply limit if requested + if let Some(l) = query_args.limit { + return_txs = return_txs.into_iter().take(l as usize).collect() + } + + return_txs +} + +/// Retrieve all of the transaction entries, or a particular entry +/// if `parent_key_id` is set, only return entries from that key +pub fn retrieve_txs<'a, T: ?Sized, C, K>( + wallet: &mut T, + tx_id: Option, + tx_slate_id: Option, + query_args: Option, + parent_key_id: Option<&Identifier>, + outstanding_only: bool, +) -> Result, Error> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let mut txs; + // Adding in new transaction list query logic. If `tx_id` or `tx_slate_id` + // is provided, then `query_args` is ignored and old logic is followed. + if query_args.is_some() && tx_id.is_none() && tx_slate_id.is_none() { + txs = apply_advanced_tx_list_filtering(wallet, &query_args.unwrap()) + } else { + txs = wallet + .tx_log_iter() + .filter(|tx_entry| { + let f_pk = match parent_key_id { + Some(k) => tx_entry.parent_key_id == *k, + None => true, + }; + let f_tx_id = match tx_id { + Some(i) => tx_entry.id == i, + None => true, + }; + let f_txs = match tx_slate_id { + Some(t) => tx_entry.tx_slate_id == Some(t), + None => true, + }; + let f_outstanding = match outstanding_only { + true => { + !tx_entry.confirmed + && (tx_entry.tx_type == TxLogEntryType::TxReceived + || tx_entry.tx_type == TxLogEntryType::TxSent + || tx_entry.tx_type == TxLogEntryType::TxReverted) + } + false => true, + }; + f_pk && f_tx_id && f_txs && f_outstanding + }) + .collect(); + txs.sort_by_key(|tx| tx.creation_ts); + } + Ok(txs) +} + +/// Refreshes the outputs in a wallet with the latest information +/// from a node +pub fn refresh_outputs<'a, T: ?Sized, C, K>( + wallet: &mut T, + keychain_mask: Option<&SecretKey>, + parent_key_id: &Identifier, + update_all: bool, +) -> Result<(), Error> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let height = wallet.w2n_client().get_chain_tip()?.0; + refresh_output_state(wallet, keychain_mask, height, parent_key_id, update_all)?; + Ok(()) +} + +/// build a local map of wallet outputs keyed by commit +/// and a list of outputs we want to query the node for +pub fn map_wallet_outputs<'a, T: ?Sized, C, K>( + wallet: &mut T, + keychain_mask: Option<&SecretKey>, + parent_key_id: &Identifier, + update_all: bool, +) -> Result, Option, bool)>, Error> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let mut wallet_outputs = HashMap::new(); + let keychain = wallet.keychain(keychain_mask)?; + let unspents: Vec = wallet + .iter() + .filter(|x| x.root_key_id == *parent_key_id && x.status != OutputStatus::Spent) + .collect(); + + let tx_entries = retrieve_txs(wallet, None, None, None, Some(&parent_key_id), true)?; + + // Only select outputs that are actually involved in an outstanding transaction + let unspents = match update_all { + false => unspents + .into_iter() + .filter(|x| match x.tx_log_entry.as_ref() { + Some(t) => tx_entries.iter().any(|te| te.id == *t), + None => true, + }) + .collect(), + true => unspents, + }; + + for out in unspents { + let commit = match out.commit.clone() { + Some(c) => pedersen::Commitment::from_vec(util::from_hex(&c).unwrap()), + None => keychain + .commit(out.value, &out.key_id, SwitchCommitmentType::Regular) + .unwrap(), // TODO: proper support for different switch commitment schemes + }; + let val = ( + out.key_id.clone(), + out.mmr_index, + out.tx_log_entry, + out.status == OutputStatus::Unspent, + ); + wallet_outputs.insert(commit, val); + } + Ok(wallet_outputs) +} + +/// Cancel transaction and associated outputs +pub fn cancel_tx_and_outputs<'a, T: ?Sized, C, K>( + wallet: &mut T, + keychain_mask: Option<&SecretKey>, + mut tx: TxLogEntry, + outputs: Vec, + parent_key_id: &Identifier, +) -> Result<(), Error> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let mut batch = wallet.batch(keychain_mask)?; + + for mut o in outputs { + // unlock locked outputs + if o.status == OutputStatus::Unconfirmed || o.status == OutputStatus::Reverted { + batch.delete(&o.key_id, &o.mmr_index)?; + } + if o.status == OutputStatus::Locked { + o.status = OutputStatus::Unspent; + batch.save(o)?; + } + } + match tx.tx_type { + TxLogEntryType::TxSent => tx.tx_type = TxLogEntryType::TxSentCancelled, + TxLogEntryType::TxReceived | TxLogEntryType::TxReverted => { + tx.tx_type = TxLogEntryType::TxReceivedCancelled + } + _ => {} + } + batch.save_tx_log_entry(tx, parent_key_id)?; + batch.commit()?; + Ok(()) +} + +/// Apply refreshed API output data to the wallet +pub fn apply_api_outputs<'a, T: ?Sized, C, K>( + wallet: &mut T, + keychain_mask: Option<&SecretKey>, + wallet_outputs: &HashMap, Option, bool)>, + api_outputs: &HashMap, + reverted_kernels: HashSet, + height: u64, + parent_key_id: &Identifier, +) -> Result<(), Error> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + // now for each commit, find the output in the wallet and the corresponding + // api output (if it exists) and refresh it in-place in the wallet. + // Note: minimizing the time we spend holding the wallet lock. + { + let last_confirmed_height = wallet.last_confirmed_height()?; + // If the server height is less than our confirmed height, don't apply + // these changes as the chain is syncing, incorrect or forking + if height < last_confirmed_height { + warn!( + "Not updating outputs as the height of the node's chain \ + is less than the last reported wallet update height." + ); + warn!("Please wait for sync on node to complete or fork to resolve and try again."); + return Ok(()); + } + let mut batch = wallet.batch(keychain_mask)?; + for (commit, (id, mmr_index, _, _)) in wallet_outputs.iter() { + if let Ok(mut output) = batch.get(id, mmr_index) { + match api_outputs.get(&commit) { + Some(o) => { + // if this is a coinbase tx being confirmed, it's recordable in tx log + if output.is_coinbase && output.status == OutputStatus::Unconfirmed { + let log_id = batch.next_tx_log_id(parent_key_id)?; + let mut t = TxLogEntry::new( + parent_key_id.clone(), + TxLogEntryType::ConfirmedCoinbase, + log_id, + ); + t.confirmed = true; + t.amount_credited = output.value; + t.amount_debited = 0; + t.num_outputs = 1; + // calculate kernel excess for coinbase + { + let secp = static_secp_instance(); + let secp = secp.lock(); + let over_commit = secp.commit_value(output.value)?; + let excess = secp.commit_sum(vec![*commit], vec![over_commit])?; + t.kernel_excess = Some(excess); + t.kernel_lookup_min_height = Some(height); + } + t.update_confirmation_ts(); + output.tx_log_entry = Some(log_id); + batch.save_tx_log_entry(t, &parent_key_id)?; + } + // also mark the transaction in which this output is involved as confirmed + // note that one involved input/output confirmation SHOULD be enough + // to reliably confirm the tx + if !output.is_coinbase + && (output.status == OutputStatus::Unconfirmed + || output.status == OutputStatus::Reverted) + { + let tx = batch.tx_log_iter().find(|t| { + Some(t.id) == output.tx_log_entry + && t.parent_key_id == *parent_key_id + }); + if let Some(mut t) = tx { + if t.tx_type == TxLogEntryType::TxReverted { + t.tx_type = TxLogEntryType::TxReceived; + t.reverted_after = None; + } + t.update_confirmation_ts(); + t.confirmed = true; + batch.save_tx_log_entry(t, &parent_key_id)?; + } + } + output.height = o.1; + output.mark_unspent(); + } + None => { + if !output.is_coinbase + && output + .tx_log_entry + .map(|i| reverted_kernels.contains(&i)) + .unwrap_or(false) + { + output.mark_reverted(); + } else { + output.mark_spent(); + } + } + } + batch.save(output)?; + } + } + + for mut tx in batch.tx_log_iter() { + if reverted_kernels.contains(&tx.id) && tx.parent_key_id == *parent_key_id { + tx.tx_type = TxLogEntryType::TxReverted; + tx.reverted_after = tx.confirmation_ts.clone().and_then(|t| { + let now = chrono::Utc::now(); + (now - t).to_std().ok() + }); + tx.confirmed = false; + batch.save_tx_log_entry(tx, &parent_key_id)?; + } + } + + { + batch.save_last_confirmed_height(parent_key_id, height)?; + } + batch.commit()?; + } + Ok(()) +} + +/// Builds a single api query to retrieve the latest output data from the node. +/// So we can refresh the local wallet outputs. +pub fn refresh_output_state<'a, T: ?Sized, C, K>( + wallet: &mut T, + keychain_mask: Option<&SecretKey>, + height: u64, + parent_key_id: &Identifier, + update_all: bool, +) -> Result<(), Error> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + debug!("Refreshing wallet outputs"); + + // build a local map of wallet outputs keyed by commit + // and a list of outputs we want to query the node for + let wallet_outputs = map_wallet_outputs(wallet, keychain_mask, parent_key_id, update_all)?; + + let wallet_output_keys = wallet_outputs.keys().copied().collect(); + + let api_outputs = wallet + .w2n_client() + .get_outputs_from_node(wallet_output_keys)?; + + // For any disappeared output, check the on-chain status of the corresponding transaction kernel + // If it is no longer present, the transaction was reverted due to a re-org + let reverted_kernels = + find_reverted_kernels(wallet, &wallet_outputs, &api_outputs, parent_key_id)?; + + apply_api_outputs( + wallet, + keychain_mask, + &wallet_outputs, + &api_outputs, + reverted_kernels, + height, + parent_key_id, + )?; + clean_old_unconfirmed(wallet, keychain_mask, height)?; + Ok(()) +} + +fn find_reverted_kernels<'a, T: ?Sized, C, K>( + wallet: &mut T, + wallet_outputs: &HashMap, Option, bool)>, + api_outputs: &HashMap, + parent_key_id: &Identifier, +) -> Result, Error> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let mut client = wallet.w2n_client().clone(); + let mut ids = HashSet::new(); + + // Get transaction IDs for outputs that are no longer unspent + for (commit, (_, _, tx_id, was_unspent)) in wallet_outputs { + if let Some(tx_id) = *tx_id { + if *was_unspent && !api_outputs.contains_key(commit) { + ids.insert(tx_id); + } + } + } + + // Get corresponding kernels + let kernels = wallet + .tx_log_iter() + .filter(|t| { + ids.contains(&t.id) + && t.parent_key_id == *parent_key_id + && t.tx_type == TxLogEntryType::TxReceived + }) + .filter_map(|t| { + t.kernel_excess + .map(|e| (t.id, e, t.kernel_lookup_min_height)) + }); + + // Check each of the kernels on-chain + let mut reverted = HashSet::new(); + for (id, excess, min_height) in kernels { + if client.get_kernel(&excess, min_height, None)?.is_none() { + reverted.insert(id); + } + } + + Ok(reverted) +} + +fn clean_old_unconfirmed<'a, T: ?Sized, C, K>( + wallet: &mut T, + keychain_mask: Option<&SecretKey>, + height: u64, +) -> Result<(), Error> +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + if height < 50 { + return Ok(()); + } + let mut ids_to_del = vec![]; + for out in wallet.iter() { + if out.status == OutputStatus::Unconfirmed + && out.height > 0 + && out.height < height - 50 + && out.is_coinbase + { + ids_to_del.push(out.key_id.clone()) + } + } + let mut batch = wallet.batch(keychain_mask)?; + for id in ids_to_del { + batch.delete(&id, &None)?; + } + batch.commit()?; + Ok(()) +} + +/// Retrieve summary info about the wallet +/// caller should refresh first if desired +pub fn retrieve_info<'a, T: ?Sized, C, K>( + wallet: &mut T, + parent_key_id: &Identifier, + minimum_confirmations: u64, +) -> Result +where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let current_height = wallet.last_confirmed_height()?; + let outputs = wallet + .iter() + .filter(|out| out.root_key_id == *parent_key_id); + + let mut unspent_total = 0; + let mut immature_total = 0; + let mut awaiting_finalization_total = 0; + let mut unconfirmed_total = 0; + let mut locked_total = 0; + let mut reverted_total = 0; + + for out in outputs { + match out.status { + OutputStatus::Unspent => { + if out.is_coinbase && out.lock_height > current_height { + immature_total += out.value; + } else if out.num_confirmations(current_height) < minimum_confirmations { + // Treat anything less than minimum confirmations as "unconfirmed". + unconfirmed_total += out.value; + } else { + unspent_total += out.value; + } + } + OutputStatus::Unconfirmed => { + // We ignore unconfirmed coinbase outputs completely. + if !out.is_coinbase { + if minimum_confirmations == 0 { + unconfirmed_total += out.value; + } else { + awaiting_finalization_total += out.value; + } + } + } + OutputStatus::Locked => { + locked_total += out.value; + } + OutputStatus::Reverted => reverted_total += out.value, + OutputStatus::Spent => {} + } + } + + Ok(WalletInfo { + last_confirmed_height: current_height, + minimum_confirmations, + total: unspent_total + unconfirmed_total + immature_total, + amount_awaiting_finalization: awaiting_finalization_total, + amount_awaiting_confirmation: unconfirmed_total, + amount_immature: immature_total, + amount_locked: locked_total, + amount_currently_spendable: unspent_total, + amount_reverted: reverted_total, + }) +} + +/// Rollback outputs associated with a transaction in the wallet +pub fn cancel_tx<'a, T: ?Sized, C, K>( + wallet: &mut T, + keychain_mask: Option<&SecretKey>, + parent_key_id: &Identifier, + tx_id: Option, + tx_slate_id: Option, +) -> Result<(), Error> + where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let mut tx_id_string = String::new(); + if let Some(tx_id) = tx_id { + tx_id_string = tx_id.to_string(); + } else if let Some(tx_slate_id) = tx_slate_id { + tx_id_string = tx_slate_id.to_string(); + } + let tx_vec = retrieve_txs( + wallet, + tx_id, + tx_slate_id, + None, + Some(&parent_key_id), + false, + )?; + if tx_vec.len() != 1 { + return Err(Error::TransactionDoesntExist(tx_id_string)); + } + let tx = tx_vec[0].clone(); + match tx.tx_type { + TxLogEntryType::TxSent | TxLogEntryType::TxReceived | TxLogEntryType::TxReverted => {} + _ => return Err(Error::TransactionNotCancellable(tx_id_string)), + } + if tx.confirmed { + return Err(Error::TransactionNotCancellable(tx_id_string)); + } + // get outputs associated with tx + let res = retrieve_outputs( + wallet, + keychain_mask, + false, + Some(tx.id), + Some(&parent_key_id), + )?; + let outputs = res.iter().map(|m| m.output.clone()).collect(); + cancel_tx_and_outputs(wallet, keychain_mask, tx, outputs, parent_key_id)?; + Ok(()) +} \ No newline at end of file diff --git a/src/wallet/wallet.rs b/src/wallet/wallet.rs index e8ce48d..9efacff 100644 --- a/src/wallet/wallet.rs +++ b/src/wallet/wallet.rs @@ -12,51 +12,499 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::ffi::OsString; use std::path::PathBuf; -use crate::node::NodeConfig; +use std::sync::Arc; +use grin_core::global; +use grin_keychain::{ExtKeychain, Identifier, Keychain}; +use grin_util::types::ZeroingString; +use grin_wallet_api::{Foreign, ForeignCheckMiddlewareFn, Owner}; +use grin_wallet_impls::{DefaultLCProvider, DefaultWalletImpl, HTTPNodeClient}; +use grin_wallet_libwallet::{Error, NodeClient, NodeVersionInfo, OutputStatus, Slate, slate_versions, SlatepackArmor, Slatepacker, SlatepackerArgs, TxLogEntry, wallet_lock, WalletBackend, WalletInfo, WalletInst, WalletLCProvider}; +use grin_wallet_libwallet::Error::GenericError; +use log::debug; +use parking_lot::Mutex; +use uuid::Uuid; + +use crate::AppConfig; +use crate::node::NodeConfig; +use crate::wallet::selection::lock_tx_context; +use crate::wallet::tx::{add_inputs_to_slate, new_tx_slate}; +use crate::wallet::updater::{cancel_tx, refresh_output_state, retrieve_txs}; use crate::wallet::WalletConfig; -/// Wallet loaded from config. +/// Wallet instance and config wrapper. #[derive(Clone)] pub struct Wallet { - /// Identifier for a wallet, name of wallet directory. - id: OsString, - /// Base path for wallet data. - pub(crate) path: String, - /// Loaded file config. + /// Wallet instance, exists when wallet is open. + instance: Option, + /// Wallet data path. + path: String, + /// Wallet configuration. pub(crate) config: WalletConfig, } -impl Wallet { - /// Create new wallet from provided name. - pub fn create(name: String ) { +/// Wallet instance type. +type WalletInstance = Arc< + Mutex< + Box< + dyn WalletInst< + 'static, + DefaultLCProvider<'static, HTTPNodeClient, ExtKeychain>, + HTTPNodeClient, + ExtKeychain, + >, + >, + >, +>; +impl Wallet { + /// Create and open new wallet. + pub fn create( + name: String, + password: String, + mnemonic: String, + external_node_url: Option + ) -> Result { + let config = WalletConfig::create(name.clone(), external_node_url); + let wallet = Self::create_wallet_instance(config.clone())?; + let mut w_lock = wallet.lock(); + let p = w_lock.lc_provider()?; + + // Create wallet. + p.create_wallet(None, + Some(ZeroingString::from(mnemonic.clone())), + mnemonic.len(), + ZeroingString::from(password.clone()), + false, + )?; + + // Open wallet. + p.open_wallet(None, ZeroingString::from(password), false, false)?; + + let w = Wallet { + instance: Some(wallet.clone()), + path: config.get_data_path(), + config, + }; + Ok(w) } - /// Load wallet from provided data path. - pub fn load(data_path: PathBuf) -> Option { - if !data_path.is_dir() { - return None; - } + /// Initialize wallet from provided data path. + pub fn init(data_path: PathBuf) -> Option { let wallet_config = WalletConfig::load(data_path.clone()); if let Some(config) = wallet_config { - // Set id as wallet directory name. - let id = data_path.file_name().unwrap().to_os_string(); let path = data_path.to_str().unwrap().to_string(); - return Some(Self { id, path, config }); + return Some(Self { instance: None, path, config }); } None } - /// Get wallet node connection URL. - pub fn get_connection_url(&self) -> String { - match self.config.get_external_node_url() { - None => { - format!("http://{}", NodeConfig::get_api_address()) + /// Check if wallet is open (instance exists). + pub fn is_open(&self) -> bool { + self.instance.is_some() + } + + /// Create wallet instance from provided config. + fn create_wallet_instance(config: WalletConfig) -> Result { + // Assume global chain type has already been initialized. + let chain_type = AppConfig::chain_type(); + if !global::GLOBAL_CHAIN_TYPE.is_init() { + global::init_global_chain_type(chain_type); + } else { + global::set_global_chain_type(chain_type); + global::set_local_chain_type(chain_type); + } + + // Setup node client. + let (node_api_url, node_secret) = if let Some(url) = config.get_external_node_url() { + (url.to_string(), None) + } else { + (NodeConfig::get_api_address(), NodeConfig::get_api_secret()) + }; + let node_client = HTTPNodeClient::new(&node_api_url, node_secret)?; + + // Create wallet instance. + let wallet = Self::inst_wallet::< + DefaultLCProvider, + HTTPNodeClient, + ExtKeychain, + >(config, node_client)?; + Ok(wallet) + } + + /// Instantiate wallet from provided node client and config. + fn inst_wallet( + config: WalletConfig, + node_client: C, + ) -> Result>>>, Error> + where + DefaultWalletImpl<'static, C>: WalletInst<'static, L, C, K>, + L: WalletLCProvider<'static, C, K>, + C: NodeClient + 'static, + K: Keychain + 'static, + { + let mut wallet = Box::new(DefaultWalletImpl::<'static, C>::new(node_client).unwrap()) + as Box>; + let lc = wallet.lc_provider()?; + lc.set_top_level_directory(config.get_data_path().as_str())?; + Ok(Arc::new(Mutex::new(wallet))) + } + + /// Open wallet. + pub fn open_wallet(&mut self, password: ZeroingString) -> Result<(), Error> { + if let None = self.instance { + let wallet = Self::create_wallet_instance(self.config.clone())?; + let mut wallet_lock = wallet.lock(); + let lc = wallet_lock.lc_provider()?; + lc.open_wallet(None, password, false, false)?; + self.instance = Some(wallet.clone()); + } + Ok(()) + } + + /// Close wallet. + pub fn close_wallet(&self) -> Result<(), Error> { + if let Some(wallet) = &self.instance { + let mut wallet_lock = wallet.lock(); + let lc = wallet_lock.lc_provider()?; + lc.close_wallet(None)?; + } + Ok(()) + } + + /// Create transaction. + fn tx_create( + &self, + amount: u64, + minimum_confirmations: u64, + selection_strategy_is_use_all: bool, + ) -> Result<(Vec, String), Error> { + let wallet = self.instance.clone().ok_or(GenericError("Wallet was not open".to_string()))?; + let parent_key_id = { + wallet_lock!(wallet.clone(), w); + w.parent_key_id().clone() + }; + + let slate = { + wallet_lock!(wallet, w); + let mut slate = new_tx_slate(&mut **w, amount, false, 2, false, None)?; + let height = w.w2n_client().get_chain_tip()?.0; + + let context = add_inputs_to_slate( + &mut **w, + None, + &mut slate, + height, + minimum_confirmations, + 500, + 1, + selection_strategy_is_use_all, + &parent_key_id, + true, + false, + false, + )?; + + { + let mut batch = w.batch(None)?; + batch.save_private_context(slate.id.as_bytes(), &context)?; + batch.commit()?; + } + + lock_tx_context(&mut **w, None, &slate, height, &context, None)?; + slate.compact()?; + slate + }; + + let packer = Slatepacker::new(SlatepackerArgs { + sender: None, // sender + recipients: vec![], + dec_key: None, + }); + let slatepack = packer.create_slatepack(&slate)?; + let api = Owner::new(self.instance.clone().unwrap(), None); + let txs = api.retrieve_txs(None, false, None, Some(slate.id), None)?; + let result = ( + txs.1, + SlatepackArmor::encode(&slatepack)?, + ); + Ok(result) + } + + /// Callback to check slate compatibility at current node. + fn check_middleware( + name: ForeignCheckMiddlewareFn, + node_version_info: Option, + slate: Option<&Slate>, + ) -> Result<(), Error> { + match name { + ForeignCheckMiddlewareFn::BuildCoinbase => Ok(()), + _ => { + let mut bhv = 3; + if let Some(n) = node_version_info { + bhv = n.block_header_version; + } + if let Some(s) = slate { + if bhv > 4 + && s.version_info.block_header_version + < slate_versions::GRIN_BLOCK_HEADER_VERSION + { + Err(Error::Compatibility( + "Incoming Slate is not compatible with this wallet. \ + Please upgrade the node or use a different one." + .into(), + ))?; + } + } + Ok(()) } - Some(url) => url.to_string() } } + + /// Receive transaction. + fn tx_receive( + &self, + account: &str, + slate_armored: &str + ) -> Result<(Vec, String), Error> { + let wallet = self.instance.clone().ok_or(GenericError("Wallet was not open".to_string()))?; + let foreign_api = Foreign::new(wallet.clone(), None, Some(Self::check_middleware), false); + let owner_api = Owner::new(wallet, None); + + let mut slate = + owner_api.slate_from_slatepack_message(None, slate_armored.to_owned(), vec![0])?; + let slatepack = owner_api.decode_slatepack_message(None, slate_armored.to_owned(), vec![0])?; + + let _ret_address = slatepack.sender; + + slate = foreign_api.receive_tx(&slate, Some(&account), None)?; + let txs = owner_api.retrieve_txs(None, false, None, Some(slate.id), None)?; + let packer = Slatepacker::new(SlatepackerArgs { + sender: None, // sender + recipients: vec![], + dec_key: None, + }); + let slatepack = packer.create_slatepack(&slate)?; + let result = ( + txs.1, + SlatepackArmor::encode(&slatepack)?, + ); + Ok(result) + } + + /// Cancel transaction. + fn tx_cancel(&self, id: u32) -> Result { + let wallet = self.instance.clone().ok_or(GenericError("Wallet was not open".to_string()))?; + wallet_lock!(wallet, w); + let parent_key_id = w.parent_key_id(); + cancel_tx(&mut **w, None, &parent_key_id, Some(id), None)?; + Ok("".to_owned()) + } + + /// Get transaction info. + pub fn get_tx(&self, tx_slate_id: &str) -> Result<(bool, Vec), Error> { + let api = Owner::new(self.instance.clone().unwrap(), None); + let uuid = Uuid::parse_str(tx_slate_id).unwrap(); + let txs = api.retrieve_txs(None, true, None, Some(uuid), None)?; + Ok(txs) + } + + /// Finalize transaction. + fn tx_finalize(&self, slate_armored: &str) -> Result<(bool, Vec), Error> { + let wallet = self.instance.clone().ok_or(GenericError("Wallet was not open".to_string()))?; + let owner_api = Owner::new(wallet, None); + let mut slate = + owner_api.slate_from_slatepack_message(None, slate_armored.to_owned(), vec![0])?; + let slatepack = owner_api.decode_slatepack_message(None, slate_armored.to_owned(), vec![0])?; + + let _ret_address = slatepack.sender; + + slate = owner_api.finalize_tx(None, &slate)?; + let txs = owner_api.retrieve_txs(None, false, None, Some(slate.id), None)?; + Ok(txs) + } + + /// Post transaction to node for broadcasting. + fn tx_post(&self, tx_slate_id: &str) -> Result<(), Error> { + let wallet = self.instance.clone().ok_or(GenericError("Wallet was not open".to_string()))?; + let api = Owner::new(wallet, None); + let tx_uuid = Uuid::parse_str(tx_slate_id).unwrap(); + let (_, txs) = api.retrieve_txs(None, true, None, Some(tx_uuid.clone()), None)?; + if txs[0].confirmed { + return Err(Error::from(GenericError(format!( + "Transaction with id {} is already confirmed. Not posting.", + tx_slate_id + )))); + } + let stored_tx = api.get_stored_tx(None, None, Some(&tx_uuid))?; + match stored_tx { + Some(stored_tx) => { + api.post_tx(None, &stored_tx, true)?; + Ok(()) + } + None => Err(Error::from(GenericError(format!( + "Transaction with id {} does not have transaction data. Not posting.", + tx_slate_id + )))), + } + } + + /// Get transactions and base wallet info. + pub fn get_txs_info( + &self, + minimum_confirmations: u64 + ) -> Result<(bool, Vec, WalletInfo), Error> { + let wallet = self.instance.clone().ok_or(GenericError("Wallet was not open".to_string()))?; + let refreshed = Self::update_state(wallet.clone()).unwrap_or(false); + let wallet_info = { + wallet_lock!(wallet, w); + let parent_key_id = w.parent_key_id(); + Self::get_info(&mut **w, &parent_key_id, minimum_confirmations)? + }; + let api = Owner::new(wallet, None); + + let txs = api.retrieve_txs(None, false, None, None, None)?; + Ok((refreshed, txs.1, wallet_info)) + } + + /// Update wallet instance state. + fn update_state<'a, L, C, K>( + wallet_inst: Arc>>>, + ) -> Result + where + L: WalletLCProvider<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, + { + let parent_key_id = { + wallet_lock!(wallet_inst, w); + w.parent_key_id().clone() + }; + let mut client = { + wallet_lock!(wallet_inst, w); + w.w2n_client().clone() + }; + let tip = client.get_chain_tip()?; + + // Step1: Update outputs and transactions purely based on UTXO state. + { + wallet_lock!(wallet_inst, w); + if !match refresh_output_state(&mut **w, None, tip.0, &parent_key_id, true) { + Ok(_) => true, + Err(_) => false, + } { + // We are unable to contact the node. + return Ok(false); + } + } + + let mut txs = { + wallet_lock!(wallet_inst, w); + retrieve_txs(&mut **w, None, None, None, Some(&parent_key_id), true)? + }; + + for tx in txs.iter_mut() { + // Step 2: Cancel any transactions with an expired TTL. + if let Some(e) = tx.ttl_cutoff_height { + if tip.0 >= e { + wallet_lock!(wallet_inst, w); + let parent_key_id = w.parent_key_id(); + cancel_tx(&mut **w, None, &parent_key_id, Some(tx.id), None)?; + continue; + } + } + // Step 3: Update outstanding transactions with no change outputs by kernel. + if tx.confirmed { + continue; + } + if tx.amount_debited != 0 && tx.amount_credited != 0 { + continue; + } + if let Some(e) = tx.kernel_excess { + let res = client.get_kernel(&e, tx.kernel_lookup_min_height, Some(tip.0)); + let kernel = match res { + Ok(k) => k, + Err(_) => return Ok(false), + }; + if let Some(k) = kernel { + debug!("Kernel Retrieved: {:?}", k); + wallet_lock!(wallet_inst, w); + let mut batch = w.batch(None)?; + tx.confirmed = true; + tx.update_confirmation_ts(); + batch.save_tx_log_entry(tx.clone(), &parent_key_id)?; + batch.commit()?; + } + } + } + + return Ok(true); + } + + /// Get summary info about the wallet. + pub fn get_info<'a, T: ?Sized, C, K>( + wallet: &mut T, + parent_key_id: &Identifier, + minimum_confirmations: u64, + ) -> Result + where + T: WalletBackend<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, + { + let current_height = wallet.last_confirmed_height()?; + let outputs = wallet + .iter() + .filter(|out| out.root_key_id == *parent_key_id); + + let mut unspent_total = 0; + let mut immature_total = 0; + let mut awaiting_finalization_total = 0; + let mut unconfirmed_total = 0; + let mut locked_total = 0; + let mut reverted_total = 0; + + for out in outputs { + match out.status { + OutputStatus::Unspent => { + if out.is_coinbase && out.lock_height > current_height { + immature_total += out.value; + } else if out.num_confirmations(current_height) < minimum_confirmations { + // Treat anything less than minimum confirmations as "unconfirmed". + unconfirmed_total += out.value; + } else { + unspent_total += out.value; + } + } + OutputStatus::Unconfirmed => { + // We ignore unconfirmed coinbase outputs completely. + if !out.is_coinbase { + if minimum_confirmations == 0 { + unconfirmed_total += out.value; + } else { + awaiting_finalization_total += out.value; + } + } + } + OutputStatus::Locked => { + locked_total += out.value; + } + OutputStatus::Reverted => reverted_total += out.value, + OutputStatus::Spent => {} + } + } + + Ok(WalletInfo { + last_confirmed_height: current_height, + minimum_confirmations, + total: unspent_total + unconfirmed_total + immature_total, + amount_awaiting_finalization: awaiting_finalization_total, + amount_awaiting_confirmation: unconfirmed_total, + amount_immature: immature_total, + amount_locked: locked_total, + amount_currently_spendable: unspent_total, + amount_reverted: reverted_total, + }) + } } \ No newline at end of file