commit da288f01395169da4759ca030889fcc6b7c0bbeb Author: yeastplume Date: Wed Feb 13 15:05:19 2019 +0000 initial commit diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..0f4d7b5 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2665 @@ +[[package]] +name = "adler32" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "aho-corasick" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "ansi_term" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "antidote" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "argon2rs" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "blake2-rfc 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)", + "scoped_threadpool 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "arrayref" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "arrayvec" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "nodrop 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)", + "odds 0.2.26 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "arrayvec" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "nodrop 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "atty" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)", + "termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "autocfg" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "backtrace" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "autocfg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "backtrace-sys 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-demangle 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "backtrace-sys" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cc 1.0.29 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "base64" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "safemem 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "bindgen" +version = "0.37.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cexpr 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "clang-sys 0.23.0 (registry+https://github.com/rust-lang/crates.io-index)", + "clap 2.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "env_logger 0.5.13 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "peeking_take_while 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "which 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "bitflags" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "bitflags" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "blake2-rfc" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "arrayvec 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)", + "constant_time_eq 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "block-buffer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "arrayref 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", + "byte-tools 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "build_const" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "built" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "git2 0.7.5 (registry+https://github.com/rust-lang/crates.io-index)", + "semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", + "toml 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "byte-tools" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "byteorder" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "bytes" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "cc" +version = "1.0.29" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "cexpr" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "nom 3.2.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "cfg-if" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "chrono" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-integer 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.87 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "clang-sys" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)", + "libloading 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "clap" +version = "2.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", + "atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "textwrap 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "yaml-rust 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "cloudabi" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "constant_time_eq" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "crc" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "build_const 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "crc32fast" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "croaring" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "croaring-sys 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "croaring-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bindgen 0.37.4 (registry+https://github.com/rust-lang/crates.io-index)", + "cc 1.0.29 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "crossbeam" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "crossbeam" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "crossbeam-channel 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "crossbeam-deque 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)", + "crossbeam-epoch 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "crossbeam-utils 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "num_cpus 1.10.0 (registry+https://github.com/rust-lang/crates.io-index)", + "parking_lot 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "crossbeam-channel" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "crossbeam-utils 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", + "smallvec 0.6.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "crossbeam-deque" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "crossbeam-epoch 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "crossbeam-utils 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "arrayvec 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "crossbeam-utils 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "memoffset 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "scopeguard 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "crossbeam-utils" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "crypto-mac" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "constant_time_eq 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "generic-array 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "csv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "ct-logs" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "sct 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "ctrlc" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "nix 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "difference" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "digest" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "generic-array 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "dirs" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_users 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "dtoa" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "encode_unicode" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "enum_primitive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-traits 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "env_logger" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "humantime 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "termcolor 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "failure" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "backtrace 0.3.13 (registry+https://github.com/rust-lang/crates.io-index)", + "failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "failure_derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.15.26 (registry+https://github.com/rust-lang/crates.io-index)", + "synstructure 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "fake-simd" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "flate2" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "crc32fast 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)", + "miniz-sys 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", + "miniz_oxide_c_api 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "fnv" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "fuchsia-zircon" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "fuchsia-zircon-sys" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "futures" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "futures-cpupool" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", + "num_cpus 1.10.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "gcc" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "generic-array" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "typenum 1.10.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "git2" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)", + "libgit2-sys 0.7.11 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "glob" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "grin_api" +version = "1.1.0" +dependencies = [ + "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", + "grin_chain 1.1.0", + "grin_core 1.1.0", + "grin_p2p 1.1.0", + "grin_pool 1.1.0", + "grin_store 1.1.0", + "grin_util 1.1.0", + "http 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)", + "hyper 0.12.19 (registry+https://github.com/rust-lang/crates.io-index)", + "hyper-rustls 0.14.0 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "ring 0.13.5 (registry+https://github.com/rust-lang/crates.io-index)", + "rustls 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.87 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.87 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.38 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-core 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-rustls 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-tcp 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "grin_chain" +version = "1.1.0" +dependencies = [ + "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "chrono 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "croaring 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "grin_core 1.1.0", + "grin_keychain 1.1.0", + "grin_store 1.1.0", + "grin_util 1.1.0", + "lmdb-zero 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "lru-cache 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.87 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.87 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "grin_core" +version = "1.1.0" +dependencies = [ + "blake2-rfc 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)", + "byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "chrono 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "croaring 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "enum_primitive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "grin_keychain 1.1.0", + "grin_util 1.1.0", + "lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "lru-cache 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "num 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "num-bigint 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.87 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.87 (registry+https://github.com/rust-lang/crates.io-index)", + "siphasher 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "uuid 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "grin_keychain" +version = "1.1.0" +dependencies = [ + "blake2-rfc 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)", + "byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "digest 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)", + "grin_util 1.1.0", + "hmac 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "pbkdf2 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)", + "ripemd160 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.87 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.87 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.38 (registry+https://github.com/rust-lang/crates.io-index)", + "sha2 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "uuid 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "grin_libwallet" +version = "1.1.0" +dependencies = [ + "blake2-rfc 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)", + "chrono 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "grin_core 1.1.0", + "grin_keychain 1.1.0", + "grin_util 1.1.0", + "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.87 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.87 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.38 (registry+https://github.com/rust-lang/crates.io-index)", + "uuid 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "grin_p2p" +version = "1.1.0" +dependencies = [ + "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", + "chrono 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "enum_primitive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "grin_core 1.1.0", + "grin_store 1.1.0", + "grin_util 1.1.0", + "lmdb-zero 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)", + "num 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.87 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.87 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "grin_pool" +version = "1.1.0" +dependencies = [ + "blake2-rfc 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)", + "chrono 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "grin_core 1.1.0", + "grin_keychain 1.1.0", + "grin_store 1.1.0", + "grin_util 1.1.0", + "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.87 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.87 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "grin_secp256k1zkp" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "arrayvec 0.3.25 (registry+https://github.com/rust-lang/crates.io-index)", + "gcc 0.3.55 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.87 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.38 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "grin_store" +version = "1.1.0" +dependencies = [ + "byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "croaring 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "env_logger 0.5.13 (registry+https://github.com/rust-lang/crates.io-index)", + "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "grin_core 1.1.0", + "grin_util 1.1.0", + "libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)", + "lmdb-zero 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "memmap 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.87 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.87 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "grin_util" +version = "1.1.0" +dependencies = [ + "backtrace 0.3.13 (registry+https://github.com/rust-lang/crates.io-index)", + "base64 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)", + "byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "grin_secp256k1zkp 0.7.4 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "log4rs 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "parking_lot 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.87 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.87 (registry+https://github.com/rust-lang/crates.io-index)", + "walkdir 2.2.7 (registry+https://github.com/rust-lang/crates.io-index)", + "zeroize 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", + "zip 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "grin_wallet" +version = "1.1.0" +dependencies = [ + "blake2-rfc 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)", + "built 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "chrono 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "clap 2.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "ctrlc 3.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", + "grin_api 1.1.0", + "grin_chain 1.1.0", + "grin_core 1.1.0", + "grin_keychain 1.1.0", + "grin_store 1.1.0", + "grin_util 1.1.0", + "hyper 0.12.19 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "prettytable-rs 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)", + "ring 0.13.5 (registry+https://github.com/rust-lang/crates.io-index)", + "rpassword 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.87 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.87 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.38 (registry+https://github.com/rust-lang/crates.io-index)", + "term 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-core 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-retry 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)", + "uuid 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "grin_wallet_config" +version = "1.1.0" +dependencies = [ + "dirs 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "grin_core 1.1.0", + "grin_util 1.1.0", + "pretty_assertions 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.87 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.87 (registry+https://github.com/rust-lang/crates.io-index)", + "toml 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "h2" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", + "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", + "http 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)", + "indexmap 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "string 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-io 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "hmac" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "crypto-mac 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", + "digest 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "http" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bytes 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", + "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "httparse" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "humantime" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "hyper" +version = "0.12.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bytes 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-cpupool 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", + "h2 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", + "http 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)", + "httparse 1.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-executor 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-io 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-reactor 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-tcp 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-threadpool 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-timer 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", + "want 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "hyper-rustls" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "ct-logs 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", + "http 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)", + "hyper 0.12.19 (registry+https://github.com/rust-lang/crates.io-index)", + "rustls 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-core 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-io 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-rustls 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-tcp 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "webpki 0.18.1 (registry+https://github.com/rust-lang/crates.io-index)", + "webpki-roots 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "idna" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-normalization 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "indexmap" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "iovec" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "itoa" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "kernel32-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "lazy_static" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "lazycell" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "libc" +version = "0.2.48" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "libgit2-sys" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cc 1.0.29 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)", + "libz-sys 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)", + "pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "liblmdb-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "gcc 0.3.55 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "libloading" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cc 1.0.29 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "libz-sys" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cc 1.0.29 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)", + "pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", + "vcpkg 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "linked-hash-map" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "linked-hash-map" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "lmdb-zero" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)", + "liblmdb-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "supercow 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "lock_api" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "owning_ref 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "scopeguard 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "log" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.87 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "log-mdc" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "log4rs" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "antidote 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "chrono 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "crossbeam 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", + "flate2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "humantime 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "log-mdc 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.87 (registry+https://github.com/rust-lang/crates.io-index)", + "serde-value 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.87 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.38 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_yaml 0.8.8 (registry+https://github.com/rust-lang/crates.io-index)", + "thread-id 3.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "typemap 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "lru-cache" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "linked-hash-map 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "matches" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "memchr" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "memchr" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "memmap" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "memoffset" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "miniz-sys" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cc 1.0.29 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "miniz_oxide" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "adler32 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "miniz_oxide_c_api" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cc 1.0.29 (registry+https://github.com/rust-lang/crates.io-index)", + "crc 1.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)", + "miniz_oxide 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "mio" +version = "0.6.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "lazycell 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "miow 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)", + "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "mio-uds" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)", + "mio 0.6.16 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "miow" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", + "ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "msdos_time" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "net2" +version = "0.2.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "nix" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "cc 1.0.29 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)", + "void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "nodrop" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "nom" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "memchr 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-bigint 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)", + "num-complex 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)", + "num-integer 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)", + "num-iter 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", + "num-rational 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-bigint 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "num-complex 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "num-integer 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)", + "num-iter 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", + "num-rational 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-bigint" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-integer 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-bigint" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-integer 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-complex" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-complex" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-integer" +version = "0.1.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-iter" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-integer 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-rational" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-bigint 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)", + "num-integer 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-rational" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-bigint 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "num-integer 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-traits" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-traits" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "num_cpus" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "odds" +version = "0.2.26" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "ordered-float" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "owning_ref" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "stable_deref_trait 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "parking_lot" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "lock_api 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "parking_lot_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "parking_lot" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "lock_api 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "parking_lot_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "parking_lot_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "smallvec 0.6.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "parking_lot_core" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "smallvec 0.6.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "pbkdf2" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "base64 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)", + "byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "constant_time_eq 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "crypto-mac 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", + "generic-array 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", + "hmac 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)", + "sha2 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "percent-encoding" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "pkg-config" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "podio" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "pretty_assertions" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", + "difference 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "prettytable-rs" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "csv 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)", + "encode_unicode 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "term 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "proc-macro2" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "proc-macro2" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "quick-error" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "quote" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "quote" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "autocfg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_chacha 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_hc 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_isaac 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_jitter 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_os 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_pcg 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_xorshift 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand_chacha" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "autocfg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand_core" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "rand_hc" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand_isaac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand_jitter" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand_os" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand_pcg" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand_xorshift" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "redox_syscall" +version = "0.1.51" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "redox_termios" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "redox_syscall 0.1.51 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "redox_users" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "argon2rs 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", + "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_syscall 0.1.51 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "regex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "aho-corasick 0.6.9 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "regex-syntax 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", + "thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "utf8-ranges 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "regex-syntax" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "ucd-util 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "ring" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cc 1.0.29 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)", + "untrusted 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "ripemd160" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "block-buffer 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "byte-tools 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "digest 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rpassword" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "rustc-serialize" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rustls" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "base64 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "ring 0.13.5 (registry+https://github.com/rust-lang/crates.io-index)", + "sct 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "untrusted 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", + "webpki 0.18.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "ryu" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "safemem" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "same-file" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "scoped-tls" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "scoped_threadpool" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "scopeguard" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "sct" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "ring 0.13.5 (registry+https://github.com/rust-lang/crates.io-index)", + "untrusted 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "serde" +version = "1.0.87" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "serde-value" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "ordered-float 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.87 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "serde_derive" +version = "1.0.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.15.26 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "serde_json" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", + "ryu 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.87 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "serde_yaml" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "dtoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", + "linked-hash-map 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.87 (registry+https://github.com/rust-lang/crates.io-index)", + "yaml-rust 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "sha2" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "block-buffer 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "byte-tools 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "digest 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)", + "fake-simd 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "siphasher" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "slab" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "smallvec" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unreachable 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "stable_deref_trait" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "string" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "strsim" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "supercow" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "syn" +version = "0.15.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "synstructure" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.15.26 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "term" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "termcolor" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "wincolor 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "termion" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_syscall 0.1.51 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "textwrap" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "thread-id" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_syscall 0.1.51 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "thread_local" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "time" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_syscall 0.1.51 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tokio" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bytes 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", + "mio 0.6.16 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-codec 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-current-thread 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-executor 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-fs 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-io 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-reactor 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-tcp 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-threadpool 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-timer 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-udp 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-uds 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tokio-codec" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bytes 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-io 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tokio-core" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bytes 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", + "iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "mio 0.6.16 (registry+https://github.com/rust-lang/crates.io-index)", + "scoped-tls 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-executor 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-io 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-reactor 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-timer 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tokio-current-thread" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-executor 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tokio-executor" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "crossbeam-utils 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tokio-fs" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-io 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-threadpool 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tokio-io" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bytes 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tokio-reactor" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "crossbeam-utils 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "mio 0.6.16 (registry+https://github.com/rust-lang/crates.io-index)", + "num_cpus 1.10.0 (registry+https://github.com/rust-lang/crates.io-index)", + "parking_lot 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-executor 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-io 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tokio-retry" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.3.23 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-core 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-service 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tokio-rustls" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rustls 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", + "webpki 0.18.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tokio-service" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tokio-tcp" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bytes 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", + "iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "mio 0.6.16 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-io 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-reactor 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tokio-threadpool" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "crossbeam 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "crossbeam-channel 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "crossbeam-deque 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)", + "crossbeam-utils 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "num_cpus 1.10.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", + "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-executor 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tokio-timer" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "crossbeam-utils 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", + "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-executor 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tokio-udp" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bytes 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "mio 0.6.16 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-codec 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-io 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-reactor 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tokio-uds" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bytes 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", + "iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "mio 0.6.16 (registry+https://github.com/rust-lang/crates.io-index)", + "mio-uds 0.6.7 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-codec 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-io 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-reactor 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "toml" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "serde 1.0.87 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "traitobject" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "try-lock" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "typemap" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unsafe-any 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "typenum" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "ucd-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "unicode-bidi" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "unicode-normalization" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "smallvec 0.6.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "unicode-width" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "unicode-xid" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "unreachable" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "unsafe-any" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "traitobject 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "untrusted" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "url" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "idna 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", + "percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "utf8-ranges" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "uuid" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.87 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "vcpkg" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "vec_map" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "walkdir" +version = "2.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "same-file 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "want" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "try-lock 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "webpki" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "ring 0.13.5 (registry+https://github.com/rust-lang/crates.io-index)", + "untrusted 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "webpki-roots" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "untrusted 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", + "webpki 0.18.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "which" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "winapi-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "wincolor" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "ws2_32-sys" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "yaml-rust" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "yaml-rust" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "linked-hash-map 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "zeroize" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "zip" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "msdos_time 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "podio 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[metadata] +"checksum adler32 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7e522997b529f05601e05166c07ed17789691f562762c7f3b987263d2dedee5c" +"checksum aho-corasick 0.6.9 (registry+https://github.com/rust-lang/crates.io-index)" = "1e9a933f4e58658d7b12defcf96dc5c720f20832deebe3e0a19efd3b6aaeeb9e" +"checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +"checksum antidote 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "34fde25430d87a9388dadbe6e34d7f72a462c8b43ac8d309b42b0a8505d7e2a5" +"checksum argon2rs 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "3f67b0b6a86dae6e67ff4ca2b6201396074996379fba2b92ff649126f37cb392" +"checksum arrayref 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "0d382e583f07208808f6b1249e60848879ba3543f57c32277bf52d69c2f0f0ee" +"checksum arrayvec 0.3.25 (registry+https://github.com/rust-lang/crates.io-index)" = "06f59fe10306bb78facd90d28c2038ad23ffaaefa85bac43c8a434cde383334f" +"checksum arrayvec 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)" = "92c7fb76bc8826a8b33b4ee5bb07a247a81e76764ab4d55e8f73e3a4d8808c71" +"checksum atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "9a7d5b8723950951411ee34d271d99dddcc2035a16ab25310ea2c8cfd4369652" +"checksum autocfg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a6d640bee2da49f60a4068a7fae53acde8982514ab7bae8b8cea9e88cbcfd799" +"checksum backtrace 0.3.13 (registry+https://github.com/rust-lang/crates.io-index)" = "b5b493b66e03090ebc4343eb02f94ff944e0cbc9ac6571491d170ba026741eb5" +"checksum backtrace-sys 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)" = "797c830ac25ccc92a7f8a7b9862bde440715531514594a6154e3d4a54dd769b6" +"checksum base64 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)" = "489d6c0ed21b11d038c31b6ceccca973e65d73ba3bd8ecb9a2babf5546164643" +"checksum bindgen 0.37.4 (registry+https://github.com/rust-lang/crates.io-index)" = "1b25ab82877ea8fe6ce1ce1f8ac54361f0218bad900af9eb11803994bf67c221" +"checksum bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4efd02e230a02e18f92fc2735f44597385ed02ad8f831e7c1c1156ee5e1ab3a5" +"checksum bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "228047a76f468627ca71776ecdebd732a3423081fcf5125585bcd7c49886ce12" +"checksum blake2-rfc 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)" = "5d6d530bdd2d52966a6d03b7a964add7ae1a288d25214066fd4b600f0f796400" +"checksum block-buffer 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a076c298b9ecdb530ed9d967e74a6027d6a7478924520acddcddc24c1c8ab3ab" +"checksum build_const 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "39092a32794787acd8525ee150305ff051b0aa6cc2abaf193924f5ab05425f39" +"checksum built 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "61f5aae2fa15b68fbcf0cbab64e659a55d10e9bacc55d3470ef77ae73030d755" +"checksum byte-tools 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "560c32574a12a89ecd91f5e742165893f86e3ab98d21f8ea548658eb9eef5f40" +"checksum byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a019b10a2a7cdeb292db131fc8113e57ea2a908f6e7894b0c3c671893b65dbeb" +"checksum bytes 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)" = "40ade3d27603c2cb345eb0912aec461a6dec7e06a4ae48589904e808335c7afa" +"checksum cc 1.0.29 (registry+https://github.com/rust-lang/crates.io-index)" = "4390a3b5f4f6bce9c1d0c00128379df433e53777fdd30e92f16a529332baec4e" +"checksum cexpr 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "42aac45e9567d97474a834efdee3081b3c942b2205be932092f53354ce503d6c" +"checksum cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "082bb9b28e00d3c9d39cc03e64ce4cea0f1bb9b3fde493f0cbc008472d22bdf4" +"checksum chrono 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "45912881121cb26fad7c38c17ba7daa18764771836b34fab7d3fbd93ed633878" +"checksum clang-sys 0.23.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d7f7c04e52c35222fffcc3a115b5daf5f7e2bfb71c13c4e2321afe1fc71859c2" +"checksum clap 2.32.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b957d88f4b6a63b9d70d5f454ac8011819c6efa7727858f458ab71c756ce2d3e" +"checksum cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" +"checksum constant_time_eq 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "8ff012e225ce166d4422e0e78419d901719760f62ae2b7969ca6b564d1b54a9e" +"checksum crc 1.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d663548de7f5cca343f1e0a48d14dcfb0e9eb4e079ec58883b7251539fa10aeb" +"checksum crc32fast 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e91d5240c6975ef33aeb5f148f35275c25eda8e8a5f95abe421978b05b8bf192" +"checksum croaring 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "d2b350ece8a9ba71eeb9c068a98a86dc420ca5c1d6bd4e1627a4581e9c843c38" +"checksum croaring-sys 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "546b00f33bdf591bce6410a8dca65047d126b1d5a9189190d085aa8c493d43a7" +"checksum crossbeam 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "24ce9782d4d5c53674646a6a4c1863a21a8fc0cb649b3c94dfc16e45071dea19" +"checksum crossbeam 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ad4c7ea749d9fb09e23c5cb17e3b70650860553a0e2744e38446b1803bf7db94" +"checksum crossbeam-channel 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "0f0ed1a4de2235cabda8558ff5840bffb97fcb64c97827f354a451307df5f72b" +"checksum crossbeam-deque 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)" = "05e44b8cf3e1a625844d1750e1f7820da46044ff6d28f4d43e455ba3e5bb2c13" +"checksum crossbeam-epoch 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "04c9e3102cc2d69cd681412141b390abd55a362afc1540965dad0ad4d34280b4" +"checksum crossbeam-utils 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)" = "f8306fcef4a7b563b76b7dd949ca48f52bc1141aa067d2ea09565f3e2652aa5c" +"checksum crypto-mac 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7afa06d05a046c7a47c3a849907ec303504608c927f4e85f7bfff22b7180d971" +"checksum csv 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7ef22b37c7a51c564a365892c012dc0271221fdcc64c69b19ba4d6fa8bd96d9c" +"checksum ct-logs 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "95a4bf5107667e12bf6ce31a3a5066d67acc88942b6742117a41198734aaccaa" +"checksum ctrlc 3.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "630391922b1b893692c6334369ff528dcc3a9d8061ccf4c803aa8f83cb13db5e" +"checksum difference 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" +"checksum digest 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)" = "03b072242a8cbaf9c145665af9d250c59af3b958f83ed6824e13533cf76d5b90" +"checksum dirs 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "88972de891f6118092b643d85a0b28e0678e0f948d7f879aa32f2d5aafe97d2a" +"checksum dtoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6d301140eb411af13d3115f9a562c85cc6b541ade9dfa314132244aaee7489dd" +"checksum encode_unicode 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "90b2c9496c001e8cb61827acdefad780795c42264c137744cae6f7d9e3450abd" +"checksum enum_primitive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "be4551092f4d519593039259a9ed8daedf0da12e5109c5280338073eaeb81180" +"checksum env_logger 0.5.13 (registry+https://github.com/rust-lang/crates.io-index)" = "15b0a4d2e39f8420210be8b27eeda28029729e2fd4291019455016c348240c38" +"checksum failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "795bd83d3abeb9220f257e597aa0080a508b27533824adf336529648f6abf7e2" +"checksum failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "ea1063915fd7ef4309e222a5a07cf9c319fb9c7836b1f89b85458672dbb127e1" +"checksum fake-simd 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" +"checksum flate2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "2291c165c8e703ee54ef3055ad6188e3d51108e2ded18e9f2476e774fc5ad3d4" +"checksum fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3" +"checksum fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" +"checksum fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" +"checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" +"checksum futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)" = "49e7653e374fe0d0c12de4250f0bdb60680b8c80eed558c5c7538eec9c89e21b" +"checksum futures-cpupool 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "ab90cde24b3319636588d0c35fe03b1333857621051837ed769faefb4c2162e4" +"checksum gcc 0.3.55 (registry+https://github.com/rust-lang/crates.io-index)" = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" +"checksum generic-array 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ef25c5683767570c2bbd7deba372926a55eaae9982d7726ee2a1050239d45b9d" +"checksum git2 0.7.5 (registry+https://github.com/rust-lang/crates.io-index)" = "591f8be1674b421644b6c030969520bc3fa12114d2eb467471982ed3e9584e71" +"checksum glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "8be18de09a56b60ed0edf84bc9df007e30040691af7acd1c41874faac5895bfb" +"checksum grin_secp256k1zkp 0.7.4 (registry+https://github.com/rust-lang/crates.io-index)" = "223095ed6108a42855ab2ce368d2056cfd384705f81c494f6d88ab3383161562" +"checksum h2 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "ddb2b25a33e231484694267af28fec74ac63b5ccf51ee2065a5e313b834d836e" +"checksum hmac 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)" = "733e1b3ac906631ca01ebb577e9bb0f5e37a454032b9036b5eaea4013ed6f99a" +"checksum http 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)" = "1a10e5b573b9a0146545010f50772b9e8b1dd0a256564cc4307694c68832a2f5" +"checksum httparse 1.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e8734b0cfd3bc3e101ec59100e101c2eecd19282202e87808b3037b442777a83" +"checksum humantime 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3ca7e5f2e110db35f93b837c81797f3714500b81d517bf20c431b16d3ca4f114" +"checksum hyper 0.12.19 (registry+https://github.com/rust-lang/crates.io-index)" = "f1ebec079129e43af5e234ef36ee3d7e6085687d145b7ea653b262d16c6b65f1" +"checksum hyper-rustls 0.14.0 (registry+https://github.com/rust-lang/crates.io-index)" = "68f2aa6b1681795bf4da8063f718cd23145aa0c9a5143d9787b345aa60d38ee4" +"checksum idna 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e" +"checksum indexmap 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7e81a7c05f79578dbc15793d8b619db9ba32b4577003ef3af1a91c416798c58d" +"checksum iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dbe6e417e7d0975db6512b90796e8ce223145ac4e33c377e4a42882a0e88bb08" +"checksum itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "1306f3464951f30e30d12373d31c79fbd52d236e5e896fd92f96ec7babbbe60b" +"checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +"checksum lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a374c89b9db55895453a74c1e38861d9deec0b01b405a82516e9d5de4820dea1" +"checksum lazycell 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b294d6fa9ee409a054354afc4352b0b9ef7ca222c69b8812cbea9e7d2bf3783f" +"checksum libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)" = "e962c7641008ac010fa60a7dfdc1712449f29c44ef2d4702394aea943ee75047" +"checksum libgit2-sys 0.7.11 (registry+https://github.com/rust-lang/crates.io-index)" = "48441cb35dc255da8ae72825689a95368bf510659ae1ad55dc4aa88cb1789bf1" +"checksum liblmdb-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "feed38a3a580f60bf61aaa067b0ff4123395966839adeaf67258a9e50c4d2e49" +"checksum libloading 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9c3ad660d7cb8c5822cd83d10897b0f1f1526792737a179e73896152f85b88c2" +"checksum libz-sys 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)" = "2eb5e43362e38e2bca2fd5f5134c4d4564a23a5c28e9b95411652021a8675ebe" +"checksum linked-hash-map 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7860ec297f7008ff7a1e3382d7f7e1dcd69efc94751a2284bafc3d013c2aa939" +"checksum linked-hash-map 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "70fb39025bc7cdd76305867c4eccf2f2dcf6e9a57f5b21a93e1c2d86cd03ec9e" +"checksum lmdb-zero 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "13416eee745b087c22934f35f1f24da22da41ba2a5ce197143d168ce055cc58d" +"checksum lock_api 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "62ebf1391f6acad60e5c8b43706dde4582df75c06698ab44511d15016bc2442c" +"checksum log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c84ec4b527950aa83a329754b01dbe3f58361d1c5efacd1f6d68c494d08a17c6" +"checksum log-mdc 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a94d21414c1f4a51209ad204c1776a3d0765002c76c6abcb602a6f09f1e881c7" +"checksum log4rs 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "25e0fc8737a634116a2deb38d821e4400ed16ce9dcb0d628a978d399260f5902" +"checksum lru-cache 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4d06ff7ff06f729ce5f4e227876cb88d10bc59cd4ae1e09fbb2bde15c850dc21" +"checksum matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" +"checksum memchr 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "148fab2e51b4f1cfc66da2a7c32981d1d3c083a803978268bb11fe4b86925e7a" +"checksum memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2efc7bc57c883d4a4d6e3246905283d8dae951bb3bd32f49d6ef297f546e1c39" +"checksum memmap 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6585fd95e7bb50d6cc31e20d4cf9afb4e2ba16c5846fc76793f11218da9c475b" +"checksum memoffset 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0f9dc261e2b62d7a622bf416ea3c5245cdd5d9a7fcc428c0d06804dfce1775b3" +"checksum miniz-sys 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)" = "0300eafb20369952951699b68243ab4334f4b10a88f411c221d444b36c40e649" +"checksum miniz_oxide 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c468f2369f07d651a5d0bb2c9079f8488a66d5466efe42d0c5c6466edcb7f71e" +"checksum miniz_oxide_c_api 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b7fe927a42e3807ef71defb191dc87d4e24479b221e67015fe38ae2b7b447bab" +"checksum mio 0.6.16 (registry+https://github.com/rust-lang/crates.io-index)" = "71646331f2619b1026cc302f87a2b8b648d5c6dd6937846a16cc8ce0f347f432" +"checksum mio-uds 0.6.7 (registry+https://github.com/rust-lang/crates.io-index)" = "966257a94e196b11bb43aca423754d87429960a768de9414f3691d6957abf125" +"checksum miow 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f2f3b1cf331de6896aabf6e9d55dca90356cc9960cca7eaaf408a355ae919" +"checksum msdos_time 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "aad9dfe950c057b1bfe9c1f2aa51583a8468ef2a5baba2ebbe06d775efeb7729" +"checksum net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)" = "42550d9fb7b6684a6d404d9fa7250c2eb2646df731d1c06afc06dcee9e1bcf88" +"checksum nix 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d37e713a259ff641624b6cb20e3b12b2952313ba36b6823c0f16e6cfd9e5de17" +"checksum nodrop 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "2f9667ddcc6cc8a43afc9b7917599d7216aa09c463919ea32c59ed6cac8bc945" +"checksum nom 3.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "05aec50c70fd288702bcd93284a8444607f3292dbdf2a30de5ea5dcdbe72287b" +"checksum num 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "4703ad64153382334aa8db57c637364c322d3372e097840c72000dabdcf6156e" +"checksum num 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "cf4825417e1e1406b3782a8ce92f4d53f26ec055e3622e1881ca8e9f5f9e08db" +"checksum num-bigint 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)" = "e63899ad0da84ce718c14936262a41cee2c79c981fc0a0e7c7beb47d5a07e8c1" +"checksum num-bigint 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "57450397855d951f1a41305e54851b1a7b8f5d2e349543a02a2effe25459f718" +"checksum num-complex 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)" = "b288631d7878aaf59442cffd36910ea604ecd7745c36054328595114001c9656" +"checksum num-complex 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "107b9be86cd2481930688277b675b0114578227f034674726605b8a482d8baf8" +"checksum num-integer 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)" = "e83d528d2677f0518c570baf2b7abdcf0cd2d248860b68507bdcb3e91d4c0cea" +"checksum num-iter 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)" = "af3fdbbc3291a5464dc57b03860ec37ca6bf915ed6ee385e7c6c052c422b2124" +"checksum num-rational 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "ee314c74bd753fc86b4780aa9475da469155f3848473a261d2d18e35245a784e" +"checksum num-rational 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4e96f040177bb3da242b5b1ecf3f54b5d5af3efbbfb18608977a5d2767b22f10" +"checksum num-traits 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)" = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31" +"checksum num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0b3a5d7cc97d6d30d8b9bc8fa19bf45349ffe46241e8816f50f62f6d6aaabee1" +"checksum num_cpus 1.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1a23f0ed30a54abaa0c7e83b1d2d87ada7c3c23078d1d87815af3e3b6385fbba" +"checksum odds 0.2.26 (registry+https://github.com/rust-lang/crates.io-index)" = "4eae0151b9dacf24fcc170d9995e511669a082856a91f958a2fe380bfab3fb22" +"checksum ordered-float 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2f0015e9e8e28ee20c581cfbfe47c650cedeb9ed0721090e0b7ebb10b9cdbcc2" +"checksum owning_ref 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "49a4b8ea2179e6a2e27411d3bca09ca6dd630821cf6894c6c7c8467a8ee7ef13" +"checksum parking_lot 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)" = "f0802bff09003b291ba756dc7e79313e51cc31667e94afbe847def490424cde5" +"checksum parking_lot 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ab41b4aed082705d1056416ae4468b6ea99d52599ecf3169b00088d43113e337" +"checksum parking_lot_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ad7f7e6ebdc79edff6fdcb87a55b620174f7a989e3eb31b65231f4af57f00b8c" +"checksum parking_lot_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "94c8c7923936b28d546dfd14d4472eaf34c99b14e1c973a32b3e6d4eb04298c9" +"checksum pbkdf2 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "0c09cddfbfc98de7f76931acf44460972edb4023eb14d0c6d4018800e552d8e0" +"checksum peeking_take_while 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" +"checksum percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831" +"checksum pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)" = "676e8eb2b1b4c9043511a9b7bea0915320d7e502b0a079fb03f9635a5252b18c" +"checksum podio 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "780fb4b6698bbf9cf2444ea5d22411cef2953f0824b98f33cf454ec5615645bd" +"checksum pretty_assertions 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3a029430f0d744bc3d15dd474d591bed2402b645d024583082b9f63bb936dac6" +"checksum prettytable-rs 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5511ca4c805aa35f0abff6be7923231d664408b60c09f44ef715f2bce106cd9e" +"checksum proc-macro2 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "77997c53ae6edd6d187fec07ec41b207063b5ee6f33680e9fa86d405cdd313d4" +"checksum proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)" = "4d317f9caece796be1980837fd5cb3dfec5613ebdb04ad0956deea83ce168915" +"checksum quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9274b940887ce9addde99c4eee6b5c44cc494b182b97e73dc8ffdcb3397fd3f0" +"checksum quote 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9949cfe66888ffe1d53e6ec9d9f3b70714083854be20fd5e271b232a017401e8" +"checksum quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)" = "cdd8e04bd9c52e0342b406469d494fcb033be4bdbe5c606016defbb1681411e1" +"checksum rand 0.3.23 (registry+https://github.com/rust-lang/crates.io-index)" = "64ac302d8f83c0c1974bf758f6b041c6c8ada916fbb44a609158ca8b064cc76c" +"checksum rand 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" +"checksum rand 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c618c47cd3ebd209790115ab837de41425723956ad3ce2e6a7f09890947cacb9" +"checksum rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)" = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" +"checksum rand_chacha 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" +"checksum rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +"checksum rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d0e7a549d590831370895ab7ba4ea0c1b6b011d106b5ff2da6eee112615e6dc0" +"checksum rand_hc 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" +"checksum rand_isaac 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" +"checksum rand_jitter 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7b9ea758282efe12823e0d952ddb269d2e1897227e464919a554f2a03ef1b832" +"checksum rand_os 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b7c690732391ae0abafced5015ffb53656abfaec61b342290e5eb56b286a679d" +"checksum rand_pcg 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "086bd09a33c7044e56bb44d5bdde5a60e7f119a9e95b0775f545de759a32fe05" +"checksum rand_xorshift 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" +"checksum rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +"checksum redox_syscall 0.1.51 (registry+https://github.com/rust-lang/crates.io-index)" = "423e376fffca3dfa06c9e9790a9ccd282fafb3cc6e6397d01dbf64f9bacc6b85" +"checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" +"checksum redox_users 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "214a97e49be64fd2c86f568dd0cb2c757d2cc53de95b273b6ad0a1c908482f26" +"checksum regex 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "37e7cbbd370869ce2e8dff25c7018702d10b21a20ef7135316f8daecd6c25b7f" +"checksum regex-syntax 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)" = "8c2f35eedad5295fdf00a63d7d4b238135723f92b434ec06774dad15c7ab0861" +"checksum ring 0.13.5 (registry+https://github.com/rust-lang/crates.io-index)" = "2c4db68a2e35f3497146b7e4563df7d4773a2433230c5e4b448328e31740458a" +"checksum ripemd160 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "482aa56cc68aaeccdaaff1cc5a72c247da8bbad3beb174ca5741f274c22883fb" +"checksum rpassword 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d37473170aedbe66ffa3ad3726939ba677d83c646ad4fd99e5b4bc38712f45ec" +"checksum rustc-demangle 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "adacaae16d02b6ec37fdc7acfcddf365978de76d1983d3ee22afc260e1ca9619" +"checksum rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)" = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda" +"checksum rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +"checksum rustls 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)" = "942b71057b31981152970d57399c25f72e27a6ee0d207a669d8304cabf44705b" +"checksum ryu 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "eb9e9b8cde282a9fe6a42dd4681319bfb63f121b8a8ee9439c6f4107e58a46f7" +"checksum safemem 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8dca453248a96cb0749e36ccdfe2b0b4e54a61bfef89fb97ec621eb8e0a93dd9" +"checksum same-file 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8f20c4be53a8a1ff4c1f1b2bd14570d2f634628709752f0702ecdd2b3f9a5267" +"checksum scoped-tls 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "332ffa32bf586782a3efaeb58f127980944bbc8c4d6913a86107ac2a5ab24b28" +"checksum scoped_threadpool 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8" +"checksum scopeguard 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "94258f53601af11e6a49f722422f6e3425c52b06245a5cf9bc09908b174f5e27" +"checksum sct 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "cb8f61f9e6eadd062a71c380043d28036304a4706b3c4dd001ff3387ed00745a" +"checksum semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +"checksum semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" +"checksum serde 1.0.87 (registry+https://github.com/rust-lang/crates.io-index)" = "2e20fde37801e83c891a2dc4ebd3b81f0da4d1fb67a9e0a2a3b921e2536a58ee" +"checksum serde-value 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7a663f873dedc4eac1a559d4c6bc0d0b2c34dc5ac4702e105014b8281489e44f" +"checksum serde_derive 1.0.87 (registry+https://github.com/rust-lang/crates.io-index)" = "633e97856567e518b59ffb2ad7c7a4fd4c5d91d9c7f32dd38a27b2bf7e8114ea" +"checksum serde_json 1.0.38 (registry+https://github.com/rust-lang/crates.io-index)" = "27dce848e7467aa0e2fcaf0a413641499c0b745452aaca1194d24dedde9e13c9" +"checksum serde_yaml 0.8.8 (registry+https://github.com/rust-lang/crates.io-index)" = "0887a8e097a69559b56aa2526bf7aff7c3048cf627dff781f0b56a6001534593" +"checksum sha2 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9eb6be24e4c23a84d7184280d2722f7f2731fcdd4a9d886efbfe4413e4847ea0" +"checksum siphasher 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac" +"checksum slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" +"checksum smallvec 0.6.8 (registry+https://github.com/rust-lang/crates.io-index)" = "88aea073965ab29f6edb5493faf96ad662fb18aa9eeb186a3b7057951605ed15" +"checksum stable_deref_trait 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "dba1a27d3efae4351c8051072d619e3ade2820635c3958d826bfea39d59b54c8" +"checksum string 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "b639411d0b9c738748b5397d5ceba08e648f4f1992231aa859af1a017f31f60b" +"checksum strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb4f380125926a99e52bc279241539c018323fab05ad6368b56f93d9369ff550" +"checksum supercow 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "171758edb47aa306a78dfa4ab9aeb5167405bd4e3dc2b64e88f6a84bbe98bd63" +"checksum syn 0.15.26 (registry+https://github.com/rust-lang/crates.io-index)" = "f92e629aa1d9c827b2bb8297046c1ccffc57c99b947a680d3ccff1f136a3bee9" +"checksum synstructure 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "73687139bf99285483c96ac0add482c3776528beac1d97d444f6e91f203a2015" +"checksum term 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "5e6b677dd1e8214ea1ef4297f85dbcbed8e8cdddb561040cc998ca2551c37561" +"checksum termcolor 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "4096add70612622289f2fdcdbd5086dc81c1e2675e6ae58d6c4f62a16c6d7f2f" +"checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096" +"checksum textwrap 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "307686869c93e71f94da64286f9a9524c0f308a9e1c87a583de8e9c9039ad3f6" +"checksum thread-id 3.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c7fbf4c9d56b320106cd64fd024dadfa0be7cb4706725fc44a7d7ce952d820c1" +"checksum thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b" +"checksum time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f" +"checksum tokio 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)" = "6e93c78d23cc61aa245a8acd2c4a79c4d7fa7fb5c3ca90d5737029f043a84895" +"checksum tokio-codec 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "5c501eceaf96f0e1793cf26beb63da3d11c738c4a943fdf3746d81d64684c39f" +"checksum tokio-core 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)" = "aeeffbbb94209023feaef3c196a41cbcdafa06b4a6f893f68779bb5e53796f71" +"checksum tokio-current-thread 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "331c8acc267855ec06eb0c94618dcbbfea45bed2d20b77252940095273fb58f6" +"checksum tokio-executor 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "30c6dbf2d1ad1de300b393910e8a3aa272b724a400b6531da03eed99e329fbf0" +"checksum tokio-fs 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "0e9cbbc8a3698b7ab652340f46633364f9eaa928ddaaee79d8b8f356dd79a09d" +"checksum tokio-io 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)" = "b53aeb9d3f5ccf2ebb29e19788f96987fa1355f8fe45ea193928eaaaf3ae820f" +"checksum tokio-reactor 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "afbcdb0f0d2a1e4c440af82d7bbf0bf91a8a8c0575bcd20c05d15be7e9d3a02f" +"checksum tokio-retry 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f05746ae87dca83a2016b4f5dba5b237b897dd12fd324f60afe282112f16969a" +"checksum tokio-rustls 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "208d62fa3e015426e3c64039d9d20adf054a3c9b4d9445560f1c41c75bef3eab" +"checksum tokio-service 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "24da22d077e0f15f55162bdbdc661228c1581892f52074fb242678d015b45162" +"checksum tokio-tcp 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "1d14b10654be682ac43efee27401d792507e30fd8d26389e1da3b185de2e4119" +"checksum tokio-threadpool 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)" = "c3fd86cb15547d02daa2b21aadaf4e37dee3368df38a526178a5afa3c034d2fb" +"checksum tokio-timer 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)" = "2910970404ba6fa78c5539126a9ae2045d62e3713041e447f695f41405a120c6" +"checksum tokio-udp 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "66268575b80f4a4a710ef83d087fdfeeabdce9b74c797535fbac18a2cb906e92" +"checksum tokio-uds 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "037ffc3ba0e12a0ab4aca92e5234e0dedeb48fddf6ccd260f1f150a36a9f2445" +"checksum toml 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)" = "758664fc71a3a69038656bee8b6be6477d2a6c315a6b81f7081f591bffa4111f" +"checksum traitobject 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "efd1f82c56340fdf16f2a953d7bda4f8fdffba13d93b00844c25572110b26079" +"checksum try-lock 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e604eb7b43c06650e854be16a2a03155743d3752dd1c943f6829e26b7a36e382" +"checksum typemap 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "653be63c80a3296da5551e1bfd2cca35227e13cdd08c6668903ae2f4f77aa1f6" +"checksum typenum 1.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "612d636f949607bdf9b123b4a6f6d966dedf3ff669f7f045890d3a4a73948169" +"checksum ucd-util 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "535c204ee4d8434478593480b8f86ab45ec9aae0e83c568ca81abf0fd0e88f86" +"checksum unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" +"checksum unicode-normalization 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "141339a08b982d942be2ca06ff8b076563cbe223d1befd5450716790d44e2426" +"checksum unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "882386231c45df4700b275c7ff55b6f3698780a650026380e72dabe76fa46526" +"checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" +"checksum unreachable 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56" +"checksum unsafe-any 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f30360d7979f5e9c6e6cea48af192ea8fab4afb3cf72597154b8f08935bc9c7f" +"checksum untrusted 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "55cd1f4b4e96b46aeb8d4855db4a7a9bd96eeeb5c6a1ab54593328761642ce2f" +"checksum url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dd4e7c0d531266369519a4aa4f399d748bd37043b00bde1e4ff1f60a120b355a" +"checksum utf8-ranges 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "796f7e48bef87609f7ade7e06495a87d5cd06c7866e6a5cbfceffc558a243737" +"checksum uuid 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)" = "e1436e58182935dcd9ce0add9ea0b558e8a87befe01c1a301e6020aeb0876363" +"checksum vcpkg 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "def296d3eb3b12371b2c7d0e83bfe1403e4db2d7a0bba324a12b21c4ee13143d" +"checksum vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a" +"checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" +"checksum walkdir 2.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "9d9d7ed3431229a144296213105a390676cc49c9b6a72bd19f3176c98e129fa1" +"checksum want 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "797464475f30ddb8830cc529aaaae648d581f99e2036a928877dfde027ddf6b3" +"checksum webpki 0.18.1 (registry+https://github.com/rust-lang/crates.io-index)" = "17d7967316d8411ca3b01821ee6c332bde138ba4363becdb492f12e514daa17f" +"checksum webpki-roots 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)" = "85d1f408918fd590908a70d36b7ac388db2edc221470333e4d6e5b598e44cabf" +"checksum which 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "e84a603e7e0b1ce1aa1ee2b109c7be00155ce52df5081590d1ffb93f4f515cb2" +"checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" +"checksum winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "92c1eb33641e276cfa214a0522acad57be5c56b10cb348b3c5117db75f3ac4b0" +"checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" +"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +"checksum winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7168bab6e1daee33b4557efd0e95d5ca70a03706d39fa5f3fe7a236f584b03c9" +"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +"checksum wincolor 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "561ed901ae465d6185fa7864d63fbd5720d0ef718366c9a4dc83cf6170d7e9ba" +"checksum ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" +"checksum yaml-rust 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "e66366e18dc58b46801afbf2ca7661a9f59cc8c5962c29892b6039b4f86fa992" +"checksum yaml-rust 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "95acf0db5515d07da9965ec0e0ba6cc2d825e2caeb7303b66ca441729801254e" +"checksum zeroize 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "8ddfeb6eee2fb3b262ef6e0898a52b7563bb8e0d5955a313b3cf2f808246ea14" +"checksum zip 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "36b9e08fb518a65cf7e08a1e482573eb87a2f4f8c6619316612a3c1f162fe822" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..d57cbc1 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,54 @@ +[package] +name = "grin_wallet" +version = "1.1.0" +authors = ["Grin Developers "] +description = "Simple, private and scalable cryptocurrency implementation based on the MimbleWimble chain format." +license = "Apache-2.0" +repository = "https://github.com/mimblewimble/grin" +keywords = [ "crypto", "grin", "mimblewimble" ] +readme = "README.md" +exclude = ["**/*.grin", "**/*.grin2"] +build = "src/build/build.rs" +edition = "2018" + +[[bin]] +name = "grin-wallet" +path = "src/bin/grin-wallet.rs" + +[workspace] +members = ["libwallet", "config"] + +[dependencies] +clap = { version = "2.31", features = ["yaml"] } +rpassword = "2.0.0" +ctrlc = { version = "3.1", features = ["termination"] } +blake2-rfc = "0.2" +byteorder = "1" +failure = "0.1" +failure_derive = "0.1" +futures = "0.1" +hyper = "0.12" +prettytable-rs = "0.7" +rand = "0.5" +serde = "1" +serde_derive = "1" +serde_json = "1" +log = "0.4" +term = "0.5" +tokio = "= 0.1.11" +tokio-core = "0.1" +tokio-retry = "0.1" +ring = "0.13" +uuid = { version = "0.6", features = ["serde", "v4"] } +url = "1.7.0" +chrono = { version = "0.4.4", features = ["serde"] } + +grin_api = { path = "../grin/api", version = "1.1.0" } +grin_core = { path = "../grin/core", version = "1.1.0" } +grin_keychain = { path = "../grin/keychain", version = "1.1.0" } +grin_store = { path = "../grin/store", version = "1.1.0" } +grin_util = { path = "../grin/util", version = "1.1.0" } +grin_chain = { path = "../grin/chain", version = "1.1.0" } + +[build-dependencies] +built = "0.3" diff --git a/config/Cargo.toml b/config/Cargo.toml new file mode 100644 index 0000000..e59af8c --- /dev/null +++ b/config/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "grin_wallet_config" +version = "1.1.0" +authors = ["Grin Developers "] +description = "Configuration for grin wallet , a simple, private and scalable cryptocurrency implementation based on the MimbleWimble chain format." +license = "Apache-2.0" +repository = "https://github.com/mimblewimble/grin" +keywords = [ "crypto", "grin", "mimblewimble" ] +workspace = ".." +edition = "2018" + +[dependencies] +rand = "0.5" +serde = "1" +serde_derive = "1" +toml = "0.4" +dirs = "1.0.3" + +grin_core = { path = "../../grin/core", version = "1.1.0" } +grin_util = { path = "../../grin/util", version = "1.1.0" } + +[dev-dependencies] +pretty_assertions = "0.5.1" diff --git a/config/src/.lib.rs.swp b/config/src/.lib.rs.swp new file mode 100644 index 0000000..e374fc0 Binary files /dev/null and b/config/src/.lib.rs.swp differ diff --git a/config/src/.types.rs.swp b/config/src/.types.rs.swp new file mode 100644 index 0000000..738c8a4 Binary files /dev/null and b/config/src/.types.rs.swp differ diff --git a/config/src/comments.rs b/config/src/comments.rs new file mode 100644 index 0000000..2e3f9c9 --- /dev/null +++ b/config/src/comments.rs @@ -0,0 +1,223 @@ +// Copyright 2018 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Comments for configuration + injection into output .toml +use std::collections::HashMap; + +/// maps entries to Comments that should precede them +fn comments() -> HashMap { + let mut retval = HashMap::new(); + + retval.insert( + "[wallet]".to_string(), + " +######################################### +### WALLET CONFIGURATION ### +######################################### +" + .to_string(), + ); + + retval.insert( + "api_listen_interface".to_string(), + " +#host IP for wallet listener, change to \"0.0.0.0\" to receive grins +" + .to_string(), + ); + + retval.insert( + "api_listen_port".to_string(), + " +#path of TLS certificate file, self-signed certificates are not supported +#tls_certificate_file = \"\" +#private key for the TLS certificate +#tls_certificate_key = \"\" + +#port for wallet listener +" + .to_string(), + ); + + retval.insert( + "owner_api_listen_port".to_string(), + " +#port for wallet owner api +" + .to_string(), + ); + + retval.insert( + "api_secret_path".to_string(), + " +#path of the secret token used by the API to authenticate the calls +#comment it to disable basic auth +" + .to_string(), + ); + retval.insert( + "check_node_api_http_addr".to_string(), + " +#where the wallet should find a running node +" + .to_string(), + ); + retval.insert( + "node_api_secret_path".to_string(), + " +#location of the node api secret for basic auth on the Grin API +" + .to_string(), + ); + retval.insert( + "owner_api_include_foreign".to_string(), + " +#include the foreign API endpoints on the same port as the owner +#API. Useful for networking environments like AWS ECS that make +#it difficult to access multiple ports on a single service. +" + .to_string(), + ); + retval.insert( + "data_file_dir".to_string(), + " +#where to find wallet files (seed, data, etc) +" + .to_string(), + ); + retval.insert( + "no_commit_cache".to_string(), + " +#If true, don't store calculated commits in the database +#better privacy, but at a performance cost of having to +#re-calculate commits every time they're used +" + .to_string(), + ); + retval.insert( + "dark_background_color_scheme".to_string(), + " +#Whether to use the black background color scheme for command line +" + .to_string(), + ); + retval.insert( + "keybase_notify_ttl".to_string(), + " +#The exploding lifetime for keybase notification on coins received. +#Unit: Minute. Default value 1440 minutes for one day. +#Refer to https://keybase.io/blog/keybase-exploding-messages for detail. +#To disable this notification, set it as 0. +" + .to_string(), + ); + + retval.insert( + "[logging]".to_string(), + " +######################################### +### LOGGING CONFIGURATION ### +######################################### +" + .to_string(), + ); + + retval.insert( + "log_to_stdout".to_string(), + " +#whether to log to stdout +" + .to_string(), + ); + + retval.insert( + "stdout_log_level".to_string(), + " +#log level for stdout: Error, Warning, Info, Debug, Trace +" + .to_string(), + ); + + retval.insert( + "log_to_file".to_string(), + " +#whether to log to a file +" + .to_string(), + ); + + retval.insert( + "file_log_level".to_string(), + " +#log level for file: Error, Warning, Info, Debug, Trace +" + .to_string(), + ); + + retval.insert( + "log_file_path".to_string(), + " +#log file path +" + .to_string(), + ); + + retval.insert( + "log_file_append".to_string(), + " +#whether to append to the log file (true), or replace it on every run (false) +" + .to_string(), + ); + + retval.insert( + "log_max_size".to_string(), + " +#maximum log file size in bytes before performing log rotation +#comment it to disable log rotation +" + .to_string(), + ); + + retval +} + +fn get_key(line: &str) -> String { + if line.contains("[") && line.contains("]") { + return line.to_owned(); + } else if line.contains("=") { + return line.split("=").collect::>()[0].trim().to_owned(); + } else { + return "NOT_FOUND".to_owned(); + } +} + +pub fn insert_comments(orig: String) -> String { + let comments = comments(); + let lines: Vec<&str> = orig.split("\n").collect(); + let mut out_lines = vec![]; + for l in lines { + let key = get_key(l); + if let Some(v) = comments.get(&key) { + out_lines.push(v.to_owned()); + } + out_lines.push(l.to_owned()); + out_lines.push("\n".to_owned()); + } + let mut ret_val = String::from(""); + for l in out_lines { + ret_val.push_str(&l); + } + ret_val.to_owned() +} diff --git a/config/src/config.rs b/config/src/config.rs new file mode 100644 index 0000000..3f63752 --- /dev/null +++ b/config/src/config.rs @@ -0,0 +1,275 @@ +// Copyright 2018 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Configuration file management + +use dirs; +use rand::distributions::{Alphanumeric, Distribution}; +use rand::thread_rng; +use std::env; +use std::fs::{self, File}; +use std::io::prelude::*; +use std::io::BufReader; +use std::io::Read; +use std::path::PathBuf; +use toml; + +use crate::comments::insert_comments; +use crate::core::global; +use crate::p2p; +use crate::servers::ServerConfig; +use crate::types::{ + ConfigError, ConfigMembers, GlobalConfig, GlobalWalletConfig, GlobalWalletConfigMembers, +}; +use crate::util::LoggingConfig; +use crate::wallet::WalletConfig; + +/// Wallet configuration file name +pub const WALLET_CONFIG_FILE_NAME: &'static str = "grin-wallet.toml"; +const WALLET_LOG_FILE_NAME: &'static str = "grin-wallet.log"; +const GRIN_HOME: &'static str = ".grin"; +/// Wallet data directory +pub const GRIN_WALLET_DIR: &'static str = "wallet_data"; +pub const API_SECRET_FILE_NAME: &'static str = ".api_secret"; + +fn get_grin_path(chain_type: &global::ChainTypes) -> Result { + // Check if grin dir exists + let mut grin_path = match dirs::home_dir() { + Some(p) => p, + None => PathBuf::new(), + }; + grin_path.push(GRIN_HOME); + grin_path.push(chain_type.shortname()); + // Create if the default path doesn't exist + if !grin_path.exists() { + fs::create_dir_all(grin_path.clone())?; + } + Ok(grin_path) +} + +fn check_config_current_dir(path: &str) -> Option { + let p = env::current_dir(); + let mut c = match p { + Ok(c) => c, + Err(_) => { + return None; + } + }; + c.push(path); + if c.exists() { + return Some(c); + } + None +} + +/// Create file with api secret +pub fn init_api_secret(api_secret_path: &PathBuf) -> Result<(), ConfigError> { + let mut api_secret_file = File::create(api_secret_path)?; + let api_secret: String = Alphanumeric + .sample_iter(&mut thread_rng()) + .take(20) + .collect(); + api_secret_file.write_all(api_secret.as_bytes())?; + Ok(()) +} + +/// Check if file contains a secret and nothing else +pub fn check_api_secret(api_secret_path: &PathBuf) -> Result<(), ConfigError> { + let api_secret_file = File::open(api_secret_path)?; + let buf_reader = BufReader::new(api_secret_file); + let mut lines_iter = buf_reader.lines(); + let first_line = lines_iter.next(); + if first_line.is_none() || first_line.unwrap().is_err() { + fs::remove_file(api_secret_path)?; + init_api_secret(api_secret_path)?; + } + Ok(()) +} + +/// Check that the api secret file exists and is valid +fn check_api_secret_file(chain_type: &global::ChainTypes) -> Result<(), ConfigError> { + let grin_path = get_grin_path(chain_type)?; + let mut api_secret_path = grin_path.clone(); + api_secret_path.push(API_SECRET_FILE_NAME); + if !api_secret_path.exists() { + init_api_secret(&api_secret_path) + } else { + check_api_secret(&api_secret_path) + } +} + +/// Handles setup and detection of paths for wallet +pub fn initial_setup_wallet( + chain_type: &global::ChainTypes, +) -> Result { + check_api_secret_file(chain_type)?; + // Use config file if current directory if it exists, .grin home otherwise + if let Some(p) = check_config_current_dir(WALLET_CONFIG_FILE_NAME) { + GlobalWalletConfig::new(p.to_str().unwrap()) + } else { + // Check if grin dir exists + let grin_path = get_grin_path(chain_type)?; + + // Get path to default config file + let mut config_path = grin_path.clone(); + config_path.push(WALLET_CONFIG_FILE_NAME); + + // Spit it out if it doesn't exist + if !config_path.exists() { + let mut default_config = GlobalWalletConfig::for_chain(chain_type); + // update paths relative to current dir + default_config.update_paths(&grin_path); + default_config.write_to_file(config_path.to_str().unwrap())?; + } + + GlobalWalletConfig::new(config_path.to_str().unwrap()) + } +} + +impl Default for GlobalWalletConfigMembers { + fn default() -> GlobalWalletConfigMembers { + GlobalWalletConfigMembers { + logging: Some(LoggingConfig::default()), + wallet: WalletConfig::default(), + } + } +} + +impl Default for GlobalWalletConfig { + fn default() -> GlobalWalletConfig { + GlobalWalletConfig { + config_file_path: None, + members: Some(GlobalWalletConfigMembers::default()), + } + } +} + +impl GlobalWalletConfig { + /// Same as GlobalConfig::default() but further tweaks parameters to + /// apply defaults for each chain type + pub fn for_chain(chain_type: &global::ChainTypes) -> GlobalWalletConfig { + let mut defaults_conf = GlobalWalletConfig::default(); + let mut defaults = &mut defaults_conf.members.as_mut().unwrap().wallet; + defaults.chain_type = Some(chain_type.clone()); + + match *chain_type { + global::ChainTypes::Mainnet => {} + global::ChainTypes::Floonet => { + defaults.api_listen_port = 13415; + defaults.check_node_api_http_addr = "http://127.0.0.1:13413".to_owned(); + } + global::ChainTypes::UserTesting => { + defaults.api_listen_port = 23415; + defaults.check_node_api_http_addr = "http://127.0.0.1:23413".to_owned(); + } + global::ChainTypes::AutomatedTesting => { + panic!("Can't run automated testing directly"); + } + } + defaults_conf + } + /// Requires the path to a config file + pub fn new(file_path: &str) -> Result { + let mut return_value = GlobalWalletConfig::default(); + return_value.config_file_path = Some(PathBuf::from(&file_path)); + + // Config file path is given but not valid + let config_file = return_value.config_file_path.clone().unwrap(); + if !config_file.exists() { + return Err(ConfigError::FileNotFoundError(String::from( + config_file.to_str().unwrap(), + ))); + } + + // Try to parse the config file if it exists, explode if it does exist but + // something's wrong with it + return_value.read_config() + } + + /// Read config + fn read_config(mut self) -> Result { + let mut file = File::open(self.config_file_path.as_mut().unwrap())?; + let mut contents = String::new(); + file.read_to_string(&mut contents)?; + let decoded: Result = toml::from_str(&contents); + match decoded { + Ok(gc) => { + self.members = Some(gc); + return Ok(self); + } + Err(e) => { + return Err(ConfigError::ParseError( + String::from( + self.config_file_path + .as_mut() + .unwrap() + .to_str() + .unwrap() + .clone(), + ), + String::from(format!("{}", e)), + )); + } + } + } + + /// Update paths + pub fn update_paths(&mut self, wallet_home: &PathBuf) { + let mut wallet_path = wallet_home.clone(); + wallet_path.push(GRIN_WALLET_DIR); + self.members.as_mut().unwrap().wallet.data_file_dir = + wallet_path.to_str().unwrap().to_owned(); + let mut secret_path = wallet_home.clone(); + secret_path.push(API_SECRET_FILE_NAME); + self.members.as_mut().unwrap().wallet.api_secret_path = + Some(secret_path.to_str().unwrap().to_owned()); + let mut node_secret_path = wallet_home.clone(); + node_secret_path.push(API_SECRET_FILE_NAME); + self.members.as_mut().unwrap().wallet.node_api_secret_path = + Some(node_secret_path.to_str().unwrap().to_owned()); + let mut log_path = wallet_home.clone(); + log_path.push(WALLET_LOG_FILE_NAME); + self.members + .as_mut() + .unwrap() + .logging + .as_mut() + .unwrap() + .log_file_path = log_path.to_str().unwrap().to_owned(); + } + + /// Serialize config + pub fn ser_config(&mut self) -> Result { + let encoded: Result = + toml::to_string(self.members.as_mut().unwrap()); + match encoded { + Ok(enc) => return Ok(enc), + Err(e) => { + return Err(ConfigError::SerializationError(String::from(format!( + "{}", + e + )))); + } + } + } + + /// Write configuration to a file + pub fn write_to_file(&mut self, name: &str) -> Result<(), ConfigError> { + let conf_out = self.ser_config()?; + let conf_out = insert_comments(conf_out); + let mut file = File::create(name)?; + file.write_all(conf_out.as_bytes())?; + Ok(()) + } +} diff --git a/config/src/lib.rs b/config/src/lib.rs new file mode 100644 index 0000000..a58f8db --- /dev/null +++ b/config/src/lib.rs @@ -0,0 +1,34 @@ +// Copyright 2018 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Crate wrapping up the Grin binary and configuration file + +#![deny(non_upper_case_globals)] +#![deny(non_camel_case_types)] +#![deny(non_snake_case)] +#![deny(unused_mut)] +#![warn(missing_docs)] + +#[macro_use] +extern crate serde_derive; + +use grin_core as core; +use grin_util as util; + +mod comments; +pub mod config; +pub mod types; + +pub use crate::config::{initial_setup_wallet, GRIN_WALLET_DIR}; +pub use crate::types::{ConfigError, GlobalWalletConfig}; diff --git a/config/src/types.rs b/config/src/types.rs new file mode 100644 index 0000000..9aee3e4 --- /dev/null +++ b/config/src/types.rs @@ -0,0 +1,87 @@ +// Copyright 2018 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Public types for config modules + +use std::fmt; +use std::io; +use std::path::PathBuf; + +use crate::util::LoggingConfig; +use crate::wallet::WalletConfig; + +/// Error type wrapping config errors. +#[derive(Debug)] +pub enum ConfigError { + /// Error with parsing of config file + ParseError(String, String), + + /// Error with fileIO while reading config file + FileIOError(String, String), + + /// No file found + FileNotFoundError(String), + + /// Error serializing config values + SerializationError(String), +} + +impl fmt::Display for ConfigError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + ConfigError::ParseError(ref file_name, ref message) => write!( + f, + "Error parsing configuration file at {} - {}", + file_name, message + ), + ConfigError::FileIOError(ref file_name, ref message) => { + write!(f, "{} {}", message, file_name) + } + ConfigError::FileNotFoundError(ref file_name) => { + write!(f, "Configuration file not found: {}", file_name) + } + ConfigError::SerializationError(ref message) => { + write!(f, "Error serializing configuration: {}", message) + } + } + } +} + +impl From for ConfigError { + fn from(error: io::Error) -> ConfigError { + ConfigError::FileIOError( + String::from(""), + String::from(format!("Error loading config file: {}", error)), + ) + } +} + +/// Wallet should be split into a separate configuration file +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct GlobalWalletConfig { + /// Keep track of the file we've read + pub config_file_path: Option, + /// Wallet members + pub members: Option, +} + +/// Wallet internal members +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct GlobalWalletConfigMembers { + /// Wallet configuration + #[serde(default)] + pub wallet: WalletConfig, + /// Logging config + pub logging: Option, +} diff --git a/libwallet/Cargo.toml b/libwallet/Cargo.toml new file mode 100644 index 0000000..a4f0acc --- /dev/null +++ b/libwallet/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "grin_libwallet" +version = "1.1.0" +authors = ["Grin Developers "] +description = "Simple, private and scalable cryptocurrency implementation based on the MimbleWimble chain format." +license = "Apache-2.0" +repository = "https://github.com/mimblewimble/grin" +keywords = [ "crypto", "grin", "mimblewimble" ] +readme = "README.md" +exclude = ["**/*.grin", "**/*.grin2"] +#build = "src/build/build.rs" +edition = "2018" + +[dependencies] +blake2-rfc = "0.2" +failure = "0.1" +failure_derive = "0.1" +rand = "0.5" +serde = "1" +serde_derive = "1" +serde_json = "1" +log = "0.4" +uuid = { version = "0.6", features = ["serde", "v4"] } +chrono = { version = "0.4.4", features = ["serde"] } + +grin_core = { path = "../../grin/core", version = "1.1.0" } +grin_keychain = { path = "../../grin/keychain", version = "1.1.0" } +grin_util = { path = "../../grin/util", version = "1.1.0" } diff --git a/libwallet/src/api.rs b/libwallet/src/api.rs new file mode 100644 index 0000000..4962725 --- /dev/null +++ b/libwallet/src/api.rs @@ -0,0 +1,968 @@ +// Copyright 2018 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Main interface into all wallet API functions. +//! Wallet APIs are split into two seperate blocks of functionality +//! called the 'Owner' and 'Foreign' APIs: +//! * The 'Owner' API is intended to expose methods that are to be +//! used by the wallet owner only. It is vital that this API is not +//! exposed to anyone other than the owner of the wallet (i.e. the +//! person with access to the seed and password. +//! * The 'Foreign' API contains methods that other wallets will +//! use to interact with the owner's wallet. This API can be exposed +//! to the outside world, with the consideration as to how that can +//! be done securely up to the implementor. +//! +//! Methods in both APIs are intended to be 'single use', that is to say each +//! method will 'open' the wallet (load the keychain with its master seed), perform +//! its operation, then 'close' the wallet (unloading references to the keychain and master +//! seed). + +use crate::util::Mutex; +use std::marker::PhantomData; +use std::sync::Arc; +use uuid::Uuid; + +use crate::core::core::hash::Hashed; +use crate::core::core::Transaction; +use crate::core::ser; +use crate::keychain::{Identifier, Keychain}; +use crate::internal::{keys, tx, updater}; +use crate::slate::Slate; +use crate::types::{ + AcctPathMapping, BlockFees, CbData, NodeClient, OutputData, OutputLockFn, TxLogEntry, + TxLogEntryType, TxWrapper, WalletBackend, WalletInfo, +}; +use crate::{Error, ErrorKind}; +use crate::util; +use crate::util::secp::{pedersen, ContextFlag, Secp256k1}; + +const USER_MESSAGE_MAX_LEN: usize = 256; + +/// Functions intended for use by the owner (e.g. master seed holder) of the wallet. +pub struct APIOwner +where + W: WalletBackend, + C: NodeClient, + K: Keychain, +{ + /// A reference-counted mutex to an implementation of the + /// [`WalletBackend`](../types/trait.WalletBackend.html) trait. + pub wallet: Arc>, + phantom: PhantomData, + phantom_c: PhantomData, +} + +impl APIOwner +where + W: WalletBackend, + C: NodeClient, + K: Keychain, +{ + /// Create a new API instance with the given wallet instance. All subsequent + /// API calls will operate on this instance of the wallet. + /// + /// Each method will call the [`WalletBackend`](../types/trait.WalletBackend.html)'s + /// [`open_with_credentials`](../types/trait.WalletBackend.html#tymethod.open_with_credentials) + /// (initialising a keychain with the master seed,) perform its operation, then close the keychain + /// with a call to [`close`](../types/trait.WalletBackend.html#tymethod.close) + /// + /// # Arguments + /// * `wallet_in` - A reference-counted mutex containing an implementation of the + /// [`WalletBackend`](../types/trait.WalletBackend.html) trait. + /// + /// # Returns + /// * An instance of the OwnerAPI holding a reference to the provided wallet + /// + /// # Example + /// ``` + /// # extern crate grin_wallet as wallet; + /// # extern crate grin_keychain as keychain; + /// # extern crate grin_util as util; + /// + /// use std::sync::Arc; + /// use util::Mutex; + /// + /// use keychain::ExtKeychain; + /// use wallet::libwallet::api::APIOwner; + /// + /// // These contain sample implementations of each part needed for a wallet + /// use wallet::{LMDBBackend, HTTPNodeClient, WalletBackend, WalletConfig}; + /// + /// let mut wallet_config = WalletConfig::default(); + /// # wallet_config.data_file_dir = "test_output/doc/wallet1".to_owned(); + /// + /// // A NodeClient must first be created to handle communication between + /// // the wallet and the node. + /// + /// let node_client = HTTPNodeClient::new(&wallet_config.check_node_api_http_addr, None); + /// let mut wallet:Arc>> = + /// Arc::new(Mutex::new( + /// LMDBBackend::new(wallet_config.clone(), "", node_client).unwrap() + /// )); + /// + /// let api_owner = APIOwner::new(wallet.clone()); + /// // .. perform wallet operations + /// + /// ``` + + pub fn new(wallet_in: Arc>) -> Self { + APIOwner { + wallet: wallet_in, + phantom: PhantomData, + phantom_c: PhantomData, + } + } + + /// Returns a list of accounts stored in the wallet (i.e. mappings between + /// user-specified labels and BIP32 derivation paths. + /// + /// # Returns + /// * Result Containing: + /// * A Vector of [`AcctPathMapping`](../types/struct.AcctPathMapping.html) data + /// * or [`libwallet::Error`](../struct.Error.html) if an error is encountered. + /// + /// # Remarks + /// + /// * A wallet should always have the path with the label 'default' path defined, + /// with path m/0/0 + /// * This method does not need to use the wallet seed or keychain. + /// + /// # Example + /// Set up as in [`new`](struct.APIOwner.html#method.new) method above. + /// ``` + /// # extern crate grin_wallet as wallet; + /// # extern crate grin_keychain as keychain; + /// # extern crate grin_util as util; + /// # use std::sync::Arc; + /// # use util::Mutex; + /// # use keychain::ExtKeychain; + /// # use wallet::libwallet::api::APIOwner; + /// # use wallet::{LMDBBackend, HTTPNodeClient, WalletBackend, WalletConfig}; + /// # let mut wallet_config = WalletConfig::default(); + /// # wallet_config.data_file_dir = "test_output/doc/wallet1".to_owned(); + /// # let node_client = HTTPNodeClient::new(&wallet_config.check_node_api_http_addr, None); + /// # let mut wallet:Arc>> = + /// # Arc::new(Mutex::new( + /// # LMDBBackend::new(wallet_config.clone(), "", node_client).unwrap() + /// # )); + /// + /// let api_owner = APIOwner::new(wallet.clone()); + /// + /// let result = api_owner.accounts(); + /// + /// if let Ok(accts) = result { + /// //... + /// } + /// ``` + + pub fn accounts(&self) -> Result, Error> { + let mut w = self.wallet.lock(); + keys::accounts(&mut *w) + } + + /// Creates a new 'account', which is a mapping of a user-specified + /// label to a BIP32 path + /// + /// # Arguments + /// * `label` - A human readable label to which to map the new BIP32 Path + /// + /// # Returns + /// * Result Containing: + /// * A [Keychain Identifier](#) for the new path + /// * or [`libwallet::Error`](../struct.Error.html) if an error is encountered. + /// + /// # Remarks + /// + /// * Wallets should be initialised with the 'default' path mapped to `m/0/0` + /// * Each call to this function will increment the first element of the path + /// so the first call will create an account at `m/1/0` and the second at + /// `m/2/0` etc. . . + /// * The account path is used throughout as the parent key for most key-derivation + /// operations. See [`set_active_account`](struct.APIOwner.html#method.set_active_account) for + /// further details. + /// + /// * This function does not need to use the root wallet seed or keychain. + /// + /// # Example + /// Set up as in [`new`](struct.APIOwner.html#method.new) method above. + /// ``` + /// # extern crate grin_wallet as wallet; + /// # extern crate grin_keychain as keychain; + /// # extern crate grin_util as util; + /// # use std::sync::Arc; + /// # use util::Mutex; + /// # use keychain::ExtKeychain; + /// # use wallet::libwallet::api::APIOwner; + /// # use wallet::{LMDBBackend, HTTPNodeClient, WalletBackend, WalletConfig}; + /// # let mut wallet_config = WalletConfig::default(); + /// # wallet_config.data_file_dir = "test_output/doc/wallet1".to_owned(); + /// # let node_client = HTTPNodeClient::new(&wallet_config.check_node_api_http_addr, None); + /// # let mut wallet:Arc>> = + /// # Arc::new(Mutex::new( + /// # LMDBBackend::new(wallet_config.clone(), "", node_client).unwrap() + /// # )); + /// + /// let api_owner = APIOwner::new(wallet.clone()); + /// + /// let result = api_owner.create_account_path("account1"); + /// + /// if let Ok(identifier) = result { + /// //... + /// } + /// ``` + + pub fn create_account_path(&self, label: &str) -> Result { + let mut w = self.wallet.lock(); + keys::new_acct_path(&mut *w, label) + } + + /// Sets the wallet's currently active account. This sets the + /// BIP32 parent path used for most key-derivation operations. + /// + /// # Arguments + /// * `label` - The human readable label for the account. Accounts can be retrieved via + /// the [`account`](struct.APIOwner.html#method.accounts) method + /// + /// # Returns + /// * Result Containing: + /// * `Ok(())` if the path was correctly set + /// * or [`libwallet::Error`](../struct.Error.html) if an error is encountered. + /// + /// # Remarks + /// + /// * Wallet parent paths are 2 path elements long, e.g. `m/0/0` is the path + /// labelled 'default'. Keys derived from this parent path are 3 elements long, + /// e.g. the secret keys derived from the `m/0/0` path will be at paths `m/0/0/0`, + /// `m/0/0/1` etc... + /// + /// * This function does not need to use the root wallet seed or keychain. + /// + /// # Example + /// Set up as in [`new`](struct.APIOwner.html#method.new) method above. + /// ``` + /// # extern crate grin_wallet as wallet; + /// # extern crate grin_keychain as keychain; + /// # extern crate grin_util as util; + /// # use std::sync::Arc; + /// # use util::Mutex; + /// # use keychain::ExtKeychain; + /// # use wallet::libwallet::api::APIOwner; + /// # use wallet::{LMDBBackend, HTTPNodeClient, WalletBackend, WalletConfig}; + /// # let mut wallet_config = WalletConfig::default(); + /// # wallet_config.data_file_dir = "test_output/doc/wallet1".to_owned(); + /// # let node_client = HTTPNodeClient::new(&wallet_config.check_node_api_http_addr, None); + /// # let mut wallet:Arc>> = + /// # Arc::new(Mutex::new( + /// # LMDBBackend::new(wallet_config.clone(), "", node_client).unwrap() + /// # )); + /// + /// let api_owner = APIOwner::new(wallet.clone()); + /// + /// let result = api_owner.create_account_path("account1"); + /// + /// if let Ok(identifier) = result { + /// // set the account active + /// let result2 = api_owner.set_active_account("account1"); + /// } + /// ``` + + pub fn set_active_account(&self, label: &str) -> Result<(), Error> { + let mut w = self.wallet.lock(); + w.set_parent_key_id_by_name(label)?; + Ok(()) + } + + /// Returns a list of outputs from the active account in the wallet. + /// + /// # Arguments + /// * `include_spent` - If `true`, outputs that have been marked as 'spent' + /// in the wallet will be returned. If `false`, spent outputs will omitted + /// from the results. + /// * `refresh_from_node` - If true, the wallet will attempt to contact + /// a node (via the [`NodeClient`](../types/trait.NodeClient.html) + /// provided during wallet instantiation). If `false`, the results will + /// contain output information that may be out-of-date (from the last time + /// the wallet's output set was refreshed against the node). + /// * `tx_id` - If `Some(i)`, only return the outputs associated with + /// the transaction log entry of id `i`. + /// + /// # Returns + /// * (`bool`, `Vec`) - A tuple: + /// * The first `bool` element indicates whether the data was successfully + /// refreshed from the node (note this may be false even if the `refresh_from_node` + /// argument was set to `true`. + /// * The second element contains the result set, of which each element is + /// a mapping between the wallet's internal [OutputData](../types/struct.OutputData.html) + /// and the Output commitment as identified in the chain's UTXO set + /// + /// # Example + /// Set up as in [`new`](struct.APIOwner.html#method.new) method above. + /// ``` + /// # extern crate grin_wallet as wallet; + /// # extern crate grin_keychain as keychain; + /// # extern crate grin_util as util; + /// # use std::sync::Arc; + /// # use util::Mutex; + /// # use keychain::ExtKeychain; + /// # use wallet::libwallet::api::APIOwner; + /// # use wallet::{LMDBBackend, HTTPNodeClient, WalletBackend, WalletConfig}; + /// # let mut wallet_config = WalletConfig::default(); + /// # wallet_config.data_file_dir = "test_output/doc/wallet1".to_owned(); + /// # let node_client = HTTPNodeClient::new(&wallet_config.check_node_api_http_addr, None); + /// # let mut wallet:Arc>> = + /// # Arc::new(Mutex::new( + /// # LMDBBackend::new(wallet_config.clone(), "", node_client).unwrap() + /// # )); + /// + /// let api_owner = APIOwner::new(wallet.clone()); + /// let show_spent = false; + /// let update_from_node = true; + /// let tx_id = None; + /// + /// let result = api_owner.retrieve_outputs(show_spent, update_from_node, tx_id); + /// + /// if let Ok((was_updated, output_mapping)) = result { + /// //... + /// } + /// ``` + + pub fn retrieve_outputs( + &self, + include_spent: bool, + refresh_from_node: bool, + tx_id: Option, + ) -> Result<(bool, Vec<(OutputData, pedersen::Commitment)>), Error> { + let mut w = self.wallet.lock(); + w.open_with_credentials()?; + let parent_key_id = w.parent_key_id(); + + let mut validated = false; + if refresh_from_node { + validated = self.update_outputs(&mut w, false); + } + + let res = Ok(( + validated, + updater::retrieve_outputs(&mut *w, include_spent, tx_id, Some(&parent_key_id))?, + )); + + w.close()?; + res + } + + /// Returns a list of [Transaction Log Entries](../types/struct.TxLogEntry.html) + /// from the active account in the wallet. + /// + /// # Arguments + /// * `refresh_from_node` - If true, the wallet will attempt to contact + /// a node (via the [`NodeClient`](../types/trait.NodeClient.html) + /// provided during wallet instantiation). If `false`, the results will + /// contain transaction information that may be out-of-date (from the last time + /// the wallet's output set was refreshed against the node). + /// * `tx_id` - If `Some(i)`, only return the transactions associated with + /// the transaction log entry of id `i`. + /// * `tx_slate_id` - If `Some(uuid)`, only return transactions associated with + /// the given [`Slate`](../../libtx/slate/struct.Slate.html) uuid. + /// + /// # Returns + /// * (`bool`, `Vec<[TxLogEntry](../types/struct.TxLogEntry.html)>`) - A tuple: + /// * The first `bool` element indicates whether the data was successfully + /// refreshed from the node (note this may be false even if the `refresh_from_node` + /// argument was set to `true`. + /// * The second element contains the set of retrieved + /// [TxLogEntries](../types/struct/TxLogEntry.html) + /// + /// # Example + /// Set up as in [`new`](struct.APIOwner.html#method.new) method above. + /// ``` + /// # extern crate grin_wallet as wallet; + /// # extern crate grin_keychain as keychain; + /// # extern crate grin_util as util; + /// # use std::sync::Arc; + /// # use util::Mutex; + /// # use keychain::ExtKeychain; + /// # use wallet::libwallet::api::APIOwner; + /// # use wallet::{LMDBBackend, HTTPNodeClient, WalletBackend, WalletConfig}; + /// # let mut wallet_config = WalletConfig::default(); + /// # wallet_config.data_file_dir = "test_output/doc/wallet1".to_owned(); + /// # let node_client = HTTPNodeClient::new(&wallet_config.check_node_api_http_addr, None); + /// # let mut wallet:Arc>> = + /// # Arc::new(Mutex::new( + /// # LMDBBackend::new(wallet_config.clone(), "", node_client).unwrap() + /// # )); + /// + /// let api_owner = APIOwner::new(wallet.clone()); + /// let update_from_node = true; + /// let tx_id = None; + /// let tx_slate_id = None; + /// + /// // Return all TxLogEntries + /// let result = api_owner.retrieve_txs(update_from_node, tx_id, tx_slate_id); + /// + /// if let Ok((was_updated, tx_log_entries)) = result { + /// //... + /// } + /// ``` + + pub fn retrieve_txs( + &self, + refresh_from_node: bool, + tx_id: Option, + tx_slate_id: Option, + ) -> Result<(bool, Vec), Error> { + let mut w = self.wallet.lock(); + w.open_with_credentials()?; + let parent_key_id = w.parent_key_id(); + + let mut validated = false; + if refresh_from_node { + validated = self.update_outputs(&mut w, false); + } + + let res = Ok(( + validated, + updater::retrieve_txs(&mut *w, tx_id, tx_slate_id, Some(&parent_key_id), false)?, + )); + + w.close()?; + res + } + + /// Returns summary information from the active account in the wallet. + /// + /// # Arguments + /// * `refresh_from_node` - If true, the wallet will attempt to contact + /// a node (via the [`NodeClient`](../types/trait.NodeClient.html) + /// provided during wallet instantiation). If `false`, the results will + /// contain transaction information that may be out-of-date (from the last time + /// the wallet's output set was refreshed against the node). + /// * `minimum_confirmations` - The minimum number of confirmations an output + /// should have before it's included in the 'amount_currently_spendable' total + /// + /// # Returns + /// * (`bool`, [`WalletInfo`](../types/struct.WalletInfo.html)) - A tuple: + /// * The first `bool` element indicates whether the data was successfully + /// refreshed from the node (note this may be false even if the `refresh_from_node` + /// argument was set to `true`. + /// * The second element contains the Summary [`WalletInfo`](../types/struct.WalletInfo.html) + /// + /// # Example + /// Set up as in [`new`](struct.APIOwner.html#method.new) method above. + /// ``` + /// # extern crate grin_wallet as wallet; + /// # extern crate grin_keychain as keychain; + /// # extern crate grin_util as util; + /// # use std::sync::Arc; + /// # use util::Mutex; + /// # use keychain::ExtKeychain; + /// # use wallet::libwallet::api::APIOwner; + /// # use wallet::{LMDBBackend, HTTPNodeClient, WalletBackend, WalletConfig}; + /// # let mut wallet_config = WalletConfig::default(); + /// # wallet_config.data_file_dir = "test_output/doc/wallet1".to_owned(); + /// # let node_client = HTTPNodeClient::new(&wallet_config.check_node_api_http_addr, None); + /// # let mut wallet:Arc>> = + /// # Arc::new(Mutex::new( + /// # LMDBBackend::new(wallet_config.clone(), "", node_client).unwrap() + /// # )); + /// + /// let mut api_owner = APIOwner::new(wallet.clone()); + /// let update_from_node = true; + /// let minimum_confirmations=10; + /// + /// // Return summary info for active account + /// let result = api_owner.retrieve_summary_info(update_from_node, minimum_confirmations); + /// + /// if let Ok((was_updated, summary_info)) = result { + /// //... + /// } + /// ``` + + pub fn retrieve_summary_info( + &mut self, + refresh_from_node: bool, + minimum_confirmations: u64, + ) -> Result<(bool, WalletInfo), Error> { + let mut w = self.wallet.lock(); + w.open_with_credentials()?; + let parent_key_id = w.parent_key_id(); + + let mut validated = false; + if refresh_from_node { + validated = self.update_outputs(&mut w, false); + } + + let wallet_info = updater::retrieve_info(&mut *w, &parent_key_id, minimum_confirmations)?; + let res = Ok((validated, wallet_info)); + + w.close()?; + res + } + + /// Initiates a new transaction as the sender, creating a new + /// [`Slate`](../../libtx/slate/struct.Slate.html) object containing + /// the sender's inputs, change outputs, and public signature data. This slate can + /// then be sent to the recipient to continue the transaction via the + /// [Foreign API's `receive_tx`](struct.APIForeign.html#method.receive_tx) method. + /// + /// When a transaction is created, the wallet must also lock inputs (and create unconfirmed + /// outputs) corresponding to the transaction created in the slate, so that the wallet doesn't + /// attempt to re-spend outputs that are already included in a transaction before the transaction + /// is confirmed. This method also returns a function that will perform that locking, and it is + /// up to the caller to decide the best time to call the lock function + /// (via the [`tx_lock_outputs`](struct.APIOwner.html#method.tx_lock_outputs) method). + /// If the exchange method is intended to be synchronous (such as via a direct http call,) + /// then the lock call can wait until the response is confirmed. If it is asynchronous, (such + /// as via file transfer,) the lock call should happen immediately (before the file is sent + /// to the recipient). + /// + /// # Arguments + /// * `src_acct_name` - The human readable account name from which to draw outputs + /// for the transaction, overriding whatever the active account is as set via the + /// [`set_active_account`](struct.APIOwner.html#method.set_active_account) method. + /// If None, the transaction will use the active account. + /// * `amount` - The amount to send, in nanogrins. (`1 G = 1_000_000_000nG`) + /// * `minimum_confirmations` - The minimum number of confirmations an output + /// should have in order to be included in the transaction. + /// * `max_outputs` - By default, the wallet selects as many inputs as possible in a + /// transaction, to reduce the Output set and the fees. The wallet will attempt to spend + /// include up to `max_outputs` in a transaction, however if this is not enough to cover + /// the whole amount, the wallet will include more outputs. This parameter should be considered + /// a soft limit. + /// * `num_change_outputs` - The target number of change outputs to create in the transaction. + /// The actual number created will be `num_change_outputs` + whatever remainder is needed. + /// * `selection_strategy_is_use_all` - If `true`, attempt to use up as many outputs as + /// possible to create the transaction, up the 'soft limit' of `max_outputs`. This helps + /// to reduce the size of the UTXO set and the amount of data stored in the wallet, and + /// minimizes fees. This will generally result in many inputs and a large change output(s), + /// usually much larger than the amount being sent. If `false`, the transaction will include + /// as many outputs as are needed to meet the amount, (and no more) starting with the smallest + /// value outputs. + /// * `message` - An optional participant message to include alongside the sender's public + /// ParticipantData within the slate. This message will include a signature created with the + /// sender's private keys, and will be publically verifiable. Note this message is for + /// the convenience of the participants during the exchange; it is not included in the final + /// transaction sent to the chain. The message will be truncated to 256 characters. + /// Validation of this message is optional. + /// + /// # Returns + /// * a result containing: + /// * ([`Slate`](../../libtx/slate/struct.Slate.html), lock_function) - A tuple: + /// * The transaction Slate, which can be forwarded to the recieving party by any means. + /// * A lock function, which should be called when the caller deems it appropriate to lock + /// the transaction outputs (i.e. there is relative certaintly that the slate will be + /// transmitted to the receiving party). Must be called before calling + /// [`finalize_tx`](struct.APIOwner.html#method.finalize_tx). + /// * or [`libwallet::Error`](../struct.Error.html) if an error is encountered. + /// + /// # Remarks + /// + /// * This method requires an active connection to a node, and will fail with error if a node + /// cannot be contacted to refresh output statuses. + /// * This method will store a partially completed transaction in the wallet's transaction log, + /// which will be updated on the corresponding call to [`finalize_tx`](struct.APIOwner.html#method.finalize_tx). + /// + /// # Example + /// Set up as in [new](struct.APIOwner.html#method.new) method above. + /// ``` + /// # extern crate grin_wallet as wallet; + /// # extern crate grin_keychain as keychain; + /// # extern crate grin_util as util; + /// # use std::sync::Arc; + /// # use util::Mutex; + /// # use keychain::ExtKeychain; + /// # use wallet::libwallet::api::APIOwner; + /// # use wallet::{LMDBBackend, HTTPNodeClient, WalletBackend, WalletConfig}; + /// # let mut wallet_config = WalletConfig::default(); + /// # wallet_config.data_file_dir = "test_output/doc/wallet1".to_owned(); + /// # let node_client = HTTPNodeClient::new(&wallet_config.check_node_api_http_addr, None); + /// # let mut wallet:Arc>> = + /// # Arc::new(Mutex::new( + /// # LMDBBackend::new(wallet_config.clone(), "", node_client).unwrap() + /// # )); + /// + /// let mut api_owner = APIOwner::new(wallet.clone()); + /// let amount = 2_000_000_000; + /// + /// // Attempt to create a transaction using the 'default' account + /// let result = api_owner.initiate_tx( + /// None, + /// amount, // amount + /// 10, // minimum confirmations + /// 500, // max outputs + /// 1, // num change outputs + /// true, // select all outputs + /// Some("Have some Grins. Love, Yeastplume".to_owned()), + /// ); + /// + /// if let Ok((slate, lock_fn)) = result { + /// // Send slate somehow + /// // ... + /// // Lock our outputs if we're happy the slate was (or is being) sent + /// api_owner.tx_lock_outputs(&slate, lock_fn); + /// } + /// ``` + + pub fn initiate_tx( + &mut self, + src_acct_name: Option<&str>, + amount: u64, + minimum_confirmations: u64, + max_outputs: usize, + num_change_outputs: usize, + selection_strategy_is_use_all: bool, + message: Option, + ) -> Result<(Slate, OutputLockFn), Error> { + let mut w = self.wallet.lock(); + w.open_with_credentials()?; + let parent_key_id = match src_acct_name { + Some(d) => { + let pm = w.get_acct_path(d.to_owned())?; + match pm { + Some(p) => p.path, + None => w.parent_key_id(), + } + } + None => w.parent_key_id(), + }; + + let message = match message { + Some(mut m) => { + m.truncate(USER_MESSAGE_MAX_LEN); + Some(m) + } + None => None, + }; + + let mut slate = tx::new_tx_slate(&mut *w, amount, 2)?; + + let (context, lock_fn) = tx::add_inputs_to_slate( + &mut *w, + &mut slate, + minimum_confirmations, + max_outputs, + num_change_outputs, + selection_strategy_is_use_all, + &parent_key_id, + 0, + message, + )?; + + // Save the aggsig context in our DB for when we + // recieve the transaction back + { + let mut batch = w.batch()?; + batch.save_private_context(slate.id.as_bytes(), &context)?; + batch.commit()?; + } + + w.close()?; + Ok((slate, lock_fn)) + } + + /// Estimates the amount to be locked and fee for the transaction without creating one + /// + /// # Arguments + /// * `src_acct_name` - The human readable account name from which to draw outputs + /// for the transaction, overriding whatever the active account is as set via the + /// [`set_active_account`](struct.APIOwner.html#method.set_active_account) method. + /// If None, the transaction will use the active account. + /// * `amount` - The amount to send, in nanogrins. (`1 G = 1_000_000_000nG`) + /// * `minimum_confirmations` - The minimum number of confirmations an output + /// should have in order to be included in the transaction. + /// * `max_outputs` - By default, the wallet selects as many inputs as possible in a + /// transaction, to reduce the Output set and the fees. The wallet will attempt to spend + /// include up to `max_outputs` in a transaction, however if this is not enough to cover + /// the whole amount, the wallet will include more outputs. This parameter should be considered + /// a soft limit. + /// * `num_change_outputs` - The target number of change outputs to create in the transaction. + /// The actual number created will be `num_change_outputs` + whatever remainder is needed. + /// * `selection_strategy_is_use_all` - If `true`, attempt to use up as many outputs as + /// possible to create the transaction, up the 'soft limit' of `max_outputs`. This helps + /// to reduce the size of the UTXO set and the amount of data stored in the wallet, and + /// minimizes fees. This will generally result in many inputs and a large change output(s), + /// usually much larger than the amount being sent. If `false`, the transaction will include + /// as many outputs as are needed to meet the amount, (and no more) starting with the smallest + /// value outputs. + /// + /// # Returns + /// * a result containing: + /// * (total, fee) - A tuple: + /// * Total amount to be locked. + /// * Transaction fee + pub fn estimate_initiate_tx( + &mut self, + src_acct_name: Option<&str>, + amount: u64, + minimum_confirmations: u64, + max_outputs: usize, + num_change_outputs: usize, + selection_strategy_is_use_all: bool, + ) -> Result< + ( + u64, // total + u64, // fee + ), + Error, + > { + let mut w = self.wallet.lock(); + w.open_with_credentials()?; + let parent_key_id = match src_acct_name { + Some(d) => { + let pm = w.get_acct_path(d.to_owned())?; + match pm { + Some(p) => p.path, + None => w.parent_key_id(), + } + } + None => w.parent_key_id(), + }; + tx::estimate_send_tx( + &mut *w, + amount, + minimum_confirmations, + max_outputs, + num_change_outputs, + selection_strategy_is_use_all, + &parent_key_id, + ) + } + + /// Lock outputs associated with a given slate/transaction + pub fn tx_lock_outputs( + &mut self, + slate: &Slate, + mut lock_fn: OutputLockFn, + ) -> Result<(), Error> { + let mut w = self.wallet.lock(); + w.open_with_credentials()?; + lock_fn(&mut *w, &slate.tx, PhantomData, PhantomData)?; + Ok(()) + } + + /// Sender finalization of the transaction. Takes the file returned by the + /// sender as well as the private file generate on the first send step. + /// Builds the complete transaction and sends it to a grin node for + /// propagation. + pub fn finalize_tx(&mut self, slate: &mut Slate) -> Result<(), Error> { + let mut w = self.wallet.lock(); + w.open_with_credentials()?; + let context = w.get_private_context(slate.id.as_bytes())?; + tx::complete_tx(&mut *w, slate, 0, &context)?; + tx::update_stored_tx(&mut *w, slate)?; + tx::update_message(&mut *w, slate)?; + { + let mut batch = w.batch()?; + batch.delete_private_context(slate.id.as_bytes())?; + batch.commit()?; + } + w.close()?; + Ok(()) + } + + /// Roll back a transaction and all associated outputs with a given + /// transaction id This means delete all change outputs, (or recipient + /// output if you're recipient), and unlock all locked outputs associated + /// with the transaction used when a transaction is created but never + /// posted + pub fn cancel_tx( + &mut self, + tx_id: Option, + tx_slate_id: Option, + ) -> Result<(), Error> { + let mut w = self.wallet.lock(); + w.open_with_credentials()?; + let parent_key_id = w.parent_key_id(); + if !self.update_outputs(&mut w, false) { + return Err(ErrorKind::TransactionCancellationError( + "Can't contact running Grin node. Not Cancelling.", + ))?; + } + tx::cancel_tx(&mut *w, &parent_key_id, tx_id, tx_slate_id)?; + w.close()?; + Ok(()) + } + + /// Retrieves a stored transaction from a TxLogEntry + pub fn get_stored_tx(&self, entry: &TxLogEntry) -> Result, Error> { + let w = self.wallet.lock(); + w.get_stored_tx(entry) + } + + /// Posts a transaction to the chain + pub fn post_tx(&self, tx: &Transaction, fluff: bool) -> Result<(), Error> { + let tx_hex = util::to_hex(ser::ser_vec(tx).unwrap()); + let client = { + let mut w = self.wallet.lock(); + w.w2n_client().clone() + }; + let res = client.post_tx(&TxWrapper { tx_hex: tx_hex }, fluff); + if let Err(e) = res { + error!("api: post_tx: failed with error: {}", e); + Err(e) + } else { + debug!( + "api: post_tx: successfully posted tx: {}, fluff? {}", + tx.hash(), + fluff + ); + Ok(()) + } + } + + /// Verifies all messages in the slate match their public keys + pub fn verify_slate_messages(&mut self, slate: &Slate) -> Result<(), Error> { + let secp = Secp256k1::with_caps(ContextFlag::VerifyOnly); + slate.verify_messages(&secp)?; + Ok(()) + } + + /// Attempt to restore contents of wallet + pub fn restore(&mut self) -> Result<(), Error> { + let mut w = self.wallet.lock(); + w.open_with_credentials()?; + w.restore()?; + w.close()?; + Ok(()) + } + + /// Attempt to check and fix the contents of the wallet + pub fn check_repair(&mut self) -> Result<(), Error> { + let mut w = self.wallet.lock(); + w.open_with_credentials()?; + self.update_outputs(&mut w, true); + w.check_repair()?; + w.close()?; + Ok(()) + } + + /// Retrieve current height from node + pub fn node_height(&mut self) -> Result<(u64, bool), Error> { + let res = { + let mut w = self.wallet.lock(); + w.open_with_credentials()?; + w.w2n_client().get_chain_height() + }; + match res { + Ok(height) => Ok((height, true)), + Err(_) => { + let outputs = self.retrieve_outputs(true, false, None)?; + let height = match outputs.1.iter().map(|(out, _)| out.height).max() { + Some(height) => height, + None => 0, + }; + Ok((height, false)) + } + } + } + + /// Attempt to update outputs in wallet, return whether it was successful + fn update_outputs(&self, w: &mut W, update_all: bool) -> bool { + let parent_key_id = w.parent_key_id(); + match updater::refresh_outputs(&mut *w, &parent_key_id, update_all) { + Ok(_) => true, + Err(_) => false, + } + } +} + +/// Wrapper around external API functions, intended to communicate +/// with other parties +pub struct APIForeign +where + W: WalletBackend, + C: NodeClient, + K: Keychain, +{ + /// Wallet, contains its keychain (TODO: Split these up into 2 traits + /// perhaps) + pub wallet: Arc>, + phantom: PhantomData, + phantom_c: PhantomData, +} + +impl<'a, W: ?Sized, C, K> APIForeign +where + W: WalletBackend, + C: NodeClient, + K: Keychain, +{ + /// Create new API instance + pub fn new(wallet_in: Arc>) -> Box { + Box::new(APIForeign { + wallet: wallet_in, + phantom: PhantomData, + phantom_c: PhantomData, + }) + } + + /// Build a new (potential) coinbase transaction in the wallet + pub fn build_coinbase(&mut self, block_fees: &BlockFees) -> Result { + let mut w = self.wallet.lock(); + w.open_with_credentials()?; + let res = updater::build_coinbase(&mut *w, block_fees); + w.close()?; + res + } + + /// Verifies all messages in the slate match their public keys + pub fn verify_slate_messages(&mut self, slate: &Slate) -> Result<(), Error> { + let secp = Secp256k1::with_caps(ContextFlag::VerifyOnly); + slate.verify_messages(&secp)?; + Ok(()) + } + + /// Receive a transaction from a sender + pub fn receive_tx( + &mut self, + slate: &mut Slate, + dest_acct_name: Option<&str>, + message: Option, + ) -> Result<(), Error> { + let mut w = self.wallet.lock(); + w.open_with_credentials()?; + let parent_key_id = match dest_acct_name { + Some(d) => { + let pm = w.get_acct_path(d.to_owned())?; + match pm { + Some(p) => p.path, + None => w.parent_key_id(), + } + } + None => w.parent_key_id(), + }; + // Don't do this multiple times + let tx = updater::retrieve_txs(&mut *w, None, Some(slate.id), Some(&parent_key_id), false)?; + for t in &tx { + if t.tx_type == TxLogEntryType::TxReceived { + return Err(ErrorKind::TransactionAlreadyReceived(slate.id.to_string()).into()); + } + } + + let message = match message { + Some(mut m) => { + m.truncate(USER_MESSAGE_MAX_LEN); + Some(m) + } + None => None, + }; + + let (_, mut create_fn) = + tx::add_output_to_slate(&mut *w, slate, &parent_key_id, 1, message)?; + create_fn(&mut *w, &slate.tx, PhantomData, PhantomData)?; + tx::update_message(&mut *w, slate)?; + w.close()?; + Ok(()) + } +} diff --git a/libwallet/src/error.rs b/libwallet/src/error.rs new file mode 100644 index 0000000..491e78e --- /dev/null +++ b/libwallet/src/error.rs @@ -0,0 +1,300 @@ +// Copyright 2018 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Error types for libwallet + +use crate::core::core::{committed, transaction}; +use crate::core::libtx; +use crate::keychain; +use crate::util::secp; +use failure::{Backtrace, Context, Fail}; +use std::env; +use std::fmt::{self, Display}; +use std::io; + +/// Error definition +#[derive(Debug, Fail)] +pub struct Error { + inner: Context, +} + +/// Wallet errors, mostly wrappers around underlying crypto or I/O errors. +#[derive(Clone, Eq, PartialEq, Debug, Fail)] +pub enum ErrorKind { + /// Not enough funds + #[fail( + display = "Not enough funds. Required: {}, Available: {}", + needed_disp, available_disp + )] + NotEnoughFunds { + /// available funds + available: u64, + /// Display friendly + available_disp: String, + /// Needed funds + needed: u64, + /// Display friendly + needed_disp: String, + }, + + /// Fee error + #[fail(display = "Fee Error: {}", _0)] + Fee(String), + + /// LibTX Error + #[fail(display = "LibTx Error")] + LibTX(libtx::ErrorKind), + + /// Keychain error + #[fail(display = "Keychain error")] + Keychain(keychain::Error), + + /// Transaction Error + #[fail(display = "Transaction error")] + Transaction(transaction::Error), + + /// API Error + #[fail(display = "Client Callback Error: {}", _0)] + ClientCallback(String), + + /// Secp Error + #[fail(display = "Secp error")] + Secp(secp::Error), + + /// Callback implementation error conversion + #[fail(display = "Trait Implementation error")] + CallbackImpl(&'static str), + + /// Wallet backend error + #[fail(display = "Wallet store error: {}", _0)] + Backend(String), + + /// Callback implementation error conversion + #[fail(display = "Restore Error")] + Restore, + + /// An error in the format of the JSON structures exchanged by the wallet + #[fail(display = "JSON format error: {}", _0)] + Format(String), + + /// Other serialization errors + #[fail(display = "Ser/Deserialization error")] + Deser(crate::core::ser::Error), + + /// IO Error + #[fail(display = "I/O error")] + IO, + + /// Error when contacting a node through its API + #[fail(display = "Node API error")] + Node, + + /// Error contacting wallet API + #[fail(display = "Wallet Communication Error: {}", _0)] + WalletComms(String), + + /// Error originating from hyper. + #[fail(display = "Hyper error")] + Hyper, + + /// Error originating from hyper uri parsing. + #[fail(display = "Uri parsing error")] + Uri, + + /// Signature error + #[fail(display = "Signature error: {}", _0)] + Signature(String), + + /// Attempt to use duplicate transaction id in separate transactions + #[fail(display = "Duplicate transaction ID error")] + DuplicateTransactionId, + + /// Wallet seed already exists + #[fail(display = "Wallet seed exists error")] + WalletSeedExists, + + /// Wallet seed doesn't exist + #[fail(display = "Wallet seed doesn't exist error")] + WalletSeedDoesntExist, + + /// Wallet seed doesn't exist + #[fail(display = "Wallet seed decryption error")] + WalletSeedDecryption, + + /// Transaction doesn't exist + #[fail(display = "Transaction {} doesn't exist", _0)] + TransactionDoesntExist(String), + + /// Transaction already rolled back + #[fail(display = "Transaction {} cannot be cancelled", _0)] + TransactionNotCancellable(String), + + /// Cancellation error + #[fail(display = "Cancellation Error: {}", _0)] + TransactionCancellationError(&'static str), + + /// Cancellation error + #[fail(display = "Tx dump Error: {}", _0)] + TransactionDumpError(&'static str), + + /// Attempt to repost a transaction that's already confirmed + #[fail(display = "Transaction already confirmed error")] + TransactionAlreadyConfirmed, + + /// Transaction has already been received + #[fail(display = "Transaction {} has already been received", _0)] + TransactionAlreadyReceived(String), + + /// Attempt to repost a transaction that's not completed and stored + #[fail(display = "Transaction building not completed: {}", _0)] + TransactionBuildingNotCompleted(u32), + + /// Invalid BIP-32 Depth + #[fail(display = "Invalid BIP32 Depth (must be 1 or greater)")] + InvalidBIP32Depth, + + /// Attempt to add an account that exists + #[fail(display = "Account Label '{}' already exists", _0)] + AccountLabelAlreadyExists(String), + + /// Reference unknown account label + #[fail(display = "Unknown Account Label '{}'", _0)] + UnknownAccountLabel(String), + + /// Error from summing commitments via committed trait. + #[fail(display = "Committed Error")] + Committed(committed::Error), + + /// Other + #[fail(display = "Generic error: {}", _0)] + GenericError(String), +} + +impl Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let show_bt = match env::var("RUST_BACKTRACE") { + Ok(r) => { + if r == "1" { + true + } else { + false + } + } + Err(_) => false, + }; + let backtrace = match self.backtrace() { + Some(b) => format!("{}", b), + None => String::from("Unknown"), + }; + let inner_output = format!("{}", self.inner,); + let backtrace_output = format!("\n Backtrace: {}", backtrace); + let mut output = inner_output.clone(); + if show_bt { + output.push_str(&backtrace_output); + } + Display::fmt(&output, f) + } +} + +impl Error { + /// get kind + pub fn kind(&self) -> ErrorKind { + self.inner.get_context().clone() + } + /// get cause string + pub fn cause_string(&self) -> String { + match self.cause() { + Some(k) => format!("{}", k), + None => format!("Unknown"), + } + } + /// get cause + pub fn cause(&self) -> Option<&dyn Fail> { + self.inner.cause() + } + /// get backtrace + pub fn backtrace(&self) -> Option<&Backtrace> { + self.inner.backtrace() + } +} + +impl From for Error { + fn from(kind: ErrorKind) -> Error { + Error { + inner: Context::new(kind), + } + } +} + +impl From> for Error { + fn from(inner: Context) -> Error { + Error { inner: inner } + } +} + +impl From for Error { + fn from(_error: io::Error) -> Error { + Error { + inner: Context::new(ErrorKind::IO), + } + } +} + +impl From for Error { + fn from(error: keychain::Error) -> Error { + Error { + inner: Context::new(ErrorKind::Keychain(error)), + } + } +} + +impl From for Error { + fn from(error: crate::core::libtx::Error) -> Error { + Error { + inner: Context::new(ErrorKind::LibTX(error.kind())), + } + } +} + +impl From for Error { + fn from(error: transaction::Error) -> Error { + Error { + inner: Context::new(ErrorKind::Transaction(error)), + } + } +} + +impl From for Error { + fn from(error: crate::core::ser::Error) -> Error { + Error { + inner: Context::new(ErrorKind::Deser(error)), + } + } +} + +impl From for Error { + fn from(error: secp::Error) -> Error { + Error { + inner: Context::new(ErrorKind::Secp(error)), + } + } +} + +impl From for Error { + fn from(error: committed::Error) -> Error { + Error { + inner: Context::new(ErrorKind::Committed(error)), + } + } +} diff --git a/libwallet/src/internal.rs b/libwallet/src/internal.rs new file mode 100644 index 0000000..63c886e --- /dev/null +++ b/libwallet/src/internal.rs @@ -0,0 +1,28 @@ +// Copyright 2018 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! lower-level wallet functions which build upon core::libtx to perform wallet +//! operations + +#![deny(non_upper_case_globals)] +#![deny(non_camel_case_types)] +#![deny(non_snake_case)] +#![deny(unused_mut)] +#![warn(missing_docs)] + +pub mod keys; +pub mod restore; +pub mod selection; +pub mod tx; +pub mod updater; diff --git a/libwallet/src/internal/keys.rs b/libwallet/src/internal/keys.rs new file mode 100644 index 0000000..cbbfebf --- /dev/null +++ b/libwallet/src/internal/keys.rs @@ -0,0 +1,120 @@ +// Copyright 2018 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Wallet key management functions +use crate::keychain::{ChildNumber, ExtKeychain, Identifier, Keychain}; +use crate::error::{Error, ErrorKind}; +use crate::types::{AcctPathMapping, NodeClient, WalletBackend}; + +/// Get next available key in the wallet for a given parent +pub fn next_available_key(wallet: &mut T) -> Result +where + T: WalletBackend, + C: NodeClient, + K: Keychain, +{ + let child = wallet.next_child()?; + Ok(child) +} + +/// Retrieve an existing key from a wallet +pub fn retrieve_existing_key( + wallet: &T, + key_id: Identifier, + mmr_index: Option, +) -> Result<(Identifier, u32), Error> +where + T: WalletBackend, + C: NodeClient, + K: Keychain, +{ + 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(wallet: &mut T) -> Result, Error> +where + T: WalletBackend, + C: NodeClient, + K: Keychain, +{ + Ok(wallet.acct_path_iter().collect()) +} + +/// Adds an new parent account path with a given label +pub fn new_acct_path(wallet: &mut T, label: &str) -> Result +where + T: WalletBackend, + C: NodeClient, + K: Keychain, +{ + let label = label.to_owned(); + if let Some(_) = wallet.acct_path_iter().find(|l| l.label == label) { + return Err(ErrorKind::AccountLabelAlreadyExists(label.clone()).into()); + } + + // We're always using paths at m/k/0 for parent keys for output derivations + // so find the highest of those, then increment (to conform with external/internal + // derivation chains in BIP32 spec) + + let highest_entry = wallet.acct_path_iter().max_by(|a, b| { + ::from(a.path.to_path().path[0]).cmp(&::from(b.path.to_path().path[0])) + }); + + let return_id = { + if let Some(e) = highest_entry { + let mut p = e.path.to_path(); + p.path[0] = ChildNumber::from(::from(p.path[0]) + 1); + p.to_identifier() + } else { + ExtKeychain::derive_key_id(2, 0, 0, 0, 0) + } + }; + + let save_path = AcctPathMapping { + label: label.to_owned(), + path: return_id.clone(), + }; + + let mut batch = wallet.batch()?; + 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( + wallet: &mut T, + label: &str, + path: &Identifier, +) -> Result<(), Error> +where + T: WalletBackend, + C: NodeClient, + K: Keychain, +{ + let label = label.to_owned(); + let save_path = AcctPathMapping { + label: label.to_owned(), + path: path.clone(), + }; + + let mut batch = wallet.batch()?; + batch.save_acct_path(save_path)?; + batch.commit()?; + Ok(()) +} diff --git a/libwallet/src/internal/restore.rs b/libwallet/src/internal/restore.rs new file mode 100644 index 0000000..117c904 --- /dev/null +++ b/libwallet/src/internal/restore.rs @@ -0,0 +1,452 @@ +// Copyright 2018 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//! Functions to restore a wallet's outputs from just the master seed + +use crate::core::global; +use crate::core::libtx::proof; +use crate::keychain::{ExtKeychain, Identifier, Keychain}; +use crate::internal::{keys, updater}; +use crate::types::*; +use crate::Error; +use crate::util::secp::{key::SecretKey, pedersen}; +use std::collections::HashMap; + +/// Utility struct for return values from below +#[derive(Clone)] +struct OutputResult { + /// + pub commit: pedersen::Commitment, + /// + pub key_id: Identifier, + /// + pub n_child: u32, + /// + pub mmr_index: u64, + /// + pub value: u64, + /// + pub height: u64, + /// + pub lock_height: u64, + /// + pub is_coinbase: bool, + /// + pub blinding: SecretKey, +} + +#[derive(Debug, Clone)] +/// Collect stats in case we want to just output a single tx log entry +/// for restored non-coinbase outputs +struct RestoredTxStats { + /// + pub log_id: u32, + /// + pub amount_credited: u64, + /// + pub num_outputs: usize, +} + +fn identify_utxo_outputs( + wallet: &mut T, + outputs: Vec<(pedersen::Commitment, pedersen::RangeProof, bool, u64, u64)>, +) -> Result, Error> +where + T: WalletBackend, + C: NodeClient, + K: Keychain, +{ + let mut wallet_outputs: Vec = Vec::new(); + + warn!( + "Scanning {} outputs in the current Grin utxo set", + outputs.len(), + ); + + for output in outputs.iter() { + let (commit, proof, is_coinbase, height, mmr_index) = output; + // attempt to unwind message from the RP and get a value + // will fail if it's not ours + let info = proof::rewind(wallet.keychain(), *commit, None, *proof)?; + + if !info.success { + continue; + } + + let lock_height = if *is_coinbase { + *height + global::coinbase_maturity() + } else { + *height + }; + + // TODO: Output paths are always going to be length 3 for now, but easy enough to grind + // through to find the right path if required later + let key_id = Identifier::from_serialized_path(3u8, &info.message.as_bytes()); + + info!( + "Output found: {:?}, amount: {:?}, key_id: {:?}, mmr_index: {},", + commit, info.value, key_id, mmr_index, + ); + + wallet_outputs.push(OutputResult { + commit: *commit, + key_id: key_id.clone(), + n_child: key_id.to_path().last_path_index(), + value: info.value, + height: *height, + lock_height: lock_height, + is_coinbase: *is_coinbase, + blinding: info.blinding, + mmr_index: *mmr_index, + }); + } + Ok(wallet_outputs) +} + +fn collect_chain_outputs(wallet: &mut T) -> Result, Error> +where + T: WalletBackend, + C: NodeClient, + K: Keychain, +{ + let batch_size = 1000; + let mut start_index = 1; + let mut result_vec: Vec = vec![]; + loop { + let (highest_index, last_retrieved_index, outputs) = wallet + .w2n_client() + .get_outputs_by_pmmr_index(start_index, batch_size)?; + warn!( + "Checking {} outputs, up to index {}. (Highest index: {})", + outputs.len(), + highest_index, + last_retrieved_index, + ); + + result_vec.append(&mut identify_utxo_outputs(wallet, outputs.clone())?); + + if highest_index == last_retrieved_index { + break; + } + start_index = last_retrieved_index + 1; + } + Ok(result_vec) +} + +/// +fn restore_missing_output( + wallet: &mut T, + output: OutputResult, + found_parents: &mut HashMap, + tx_stats: &mut Option<&mut HashMap>, +) -> Result<(), Error> +where + T: WalletBackend, + C: NodeClient, + K: Keychain, +{ + let commit = wallet.calc_commit_for_cache(output.value, &output.key_id)?; + let mut batch = wallet.batch()?; + + let parent_key_id = output.key_id.parent_path(); + if !found_parents.contains_key(&parent_key_id) { + found_parents.insert(parent_key_id.clone(), 0); + if let Some(ref mut s) = tx_stats { + s.insert( + parent_key_id.clone(), + RestoredTxStats { + log_id: batch.next_tx_log_id(&parent_key_id)?, + amount_credited: 0, + num_outputs: 0, + }, + ); + } + } + + let log_id = if tx_stats.is_none() || output.is_coinbase { + let log_id = batch.next_tx_log_id(&parent_key_id)?; + let entry_type = match output.is_coinbase { + true => TxLogEntryType::ConfirmedCoinbase, + false => TxLogEntryType::TxReceived, + }; + let mut t = TxLogEntry::new(parent_key_id.clone(), entry_type, log_id); + t.confirmed = true; + t.amount_credited = output.value; + t.num_outputs = 1; + t.update_confirmation_ts(); + batch.save_tx_log_entry(t, &parent_key_id)?; + log_id + } else { + if let Some(ref mut s) = tx_stats { + let ts = s.get(&parent_key_id).unwrap().clone(); + s.insert( + parent_key_id.clone(), + RestoredTxStats { + log_id: ts.log_id, + amount_credited: ts.amount_credited + output.value, + num_outputs: ts.num_outputs + 1, + }, + ); + ts.log_id + } else { + 0 + } + }; + + let _ = batch.save(OutputData { + root_key_id: parent_key_id.clone(), + key_id: output.key_id, + n_child: output.n_child, + mmr_index: Some(output.mmr_index), + commit: commit, + value: output.value, + status: OutputStatus::Unspent, + height: output.height, + lock_height: output.lock_height, + is_coinbase: output.is_coinbase, + tx_log_entry: Some(log_id), + }); + + let max_child_index = found_parents.get(&parent_key_id).unwrap().clone(); + if output.n_child >= max_child_index { + found_parents.insert(parent_key_id.clone(), output.n_child); + } + + batch.commit()?; + Ok(()) +} + +/// +fn cancel_tx_log_entry(wallet: &mut T, output: &OutputData) -> Result<(), Error> +where + T: WalletBackend, + C: NodeClient, + K: Keychain, +{ + let parent_key_id = output.key_id.parent_path(); + let updated_tx_entry = if output.tx_log_entry.is_some() { + let entries = updater::retrieve_txs( + wallet, + output.tx_log_entry.clone(), + None, + Some(&parent_key_id), + false, + )?; + if entries.len() > 0 { + let mut entry = entries[0].clone(); + match entry.tx_type { + TxLogEntryType::TxSent => entry.tx_type = TxLogEntryType::TxSentCancelled, + TxLogEntryType::TxReceived => entry.tx_type = TxLogEntryType::TxReceivedCancelled, + _ => {} + } + Some(entry) + } else { + None + } + } else { + None + }; + let mut batch = wallet.batch()?; + if let Some(t) = updated_tx_entry { + batch.save_tx_log_entry(t, &parent_key_id)?; + } + batch.commit()?; + Ok(()) +} + +/// Check / repair wallet contents +/// assume wallet contents have been freshly updated with contents +/// of latest block +pub fn check_repair(wallet: &mut T) -> Result<(), Error> +where + T: WalletBackend, + C: NodeClient, + K: Keychain, +{ + // First, get a definitive list of outputs we own from the chain + warn!("Starting wallet check."); + let chain_outs = collect_chain_outputs(wallet)?; + warn!( + "Identified {} wallet_outputs as belonging to this wallet", + chain_outs.len(), + ); + + // Now, get all outputs owned by this wallet (regardless of account) + let wallet_outputs = { + let res = updater::retrieve_outputs(&mut *wallet, true, None, None)?; + res + }; + + let mut missing_outs = vec![]; + let mut accidental_spend_outs = vec![]; + let mut locked_outs = vec![]; + + // check all definitive outputs exist in the wallet outputs + for deffo in chain_outs.into_iter() { + let matched_out = wallet_outputs.iter().find(|wo| wo.1 == deffo.commit); + match matched_out { + Some(s) => { + if s.0.status == OutputStatus::Spent { + accidental_spend_outs.push((s.0.clone(), deffo.clone())); + } + if s.0.status == OutputStatus::Locked { + locked_outs.push((s.0.clone(), deffo.clone())); + } + } + None => missing_outs.push(deffo), + } + } + + // mark problem spent outputs as unspent (confirmed against a short-lived fork, for example) + for m in accidental_spend_outs.into_iter() { + let mut o = m.0; + warn!( + "Output for {} with ID {} ({:?}) marked as spent but exists in UTXO set. \ + Marking unspent and cancelling any associated transaction log entries.", + o.value, o.key_id, m.1.commit, + ); + o.status = OutputStatus::Unspent; + // any transactions associated with this should be cancelled + cancel_tx_log_entry(wallet, &o)?; + let mut batch = wallet.batch()?; + batch.save(o)?; + batch.commit()?; + } + + let mut found_parents: HashMap = HashMap::new(); + + // Restore missing outputs, adding transaction for it back to the log + for m in missing_outs.into_iter() { + warn!( + "Confirmed output for {} with ID {} ({:?}) exists in UTXO set but not in wallet. \ + Restoring.", + m.value, m.key_id, m.commit, + ); + restore_missing_output(wallet, m, &mut found_parents, &mut None)?; + } + + // Unlock locked outputs + for m in locked_outs.into_iter() { + let mut o = m.0; + warn!( + "Confirmed output for {} with ID {} ({:?}) exists in UTXO set and is locked. \ + Unlocking and cancelling associated transaction log entries.", + o.value, o.key_id, m.1.commit, + ); + o.status = OutputStatus::Unspent; + cancel_tx_log_entry(wallet, &o)?; + let mut batch = wallet.batch()?; + batch.save(o)?; + batch.commit()?; + } + + let unconfirmed_outs: Vec<&(OutputData, pedersen::Commitment)> = wallet_outputs + .iter() + .filter(|o| o.0.status == OutputStatus::Unconfirmed) + .collect(); + // Delete unconfirmed outputs + for m in unconfirmed_outs.into_iter() { + let o = m.0.clone(); + warn!( + "Unconfirmed output for {} with ID {} ({:?}) not in UTXO set. \ + Deleting and cancelling associated transaction log entries.", + o.value, o.key_id, m.1, + ); + cancel_tx_log_entry(wallet, &o)?; + let mut batch = wallet.batch()?; + batch.delete(&o.key_id, &o.mmr_index)?; + batch.commit()?; + } + + // restore labels, account paths and child derivation indices + let label_base = "account"; + let mut acct_index = 1; + for (path, max_child_index) in found_parents.iter() { + // default path already exists + if *path != ExtKeychain::derive_key_id(2, 0, 0, 0, 0) { + let label = format!("{}_{}", label_base, acct_index); + keys::set_acct_path(wallet, &label, path)?; + acct_index += 1; + } + let mut batch = wallet.batch()?; + debug!("Next child for account {} is {}", path, max_child_index + 1); + batch.save_child_index(path, max_child_index + 1)?; + batch.commit()?; + } + Ok(()) +} + +/// Restore a wallet +pub fn restore(wallet: &mut T) -> Result<(), Error> +where + T: WalletBackend, + C: NodeClient, + K: Keychain, +{ + // Don't proceed if wallet_data has anything in it + let is_empty = wallet.iter().next().is_none(); + if !is_empty { + error!("Not restoring. Please back up and remove existing db directory first."); + return Ok(()); + } + + warn!("Starting restore."); + + let result_vec = collect_chain_outputs(wallet)?; + + warn!( + "Identified {} wallet_outputs as belonging to this wallet", + result_vec.len(), + ); + + let mut found_parents: HashMap = HashMap::new(); + let mut restore_stats = HashMap::new(); + + // Now save what we have + for output in result_vec { + restore_missing_output( + wallet, + output, + &mut found_parents, + &mut Some(&mut restore_stats), + )?; + } + + // restore labels, account paths and child derivation indices + let label_base = "account"; + let mut acct_index = 1; + for (path, max_child_index) in found_parents.iter() { + // default path already exists + if *path != ExtKeychain::derive_key_id(2, 0, 0, 0, 0) { + let label = format!("{}_{}", label_base, acct_index); + keys::set_acct_path(wallet, &label, path)?; + acct_index += 1; + } + // restore tx log entry for non-coinbase outputs + if let Some(s) = restore_stats.get(path) { + let mut batch = wallet.batch()?; + let mut t = TxLogEntry::new(path.clone(), TxLogEntryType::TxReceived, s.log_id); + t.confirmed = true; + t.amount_credited = s.amount_credited; + t.num_outputs = s.num_outputs; + t.update_confirmation_ts(); + batch.save_tx_log_entry(t, &path)?; + batch.commit()?; + } + let mut batch = wallet.batch()?; + batch.save_child_index(path, max_child_index + 1)?; + debug!("Next child for account {} is {}", path, max_child_index + 1); + batch.commit()?; + } + Ok(()) +} diff --git a/libwallet/src/internal/selection.rs b/libwallet/src/internal/selection.rs new file mode 100644 index 0000000..fbe25d1 --- /dev/null +++ b/libwallet/src/internal/selection.rs @@ -0,0 +1,547 @@ +// Copyright 2018 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Selection of inputs for building transactions + +use crate::core::core::{amount_to_hr_string, Transaction}; +use crate::core::libtx::{build, tx_fee}; +use crate::keychain::{Identifier, Keychain}; +use crate::error::{Error, ErrorKind}; +use crate::internal::keys; +use crate::slate::Slate; +use crate::types::*; +use std::collections::HashMap; +use std::marker::PhantomData; + +/// 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( + wallet: &mut T, + slate: &mut Slate, + minimum_confirmations: u64, + max_outputs: usize, + change_outputs: usize, + selection_strategy_is_use_all: bool, + parent_key_id: Identifier, +) -> Result<(Context, OutputLockFn), Error> +where + T: WalletBackend, + C: NodeClient, + K: Keychain, +{ + let (elems, inputs, change_amounts_derivations, fee) = select_send_tx( + wallet, + slate.amount, + slate.height, + minimum_confirmations, + slate.lock_height, + max_outputs, + change_outputs, + selection_strategy_is_use_all, + &parent_key_id, + )?; + + slate.fee = fee; + + let keychain = wallet.keychain().clone(); + let blinding = slate.add_transaction_elements(&keychain, elems)?; + + // Create our own private context + let mut context = Context::new( + wallet.keychain().secp(), + blinding.secret_key(&keychain.secp()).unwrap(), + ); + + // Store our private identifiers for each input + for input in inputs { + context.add_input(&input.key_id, &input.mmr_index); + } + + 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); + commits.insert( + id.clone(), + wallet.calc_commit_for_cache(*change_amount, &id)?, + ); + } + + let lock_inputs_in = context.get_inputs().clone(); + let _lock_outputs = context.get_outputs().clone(); + let messages_in = Some(slate.participant_messages()); + let slate_id_in = slate.id.clone(); + let height_in = slate.height; + + // Return a closure to acquire wallet lock and lock the coins being spent + // so we avoid accidental double spend attempt. + let update_sender_wallet_fn = + move |wallet: &mut T, tx: &Transaction, _: PhantomData, _: PhantomData| { + let tx_entry = { + // These ensure the closure remains FnMut + let lock_inputs = lock_inputs_in.clone(); + let messages = messages_in.clone(); + let slate_id = slate_id_in.clone(); + let height = height_in.clone(); + let mut batch = wallet.batch()?; + 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.clone()); + let filename = format!("{}.grintx", slate_id); + t.stored_tx = Some(filename); + t.fee = Some(fee); + 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 = amount_debited + coin.value; + batch.lock_output(&mut coin)?; + } + + t.amount_debited = amount_debited; + t.messages = messages; + + // write the output representing our change + for (change_amount, id, _) in &change_amounts_derivations { + t.num_outputs += 1; + t.amount_credited += change_amount; + let commit = commits.get(&id).unwrap().clone(); + batch.save(OutputData { + root_key_id: parent_key_id.clone(), + key_id: id.clone(), + n_child: id.to_path().last_path_index(), + commit: commit, + mmr_index: None, + value: change_amount.clone(), + status: OutputStatus::Unconfirmed, + height: height, + lock_height: 0, + is_coinbase: false, + tx_log_entry: Some(log_id), + })?; + } + batch.save_tx_log_entry(t.clone(), &parent_key_id)?; + batch.commit()?; + t + }; + wallet.store_tx(&format!("{}", tx_entry.tx_slate_id.unwrap()), tx)?; + Ok(()) + }; + + Ok((context, Box::new(update_sender_wallet_fn))) +} + +/// Creates a new output in the wallet for the recipient, +/// returning the key of the fresh output and a closure +/// that actually performs the addition of the output to the +/// wallet +pub fn build_recipient_output( + wallet: &mut T, + slate: &mut Slate, + parent_key_id: Identifier, +) -> Result<(Identifier, Context, OutputLockFn), Error> +where + T: WalletBackend, + C: NodeClient, + K: Keychain, +{ + // Create a potential output for this transaction + let key_id = keys::next_available_key(wallet).unwrap(); + + let keychain = wallet.keychain().clone(); + let key_id_inner = key_id.clone(); + let amount = slate.amount; + let height = slate.height; + + let slate_id = slate.id.clone(); + let blinding = + slate.add_transaction_elements(&keychain, vec![build::output(amount, key_id.clone())])?; + + // Add blinding sum to our context + let mut context = Context::new( + keychain.secp(), + blinding + .secret_key(wallet.keychain().clone().secp()) + .unwrap(), + ); + + context.add_output(&key_id, &None); + let messages_in = Some(slate.participant_messages()); + + // Create closure that adds the output to recipient's wallet + // (up to the caller to decide when to do) + let wallet_add_fn = + move |wallet: &mut T, _tx: &Transaction, _: PhantomData, _: PhantomData| { + // Ensure closure remains FnMut + let messages = messages_in.clone(); + let commit = wallet.calc_commit_for_cache(amount, &key_id_inner)?; + let mut batch = wallet.batch()?; + 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.messages = messages; + batch.save(OutputData { + root_key_id: parent_key_id.clone(), + key_id: key_id_inner.clone(), + mmr_index: None, + n_child: key_id_inner.to_path().last_path_index(), + commit: commit, + value: amount, + status: OutputStatus::Unconfirmed, + height: height, + lock_height: 0, + is_coinbase: false, + tx_log_entry: Some(log_id), + })?; + batch.save_tx_log_entry(t, &parent_key_id)?; + batch.commit()?; + //TODO: Check whether we want to call this + //wallet.store_tx(&format!("{}", t.tx_slate_id.unwrap()), tx)?; + Ok(()) + }; + Ok((key_id, context, Box::new(wallet_add_fn))) +} + +/// 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( + wallet: &mut T, + amount: u64, + current_height: u64, + minimum_confirmations: u64, + lock_height: u64, + max_outputs: usize, + change_outputs: usize, + selection_strategy_is_use_all: bool, + parent_key_id: &Identifier, +) -> Result< + ( + Vec>>, + Vec, + Vec<(u64, Identifier, Option)>, // change amounts and derivations + u64, // fee + ), + Error, +> +where + T: WalletBackend, + C: NodeClient, + K: Keychain, +{ + let (coins, _total, amount, fee) = select_coins_and_fee( + wallet, + amount, + current_height, + minimum_confirmations, + max_outputs, + change_outputs, + selection_strategy_is_use_all, + &parent_key_id, + )?; + + // build transaction skeleton with inputs and change + let (mut parts, change_amounts_derivations) = + inputs_and_change(&coins, wallet, amount, fee, change_outputs)?; + + // This is more proof of concept than anything but here we set lock_height + // on tx being sent (based on current chain height via api). + parts.push(build::with_lock_height(lock_height)); + + Ok((parts, coins, change_amounts_derivations, fee)) +} + +/// Select outputs and calculating fee. +pub fn select_coins_and_fee( + wallet: &mut T, + amount: u64, + 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, + C: NodeClient, + K: Keychain, +{ + // 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 + + // TODO - Is it safe to spend without a change output? (1 input -> 1 output) + // TODO - Does this not potentially reveal the senders private key? + // + // First attempt to spend without change + let mut fee = tx_fee(coins.len(), 1, 1, None); + let mut total: u64 = coins.iter().map(|c| c.value).sum(); + let mut amount_with_fee = amount + fee; + + if total == 0 { + return Err(ErrorKind::NotEnoughFunds { + available: 0, + available_disp: amount_to_hr_string(0, false), + needed: amount_with_fee as u64, + needed_disp: amount_to_hr_string(amount_with_fee as u64, false), + })?; + } + + // The amount with fee is more than the total values of our max outputs + if total < amount_with_fee && coins.len() == max_outputs { + return Err(ErrorKind::NotEnoughFunds { + available: total, + available_disp: amount_to_hr_string(total, false), + needed: amount_with_fee as u64, + needed_disp: amount_to_hr_string(amount_with_fee as u64, false), + })?; + } + + let num_outputs = change_outputs + 1; + + // We need to add a change address or amount with fee is more than total + if total != amount_with_fee { + fee = tx_fee(coins.len(), num_outputs, 1, None); + amount_with_fee = 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(ErrorKind::NotEnoughFunds { + available: total as u64, + available_disp: amount_to_hr_string(total, false), + needed: amount_with_fee as u64, + needed_disp: amount_to_hr_string(amount_with_fee as u64, false), + })?; + } + + // select some spendable coins from the wallet + coins = select_coins( + wallet, + amount_with_fee, + current_height, + minimum_confirmations, + max_outputs, + selection_strategy_is_use_all, + parent_key_id, + ) + .1; + fee = tx_fee(coins.len(), num_outputs, 1, None); + total = coins.iter().map(|c| c.value).sum(); + amount_with_fee = amount + fee; + } + } + Ok((coins, total, amount, fee)) +} + +/// Selects inputs and change for a transaction +pub fn inputs_and_change( + coins: &Vec, + wallet: &mut T, + amount: u64, + fee: u64, + num_change_outputs: usize, +) -> Result< + ( + Vec>>, + Vec<(u64, Identifier, Option)>, + ), + Error, +> +where + T: WalletBackend, + C: NodeClient, + K: Keychain, +{ + 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(); + + parts.push(build::with_fee(fee)); + + // 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 + 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().unwrap(); + + change_amounts_derivations.push((change_amount, change_key.clone(), None)); + parts.push(build::output(change_amount, change_key)); + } + } + + Ok((parts, change_amounts_derivations)) +} + +/// Select spendable coins from a wallet. +/// Default strategy is to spend the maximum number of outputs (up to +/// max_outputs). Alternative strategy is to spend smallest outputs first +/// but only as many as necessary. When we introduce additional strategies +/// we should pass something other than a bool in. +/// TODO: Possibly move this into another trait to be owned by a wallet? + +pub fn select_coins( + 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, + C: NodeClient, + K: Keychain, +{ + // 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.iter().cloned().collect::>(); + 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 { + return Some(outputs.iter().cloned().collect()); + } else { + let mut selected_amount = 0; + return Some( + outputs + .iter() + .take_while(|out| { + let res = selected_amount < amount; + selected_amount += out.value; + res + }) + .cloned() + .collect(), + ); + } + } else { + None + } +} diff --git a/libwallet/src/internal/tx.rs b/libwallet/src/internal/tx.rs new file mode 100644 index 0000000..510e360 --- /dev/null +++ b/libwallet/src/internal/tx.rs @@ -0,0 +1,321 @@ +// Copyright 2018 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Transaction building functions + +use uuid::Uuid; + +use crate::keychain::{Identifier, Keychain}; +use crate::internal::{selection, updater}; +use crate::slate::Slate; +use crate::types::{Context, NodeClient, OutputLockFn, TxLogEntryType, WalletBackend}; +use crate::{Error, ErrorKind}; + +/// 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( + wallet: &mut T, + amount: u64, + num_participants: usize, +) -> Result +where + T: WalletBackend, + C: NodeClient, + K: Keychain, +{ + let current_height = wallet.w2n_client().get_chain_height()?; + let mut slate = Slate::blank(num_participants); + slate.amount = amount; + slate.height = current_height; + slate.lock_height = current_height; + Ok(slate) +} + +/// Estimates locked amount and fee for the transaction without creating one +pub fn estimate_send_tx( + wallet: &mut T, + amount: u64, + minimum_confirmations: u64, + max_outputs: usize, + num_change_outputs: usize, + selection_strategy_is_use_all: bool, + parent_key_id: &Identifier, +) -> Result< + ( + u64, // total + u64, // fee + ), + Error, +> +where + T: WalletBackend, + C: NodeClient, + K: Keychain, +{ + // Get lock height + let current_height = wallet.w2n_client().get_chain_height()?; + // ensure outputs we're selecting are up to date + updater::refresh_outputs(wallet, parent_key_id, false)?; + + // Sender selects outputs into a new slate and save our corresponding keys in + // a transaction context. The secret key in our transaction context will be + // randomly selected. This returns the public slate, and a closure that locks + // our inputs and outputs once we're convinced the transaction exchange went + // according to plan + // This function is just a big helper to do all of that, in theory + // this process can be split up in any way + let (_coins, total, _amount, fee) = selection::select_coins_and_fee( + wallet, + amount, + current_height, + minimum_confirmations, + max_outputs, + num_change_outputs, + selection_strategy_is_use_all, + parent_key_id, + )?; + Ok((total, fee)) +} + +/// Add inputs to the slate (effectively becoming the sender) +pub fn add_inputs_to_slate( + wallet: &mut T, + slate: &mut Slate, + minimum_confirmations: u64, + max_outputs: usize, + num_change_outputs: usize, + selection_strategy_is_use_all: bool, + parent_key_id: &Identifier, + participant_id: usize, + message: Option, +) -> Result<(Context, OutputLockFn), Error> +where + T: WalletBackend, + C: NodeClient, + K: Keychain, +{ + // sender should always refresh outputs + updater::refresh_outputs(wallet, 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, sender_lock_fn) = selection::build_send_tx( + wallet, + slate, + minimum_confirmations, + max_outputs, + num_change_outputs, + selection_strategy_is_use_all, + parent_key_id.clone(), + )?; + + // 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 + let _ = slate.fill_round_1( + wallet.keychain(), + &mut context.sec_key, + &context.sec_nonce, + participant_id, + message, + )?; + + Ok((context, sender_lock_fn)) +} + +/// Add outputs to the slate, becoming the recipient +pub fn add_output_to_slate( + wallet: &mut T, + slate: &mut Slate, + parent_key_id: &Identifier, + participant_id: usize, + message: Option, +) -> Result<(Context, OutputLockFn), Error> +where + T: WalletBackend, + C: NodeClient, + K: Keychain, +{ + // create an output using the amount in the slate + let (_, mut context, create_fn) = + selection::build_recipient_output(wallet, slate, parent_key_id.clone())?; + + // fill public keys + let _ = slate.fill_round_1( + wallet.keychain(), + &mut context.sec_key, + &context.sec_nonce, + 1, + message, + )?; + + // perform partial sig + let _ = slate.fill_round_2( + wallet.keychain(), + &context.sec_key, + &context.sec_nonce, + participant_id, + )?; + + Ok((context, create_fn)) +} + +/// Complete a transaction as the sender +pub fn complete_tx( + wallet: &mut T, + slate: &mut Slate, + participant_id: usize, + context: &Context, +) -> Result<(), Error> +where + T: WalletBackend, + C: NodeClient, + K: Keychain, +{ + let _ = slate.fill_round_2( + wallet.keychain(), + &context.sec_key, + &context.sec_nonce, + participant_id, + )?; + // Final transaction can be built by anyone at this stage + slate.finalize(wallet.keychain())?; + Ok(()) +} + +/// Rollback outputs associated with a transaction in the wallet +pub fn cancel_tx( + wallet: &mut T, + parent_key_id: &Identifier, + tx_id: Option, + tx_slate_id: Option, +) -> Result<(), Error> +where + T: WalletBackend, + C: NodeClient, + K: Keychain, +{ + let mut tx_id_string = String::new(); + if let Some(tx_id) = tx_id { + tx_id_string = tx_id.to_string(); + } else if let Some(tx_slate_id) = tx_slate_id { + tx_id_string = tx_slate_id.to_string(); + } + let tx_vec = updater::retrieve_txs(wallet, tx_id, tx_slate_id, Some(&parent_key_id), false)?; + if tx_vec.len() != 1 { + return Err(ErrorKind::TransactionDoesntExist(tx_id_string))?; + } + let tx = tx_vec[0].clone(); + if tx.tx_type != TxLogEntryType::TxSent && tx.tx_type != TxLogEntryType::TxReceived { + return Err(ErrorKind::TransactionNotCancellable(tx_id_string))?; + } + if tx.confirmed == true { + return Err(ErrorKind::TransactionNotCancellable(tx_id_string))?; + } + // get outputs associated with tx + let res = updater::retrieve_outputs(wallet, false, Some(tx.id), Some(&parent_key_id))?; + let outputs = res.iter().map(|(out, _)| out).cloned().collect(); + updater::cancel_tx_and_outputs(wallet, tx, outputs, parent_key_id)?; + Ok(()) +} + +/// Retrieve the associated stored finalised hex Transaction for a given transaction Id +/// as well as whether it's been confirmed +pub fn retrieve_tx_hex( + wallet: &mut T, + parent_key_id: &Identifier, + tx_id: u32, +) -> Result<(bool, Option), Error> +where + T: WalletBackend, + C: NodeClient, + K: Keychain, +{ + let tx_vec = updater::retrieve_txs(wallet, Some(tx_id), None, Some(parent_key_id), false)?; + if tx_vec.len() != 1 { + return Err(ErrorKind::TransactionDoesntExist(tx_id.to_string()))?; + } + let tx = tx_vec[0].clone(); + Ok((tx.confirmed, tx.stored_tx)) +} + +/// Update the stored transaction (this update needs to happen when the TX is finalised) +pub fn update_stored_tx(wallet: &mut T, slate: &Slate) -> Result<(), Error> +where + T: WalletBackend, + C: NodeClient, + K: Keychain, +{ + // finalize command + let tx_vec = updater::retrieve_txs(wallet, None, Some(slate.id), 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 { + tx = Some(t.clone()); + break; + } + } + let tx = match tx { + Some(t) => t, + None => return Err(ErrorKind::TransactionDoesntExist(slate.id.to_string()))?, + }; + wallet.store_tx(&format!("{}", tx.tx_slate_id.unwrap()), &slate.tx)?; + Ok(()) +} + +/// Update the transaction participant messages +pub fn update_message(wallet: &mut T, slate: &Slate) -> Result<(), Error> +where + T: WalletBackend, + C: NodeClient, + K: Keychain, +{ + let tx_vec = updater::retrieve_txs(wallet, None, Some(slate.id), None, false)?; + if tx_vec.is_empty() { + return Err(ErrorKind::TransactionDoesntExist(slate.id.to_string()))?; + } + let mut batch = wallet.batch()?; + for mut tx in tx_vec.into_iter() { + tx.messages = Some(slate.participant_messages()); + let parent_key = tx.parent_key_id.clone(); + batch.save_tx_log_entry(tx, &parent_key)?; + } + batch.commit()?; + Ok(()) +} +#[cfg(test)] +mod test { + use crate::core::libtx::build; + use crate::keychain::{ExtKeychain, ExtKeychainPath, Keychain}; + + #[test] + // demonstrate that input.commitment == referenced output.commitment + // based on the public key and amount begin spent + fn output_commitment_equals_input_commitment_on_spend() { + let keychain = ExtKeychain::from_random_seed(false).unwrap(); + let key_id1 = ExtKeychainPath::new(1, 1, 0, 0, 0).to_identifier(); + + let tx1 = build::transaction(vec![build::output(105, key_id1.clone())], &keychain).unwrap(); + let tx2 = build::transaction(vec![build::input(105, key_id1.clone())], &keychain).unwrap(); + + assert_eq!(tx1.outputs()[0].features, tx2.inputs()[0].features); + assert_eq!(tx1.outputs()[0].commitment(), tx2.inputs()[0].commitment()); + } +} diff --git a/libwallet/src/internal/updater.rs b/libwallet/src/internal/updater.rs new file mode 100644 index 0000000..61271fe --- /dev/null +++ b/libwallet/src/internal/updater.rs @@ -0,0 +1,517 @@ +// Copyright 2018 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Utilities to check the status of all the outputs we have stored in +//! the wallet storage and update them. + +use failure::ResultExt; +use std::collections::HashMap; +use uuid::Uuid; + +use crate::core::consensus::reward; +use crate::core::core::{Output, TxKernel}; +use crate::core::libtx::reward; +use crate::core::{global, ser}; +use crate::keychain::{Identifier, Keychain}; +use crate::error::{Error, ErrorKind}; +use crate::internal::keys; +use crate::types::{ + BlockFees, CbData, NodeClient, OutputData, OutputStatus, TxLogEntry, TxLogEntryType, + WalletBackend, WalletInfo, +}; +use crate::util; +use crate::util::secp::pedersen; + +/// Retrieve all of the outputs (doesn't attempt to update from node) +pub fn retrieve_outputs( + wallet: &mut T, + show_spent: bool, + tx_id: Option, + parent_key_id: Option<&Identifier>, +) -> Result, Error> +where + T: WalletBackend, + C: NodeClient, + K: Keychain, +{ + // 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) + .map(|o| o.clone()) + .collect(); + } + + outputs.sort_by_key(|out| out.n_child); + let keychain = wallet.keychain().clone(); + + let res = outputs + .into_iter() + .map(|out| { + 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).unwrap(), + }; + (out, commit) + }) + .collect(); + Ok(res) +} + +/// 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( + wallet: &mut T, + tx_id: Option, + tx_slate_id: Option, + parent_key_id: Option<&Identifier>, + outstanding_only: bool, +) -> Result, Error> +where + T: WalletBackend, + C: NodeClient, + K: Keychain, +{ + let mut txs: Vec = 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) + } + 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( + wallet: &mut T, + parent_key_id: &Identifier, + update_all: bool, +) -> Result<(), Error> +where + T: WalletBackend, + C: NodeClient, + K: Keychain, +{ + let height = wallet.w2n_client().get_chain_height()?; + refresh_output_state(wallet, 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( + wallet: &mut T, + parent_key_id: &Identifier, + update_all: bool, +) -> Result)>, Error> +where + T: WalletBackend, + C: NodeClient, + K: Keychain, +{ + let mut wallet_outputs: HashMap)> = + HashMap::new(); + let keychain = wallet.keychain().clone(); + 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, Some(&parent_key_id), true)?; + + // Only select outputs that are actually involved in an outstanding transaction + let unspents: Vec = match update_all { + false => unspents + .into_iter() + .filter(|x| match x.tx_log_entry.as_ref() { + Some(t) => { + if let Some(_) = tx_entries.iter().find(|&te| te.id == *t) { + true + } else { + false + } + } + 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).unwrap(), + }; + wallet_outputs.insert(commit, (out.key_id.clone(), out.mmr_index)); + } + Ok(wallet_outputs) +} + +/// Cancel transaction and associated outputs +pub fn cancel_tx_and_outputs( + wallet: &mut T, + tx: TxLogEntry, + outputs: Vec, + parent_key_id: &Identifier, +) -> Result<(), Error> +where + T: WalletBackend, + C: NodeClient, + K: Keychain, +{ + let mut batch = wallet.batch()?; + + for mut o in outputs { + // unlock locked outputs + if o.status == OutputStatus::Unconfirmed { + batch.delete(&o.key_id, &o.mmr_index)?; + } + if o.status == OutputStatus::Locked { + o.status = OutputStatus::Unspent; + batch.save(o)?; + } + } + let mut tx = tx.clone(); + if tx.tx_type == TxLogEntryType::TxSent { + tx.tx_type = TxLogEntryType::TxSentCancelled; + } + if tx.tx_type == TxLogEntryType::TxReceived { + 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( + wallet: &mut T, + wallet_outputs: &HashMap)>, + api_outputs: &HashMap, + height: u64, + parent_key_id: &Identifier, +) -> Result<(), Error> +where + T: WalletBackend, + C: NodeClient, + K: Keychain, +{ + // now for each commit, find the output in the wallet and the corresponding + // api output (if it exists) and refresh it in-place in the wallet. + // 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()?; + 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; + 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 { + 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 { + t.update_confirmation_ts(); + t.confirmed = true; + batch.save_tx_log_entry(t, &parent_key_id)?; + } + } + output.height = o.1; + output.mark_unspent(); + } + None => output.mark_spent(), + }; + batch.save(output)?; + } + } + { + batch.save_last_confirmed_height(parent_key_id, height)?; + } + batch.commit()?; + } + Ok(()) +} + +/// Builds a single api query to retrieve the latest output data from the node. +/// So we can refresh the local wallet outputs. +fn refresh_output_state( + wallet: &mut T, + height: u64, + parent_key_id: &Identifier, + update_all: bool, +) -> Result<(), Error> +where + T: WalletBackend, + C: NodeClient, + K: Keychain, +{ + 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, parent_key_id, update_all)?; + + let wallet_output_keys = wallet_outputs.keys().map(|commit| commit.clone()).collect(); + + let api_outputs = wallet + .w2n_client() + .get_outputs_from_node(wallet_output_keys)?; + apply_api_outputs(wallet, &wallet_outputs, &api_outputs, height, parent_key_id)?; + clean_old_unconfirmed(wallet, height)?; + Ok(()) +} + +fn clean_old_unconfirmed(wallet: &mut T, height: u64) -> Result<(), Error> +where + T: WalletBackend, + C: NodeClient, + K: Keychain, +{ + 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()?; + 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( + wallet: &mut T, + parent_key_id: &Identifier, + minimum_confirmations: u64, +) -> Result +where + T: WalletBackend, + C: NodeClient, + K: Keychain, +{ + 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 unconfirmed_total = 0; + let mut locked_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 { + unspent_total += out.value; + } else { + unconfirmed_total += out.value; + } + } + } + OutputStatus::Locked => { + locked_total += out.value; + } + OutputStatus::Spent => {} + } + } + + Ok(WalletInfo { + last_confirmed_height: current_height, + minimum_confirmations, + total: unspent_total + unconfirmed_total + immature_total, + amount_awaiting_confirmation: unconfirmed_total, + amount_immature: immature_total, + amount_locked: locked_total, + amount_currently_spendable: unspent_total, + }) +} + +/// Build a coinbase output and insert into wallet +pub fn build_coinbase( + wallet: &mut T, + block_fees: &BlockFees, +) -> Result +where + T: WalletBackend, + C: NodeClient, + K: Keychain, +{ + let (out, kern, block_fees) = receive_coinbase(wallet, block_fees).context(ErrorKind::Node)?; + + let out_bin = ser::ser_vec(&out).context(ErrorKind::Node)?; + + let kern_bin = ser::ser_vec(&kern).context(ErrorKind::Node)?; + + let key_id_bin = match block_fees.key_id { + Some(key_id) => ser::ser_vec(&key_id).context(ErrorKind::Node)?, + None => vec![], + }; + + Ok(CbData { + output: util::to_hex(out_bin), + kernel: util::to_hex(kern_bin), + key_id: util::to_hex(key_id_bin), + }) +} + +//TODO: Split up the output creation and the wallet insertion +/// Build a coinbase output and the corresponding kernel +pub fn receive_coinbase( + wallet: &mut T, + block_fees: &BlockFees, +) -> Result<(Output, TxKernel, BlockFees), Error> +where + T: WalletBackend, + C: NodeClient, + K: Keychain, +{ + let height = block_fees.height; + let lock_height = height + global::coinbase_maturity(); + let key_id = block_fees.key_id(); + let parent_key_id = wallet.parent_key_id(); + + let key_id = match key_id { + Some(key_id) => match keys::retrieve_existing_key(wallet, key_id, None) { + Ok(k) => k.0, + Err(_) => keys::next_available_key(wallet)?, + }, + None => keys::next_available_key(wallet)?, + }; + + { + // Now acquire the wallet lock and write the new output. + let amount = reward(block_fees.fees); + let commit = wallet.calc_commit_for_cache(amount, &key_id)?; + let mut batch = wallet.batch()?; + batch.save(OutputData { + root_key_id: parent_key_id, + key_id: key_id.clone(), + n_child: key_id.to_path().last_path_index(), + mmr_index: None, + commit: commit, + value: amount, + status: OutputStatus::Unconfirmed, + height: height, + lock_height: lock_height, + is_coinbase: true, + tx_log_entry: None, + })?; + batch.commit()?; + } + + debug!( + "receive_coinbase: built candidate output - {:?}, {}", + key_id.clone(), + key_id, + ); + + let mut block_fees = block_fees.clone(); + block_fees.key_id = Some(key_id.clone()); + + debug!("receive_coinbase: {:?}", block_fees); + + let (out, kern) = reward::output(wallet.keychain(), &key_id, block_fees.fees).unwrap(); + /* .context(ErrorKind::Keychain)?; */ + Ok((out, kern, block_fees)) +} diff --git a/libwallet/src/lib.rs b/libwallet/src/lib.rs new file mode 100644 index 0000000..7447b0c --- /dev/null +++ b/libwallet/src/lib.rs @@ -0,0 +1,48 @@ +// Copyright 2019 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Higher level wallet functions which can be used by callers to operate +//! on the wallet, as well as helpers to invoke and instantiate wallets +//! and listeners + +#![deny(non_upper_case_globals)] +#![deny(non_camel_case_types)] +#![deny(non_snake_case)] +#![deny(unused_mut)] +#![warn(missing_docs)] + +#[macro_use] +extern crate grin_core as core; + +extern crate grin_keychain as keychain; +extern crate grin_util as util; + +use blake2_rfc as blake2; + +use failure; +extern crate failure_derive; + +#[macro_use] +extern crate serde_derive; +#[macro_use] +extern crate log; + +pub mod api; +mod error; +pub mod internal; +pub mod slate; +pub mod slate_versions; +pub mod types; + +pub use crate::error::{Error, ErrorKind}; diff --git a/libwallet/src/slate.rs b/libwallet/src/slate.rs new file mode 100644 index 0000000..a6a3767 --- /dev/null +++ b/libwallet/src/slate.rs @@ -0,0 +1,559 @@ +// Copyright 2018 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Functions for building partial transactions to be passed +//! around during an interactive wallet exchange + +use crate::blake2::blake2b::blake2b; +use crate::keychain::{BlindSum, BlindingFactor, Keychain}; +use crate::error::{Error, ErrorKind}; +use crate::slate_versions::v0::SlateV0; +use crate::util::secp; +use crate::util::secp::key::{PublicKey, SecretKey}; +use crate::util::secp::Signature; +use crate::util::RwLock; +use grin_core::core::amount_to_hr_string; +use grin_core::core::committed::Committed; +use grin_core::core::transaction::{kernel_features, kernel_sig_msg, Transaction, Weighting}; +use grin_core::core::verifier_cache::LruVerifierCache; +use grin_core::libtx::{aggsig, build, secp_ser, tx_fee}; +use rand::thread_rng; +use std::sync::Arc; +use uuid::Uuid; + +const CURRENT_SLATE_VERSION: u64 = 1; + +/// A wrapper around slates the enables support for versioning +#[derive(Serialize, Deserialize, Clone)] +#[serde(untagged)] +pub enum VersionedSlate { + /// Pre versioning version + V0(SlateV0), + /// Version 1 with versioning and hex serialization - current + V1(Slate), +} + +impl From for Slate { + fn from(ver: VersionedSlate) -> Self { + match ver { + VersionedSlate::V0(slate_v0) => Slate::from(slate_v0), + VersionedSlate::V1(slate) => slate, + } + } +} + +/// Public data for each participant in the slate + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct ParticipantData { + /// Id of participant in the transaction. (For now, 0=sender, 1=rec) + pub id: u64, + /// Public key corresponding to private blinding factor + #[serde(with = "secp_ser::pubkey_serde")] + pub public_blind_excess: PublicKey, + /// Public key corresponding to private nonce + #[serde(with = "secp_ser::pubkey_serde")] + pub public_nonce: PublicKey, + /// Public partial signature + #[serde(with = "secp_ser::option_sig_serde")] + pub part_sig: Option, + /// A message for other participants + pub message: Option, + /// Signature, created with private key corresponding to 'public_blind_excess' + #[serde(with = "secp_ser::option_sig_serde")] + pub message_sig: Option, +} + +impl ParticipantData { + /// A helper to return whether this participant + /// has completed round 1 and round 2; + /// Round 1 has to be completed before instantiation of this struct + /// anyhow, and for each participant consists of: + /// -Inputs added to transaction + /// -Outputs added to transaction + /// -Public signature nonce chosen and added + /// -Public contribution to blinding factor chosen and added + /// Round 2 can only be completed after all participants have + /// performed round 1, and adds: + /// -Part sig is filled out + pub fn is_complete(&self) -> bool { + self.part_sig.is_some() + } +} + +/// Public message data (for serialising and storage) +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct ParticipantMessageData { + /// id of the particpant in the tx + pub id: u64, + /// Public key + #[serde(with = "secp_ser::pubkey_serde")] + pub public_key: PublicKey, + /// Message, + pub message: Option, + /// Signature + #[serde(with = "secp_ser::option_sig_serde")] + pub message_sig: Option, +} + +impl ParticipantMessageData { + /// extract relevant message data from participant data + pub fn from_participant_data(p: &ParticipantData) -> ParticipantMessageData { + ParticipantMessageData { + id: p.id, + public_key: p.public_blind_excess, + message: p.message.clone(), + message_sig: p.message_sig.clone(), + } + } +} + +/// A 'Slate' is passed around to all parties to build up all of the public +/// transaction data needed to create a finalized transaction. Callers can pass +/// the slate around by whatever means they choose, (but we can provide some +/// binary or JSON serialization helpers here). + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Slate { + /// The number of participants intended to take part in this transaction + pub num_participants: usize, + /// Unique transaction ID, selected by sender + pub id: Uuid, + /// The core transaction data: + /// inputs, outputs, kernels, kernel offset + pub tx: Transaction, + /// base amount (excluding fee) + pub amount: u64, + /// fee amount + pub fee: u64, + /// Block height for the transaction + pub height: u64, + /// Lock height + pub lock_height: u64, + /// Participant data, each participant in the transaction will + /// insert their public data here. For now, 0 is sender and 1 + /// is receiver, though this will change for multi-party + pub participant_data: Vec, + /// Slate format version + #[serde(default = "no_version")] + pub version: u64, +} + +fn no_version() -> u64 { + 0 +} + +/// Helper just to facilitate serialization +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct ParticipantMessages { + /// included messages + pub messages: Vec, +} + +impl Slate { + /// Create a new slate + pub fn blank(num_participants: usize) -> Slate { + Slate { + num_participants: num_participants, + id: Uuid::new_v4(), + tx: Transaction::empty(), + amount: 0, + fee: 0, + height: 0, + lock_height: 0, + participant_data: vec![], + version: CURRENT_SLATE_VERSION, + } + } + + /// Adds selected inputs and outputs to the slate's transaction + /// Returns blinding factor + pub fn add_transaction_elements( + &mut self, + keychain: &K, + mut elems: Vec>>, + ) -> Result + where + K: Keychain, + { + // Append to the exiting transaction + if self.tx.kernels().len() != 0 { + elems.insert(0, build::initial_tx(self.tx.clone())); + } + let (tx, blind) = build::partial_transaction(elems, keychain)?; + self.tx = tx; + Ok(blind) + } + + /// Completes callers part of round 1, adding public key info + /// to the slate + pub fn fill_round_1( + &mut self, + keychain: &K, + sec_key: &mut SecretKey, + sec_nonce: &SecretKey, + participant_id: usize, + message: Option, + ) -> Result<(), Error> + where + K: Keychain, + { + // Whoever does this first generates the offset + if self.tx.offset == BlindingFactor::zero() { + self.generate_offset(keychain, sec_key)?; + } + self.add_participant_info( + keychain, + &sec_key, + &sec_nonce, + participant_id, + None, + message, + )?; + Ok(()) + } + + // This is the msg that we will sign as part of the tx kernel. + // Currently includes the fee and the lock_height. + fn msg_to_sign(&self) -> Result { + // Currently we only support interactively creating a tx with a "default" kernel. + let features = kernel_features(self.lock_height); + let msg = kernel_sig_msg(self.fee, self.lock_height, features)?; + Ok(msg) + } + + /// Completes caller's part of round 2, completing signatures + pub fn fill_round_2( + &mut self, + keychain: &K, + sec_key: &SecretKey, + sec_nonce: &SecretKey, + participant_id: usize, + ) -> Result<(), Error> + where + K: Keychain, + { + self.check_fees()?; + + self.verify_part_sigs(keychain.secp())?; + let sig_part = aggsig::calculate_partial_sig( + keychain.secp(), + sec_key, + sec_nonce, + &self.pub_nonce_sum(keychain.secp())?, + Some(&self.pub_blind_sum(keychain.secp())?), + &self.msg_to_sign()?, + )?; + self.participant_data[participant_id].part_sig = Some(sig_part); + Ok(()) + } + + /// Creates the final signature, callable by either the sender or recipient + /// (after phase 3: sender confirmation) + /// TODO: Only callable by receiver at the moment + pub fn finalize(&mut self, keychain: &K) -> Result<(), Error> + where + K: Keychain, + { + let final_sig = self.finalize_signature(keychain)?; + self.finalize_transaction(keychain, &final_sig) + } + + /// Return the sum of public nonces + fn pub_nonce_sum(&self, secp: &secp::Secp256k1) -> Result { + let pub_nonces = self + .participant_data + .iter() + .map(|p| &p.public_nonce) + .collect(); + match PublicKey::from_combination(secp, pub_nonces) { + Ok(k) => Ok(k), + Err(e) => Err(ErrorKind::Secp(e))?, + } + } + + /// Return the sum of public blinding factors + fn pub_blind_sum(&self, secp: &secp::Secp256k1) -> Result { + let pub_blinds = self + .participant_data + .iter() + .map(|p| &p.public_blind_excess) + .collect(); + match PublicKey::from_combination(secp, pub_blinds) { + Ok(k) => Ok(k), + Err(e) => Err(ErrorKind::Secp(e))?, + } + } + + /// Return vector of all partial sigs + fn part_sigs(&self) -> Vec<&Signature> { + self.participant_data + .iter() + .map(|p| p.part_sig.as_ref().unwrap()) + .collect() + } + + /// Adds participants public keys to the slate data + /// and saves participant's transaction context + /// sec_key can be overridden to replace the blinding + /// factor (by whoever split the offset) + fn add_participant_info( + &mut self, + keychain: &K, + sec_key: &SecretKey, + sec_nonce: &SecretKey, + id: usize, + part_sig: Option, + message: Option, + ) -> Result<(), Error> + where + K: Keychain, + { + // Add our public key and nonce to the slate + let pub_key = PublicKey::from_secret_key(keychain.secp(), &sec_key)?; + let pub_nonce = PublicKey::from_secret_key(keychain.secp(), &sec_nonce)?; + // Sign the provided message + let message_sig = { + if let Some(m) = message.clone() { + let hashed = blake2b(secp::constants::MESSAGE_SIZE, &[], &m.as_bytes()[..]); + let m = secp::Message::from_slice(&hashed.as_bytes())?; + let res = aggsig::sign_single(&keychain.secp(), &m, &sec_key, Some(&pub_key))?; + Some(res) + } else { + None + } + }; + self.participant_data.push(ParticipantData { + id: id as u64, + public_blind_excess: pub_key, + public_nonce: pub_nonce, + part_sig: part_sig, + message: message, + message_sig: message_sig, + }); + Ok(()) + } + + /// helper to return all participant messages + pub fn participant_messages(&self) -> ParticipantMessages { + let mut ret = ParticipantMessages { messages: vec![] }; + for ref m in self.participant_data.iter() { + ret.messages + .push(ParticipantMessageData::from_participant_data(m)); + } + ret + } + + /// Somebody involved needs to generate an offset with their private key + /// For now, we'll have the transaction initiator be responsible for it + /// Return offset private key for the participant to use later in the + /// transaction + fn generate_offset(&mut self, keychain: &K, sec_key: &mut SecretKey) -> Result<(), Error> + where + K: Keychain, + { + // Generate a random kernel offset here + // and subtract it from the blind_sum so we create + // the aggsig context with the "split" key + self.tx.offset = + BlindingFactor::from_secret_key(SecretKey::new(&keychain.secp(), &mut thread_rng())); + let blind_offset = keychain.blind_sum( + &BlindSum::new() + .add_blinding_factor(BlindingFactor::from_secret_key(sec_key.clone())) + .sub_blinding_factor(self.tx.offset), + )?; + *sec_key = blind_offset.secret_key(&keychain.secp())?; + Ok(()) + } + + /// Checks the fees in the transaction in the given slate are valid + fn check_fees(&self) -> Result<(), Error> { + // double check the fee amount included in the partial tx + // we don't necessarily want to just trust the sender + // we could just overwrite the fee here (but we won't) due to the sig + let fee = tx_fee( + self.tx.inputs().len(), + self.tx.outputs().len(), + self.tx.kernels().len(), + None, + ); + if fee > self.tx.fee() { + return Err(ErrorKind::Fee( + format!("Fee Dispute Error: {}, {}", self.tx.fee(), fee,).to_string(), + ))?; + } + + if fee > self.amount + self.fee { + let reason = format!( + "Rejected the transfer because transaction fee ({}) exceeds received amount ({}).", + amount_to_hr_string(fee, false), + amount_to_hr_string(self.amount + self.fee, false) + ); + info!("{}", reason); + return Err(ErrorKind::Fee(reason.to_string()))?; + } + + Ok(()) + } + + /// Verifies all of the partial signatures in the Slate are valid + fn verify_part_sigs(&self, secp: &secp::Secp256k1) -> Result<(), Error> { + // collect public nonces + for p in self.participant_data.iter() { + if p.is_complete() { + aggsig::verify_partial_sig( + secp, + p.part_sig.as_ref().unwrap(), + &self.pub_nonce_sum(secp)?, + &p.public_blind_excess, + Some(&self.pub_blind_sum(secp)?), + &self.msg_to_sign()?, + )?; + } + } + Ok(()) + } + + /// Verifies any messages in the slate's participant data match their signatures + pub fn verify_messages(&self, secp: &secp::Secp256k1) -> Result<(), Error> { + for p in self.participant_data.iter() { + if let Some(msg) = &p.message { + let hashed = blake2b(secp::constants::MESSAGE_SIZE, &[], &msg.as_bytes()[..]); + let m = secp::Message::from_slice(&hashed.as_bytes())?; + let signature = match p.message_sig { + None => { + error!("verify_messages - participant message doesn't have signature. Message: \"{}\"", + String::from_utf8_lossy(&msg.as_bytes()[..])); + return Err(ErrorKind::Signature( + "Optional participant messages doesn't have signature".to_owned(), + ))?; + } + Some(s) => s, + }; + if !aggsig::verify_single( + secp, + &signature, + &m, + None, + &p.public_blind_excess, + Some(&p.public_blind_excess), + false, + ) { + error!("verify_messages - participant message doesn't match signature. Message: \"{}\"", + String::from_utf8_lossy(&msg.as_bytes()[..])); + return Err(ErrorKind::Signature( + "Optional participant messages do not match signatures".to_owned(), + ))?; + } else { + info!( + "verify_messages - signature verified ok. Participant message: \"{}\"", + String::from_utf8_lossy(&msg.as_bytes()[..]) + ); + } + } + } + Ok(()) + } + + /// This should be callable by either the sender or receiver + /// once phase 3 is done + /// + /// Receive Part 3 of interactive transactions from sender, Sender + /// Confirmation Return Ok/Error + /// -Receiver receives sS + /// -Receiver verifies sender's sig, by verifying that + /// kS * G + e *xS * G = sS* G + /// -Receiver calculates final sig as s=(sS+sR, kS * G+kR * G) + /// -Receiver puts into TX kernel: + /// + /// Signature S + /// pubkey xR * G+xS * G + /// fee (= M) + /// + /// Returns completed transaction ready for posting to the chain + + fn finalize_signature(&mut self, keychain: &K) -> Result + where + K: Keychain, + { + self.verify_part_sigs(keychain.secp())?; + + let part_sigs = self.part_sigs(); + let pub_nonce_sum = self.pub_nonce_sum(keychain.secp())?; + let final_pubkey = self.pub_blind_sum(keychain.secp())?; + // get the final signature + let final_sig = aggsig::add_signatures(&keychain.secp(), part_sigs, &pub_nonce_sum)?; + + // Calculate the final public key (for our own sanity check) + + // Check our final sig verifies + aggsig::verify_completed_sig( + &keychain.secp(), + &final_sig, + &final_pubkey, + Some(&final_pubkey), + &self.msg_to_sign()?, + )?; + + Ok(final_sig) + } + + /// builds a final transaction after the aggregated sig exchange + fn finalize_transaction( + &mut self, + keychain: &K, + final_sig: &secp::Signature, + ) -> Result<(), Error> + where + K: Keychain, + { + let kernel_offset = self.tx.offset; + + self.check_fees()?; + + let mut final_tx = self.tx.clone(); + + // build the final excess based on final tx and offset + let final_excess = { + // sum the input/output commitments on the final tx + let overage = final_tx.fee() as i64; + let tx_excess = final_tx.sum_commitments(overage)?; + + // subtract the kernel_excess (built from kernel_offset) + let offset_excess = keychain + .secp() + .commit(0, kernel_offset.secret_key(&keychain.secp())?)?; + keychain + .secp() + .commit_sum(vec![tx_excess], vec![offset_excess])? + }; + + // update the tx kernel to reflect the offset excess and sig + assert_eq!(final_tx.kernels().len(), 1); + final_tx.kernels_mut()[0].excess = final_excess.clone(); + final_tx.kernels_mut()[0].excess_sig = final_sig.clone(); + + // confirm the kernel verifies successfully before proceeding + debug!("Validating final transaction"); + final_tx.kernels()[0].verify()?; + + // confirm the overall transaction is valid (including the updated kernel) + // accounting for tx weight limits + let verifier_cache = Arc::new(RwLock::new(LruVerifierCache::new())); + let _ = final_tx.validate(Weighting::AsTransaction, verifier_cache)?; + + self.tx = final_tx; + Ok(()) + } +} diff --git a/libwallet/src/slate_versions/mod.rs b/libwallet/src/slate_versions/mod.rs new file mode 100644 index 0000000..03b4709 --- /dev/null +++ b/libwallet/src/slate_versions/mod.rs @@ -0,0 +1,18 @@ +// Copyright 2019 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! This module contains old slate versions and conversions to the newest slate version +//! Used for serialization and deserialization of slates in a backwards compatible way. +#[allow(missing_docs)] +pub mod v0; diff --git a/libwallet/src/slate_versions/v0.rs b/libwallet/src/slate_versions/v0.rs new file mode 100644 index 0000000..7757085 --- /dev/null +++ b/libwallet/src/slate_versions/v0.rs @@ -0,0 +1,370 @@ +// Copyright 2018 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Contains V0 of the slate +use crate::core::core::transaction::{ + Input, KernelFeatures, Output, OutputFeatures, Transaction, TransactionBody, TxKernel, +}; +use crate::keychain::BlindingFactor; +use crate::slate::{ParticipantData, Slate}; +use crate::util::secp; +use crate::util::secp::key::PublicKey; +use crate::util::secp::pedersen::{Commitment, RangeProof}; +use crate::util::secp::Signature; +use uuid::Uuid; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct SlateV0 { + /// The number of participants intended to take part in this transaction + pub num_participants: usize, + /// Unique transaction ID, selected by sender + pub id: Uuid, + /// The core transaction data: + /// inputs, outputs, kernels, kernel offset + pub tx: TransactionV0, + /// base amount (excluding fee) + pub amount: u64, + /// fee amount + pub fee: u64, + /// Block height for the transaction + pub height: u64, + /// Lock height + pub lock_height: u64, + /// Participant data, each participant in the transaction will + /// insert their public data here. For now, 0 is sender and 1 + /// is receiver, though this will change for multi-party + pub participant_data: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct ParticipantDataV0 { + /// Id of participant in the transaction. (For now, 0=sender, 1=rec) + pub id: u64, + /// Public key corresponding to private blinding factor + pub public_blind_excess: PublicKey, + /// Public key corresponding to private nonce + pub public_nonce: PublicKey, + /// Public partial signature + pub part_sig: Option, + /// A message for other participants + pub message: Option, + /// Signature, created with private key corresponding to 'public_blind_excess' + pub message_sig: Option, +} + +/// A transaction +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct TransactionV0 { + /// The kernel "offset" k2 + /// excess is k1G after splitting the key k = k1 + k2 + pub offset: BlindingFactor, + /// The transaction body - inputs/outputs/kernels + pub body: TransactionBodyV0, +} + +/// TransactionBody is a common abstraction for transaction and block +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct TransactionBodyV0 { + /// List of inputs spent by the transaction. + pub inputs: Vec, + /// List of outputs the transaction produces. + pub outputs: Vec, + /// List of kernels that make up this transaction (usually a single kernel). + pub kernels: Vec, +} +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct InputV0 { + /// The features of the output being spent. + /// We will check maturity for coinbase output. + pub features: OutputFeatures, + /// The commit referencing the output being spent. + pub commit: Commitment, +} + +#[derive(Debug, Copy, Clone, Serialize, Deserialize)] +pub struct OutputV0 { + /// Options for an output's structure or use + pub features: OutputFeatures, + /// The homomorphic commitment representing the output amount + pub commit: Commitment, + /// A proof that the commitment is in the right range + pub proof: RangeProof, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct TxKernelV0 { + /// Options for a kernel's structure or use + pub features: KernelFeatures, + /// Fee originally included in the transaction this proof is for. + pub fee: u64, + /// This kernel is not valid earlier than lock_height blocks + /// The max lock_height of all *inputs* to this transaction + pub lock_height: u64, + /// Remainder of the sum of all transaction commitments. If the transaction + /// is well formed, amounts components should sum to zero and the excess + /// is hence a valid public key. + pub excess: Commitment, + /// The signature proving the excess is a valid public key, which signs + /// the transaction fee. + pub excess_sig: secp::Signature, +} + +impl From for Slate { + fn from(slate: SlateV0) -> Slate { + let SlateV0 { + num_participants, + id, + tx, + amount, + fee, + height, + lock_height, + participant_data, + } = slate; + let tx = Transaction::from(tx); + let participant_data = map_vec!(participant_data, |data| ParticipantData::from(data)); + let version = 0; + Slate { + num_participants, + id, + tx, + amount, + fee, + height, + lock_height, + participant_data, + version, + } + } +} + +impl From<&ParticipantDataV0> for ParticipantData { + fn from(data: &ParticipantDataV0) -> ParticipantData { + let ParticipantDataV0 { + id, + public_blind_excess, + public_nonce, + part_sig, + message, + message_sig, + } = data; + let id = *id; + let public_blind_excess = *public_blind_excess; + let public_nonce = *public_nonce; + let part_sig = *part_sig; + let message: Option = message.as_ref().map(|t| String::from(&**t)); + let message_sig = *message_sig; + ParticipantData { + id, + public_blind_excess, + public_nonce, + part_sig, + message, + message_sig, + } + } +} + +impl From for Transaction { + fn from(tx: TransactionV0) -> Transaction { + let TransactionV0 { offset, body } = tx; + let body = TransactionBody::from(&body); + let transaction = Transaction::new(body.inputs, body.outputs, body.kernels); + transaction.with_offset(offset) + } +} + +impl From<&TransactionBodyV0> for TransactionBody { + fn from(body: &TransactionBodyV0) -> Self { + let TransactionBodyV0 { + inputs, + outputs, + kernels, + } = body; + + let inputs = map_vec!(inputs, |inp| Input::from(inp)); + let outputs = map_vec!(outputs, |out| Output::from(out)); + let kernels = map_vec!(kernels, |kern| TxKernel::from(kern)); + TransactionBody { + inputs, + outputs, + kernels, + } + } +} + +impl From<&InputV0> for Input { + fn from(input: &InputV0) -> Input { + let InputV0 { features, commit } = *input; + Input { features, commit } + } +} + +impl From<&OutputV0> for Output { + fn from(output: &OutputV0) -> Output { + let OutputV0 { + features, + commit, + proof, + } = *output; + Output { + features, + commit, + proof, + } + } +} + +impl From<&TxKernelV0> for TxKernel { + fn from(kernel: &TxKernelV0) -> TxKernel { + let TxKernelV0 { + features, + fee, + lock_height, + excess, + excess_sig, + } = *kernel; + TxKernel { + features, + fee, + lock_height, + excess, + excess_sig, + } + } +} + +impl From for SlateV0 { + fn from(slate: Slate) -> SlateV0 { + let Slate { + num_participants, + id, + tx, + amount, + fee, + height, + lock_height, + participant_data, + version: _, + } = slate; + let tx = TransactionV0::from(tx); + let participant_data = map_vec!(participant_data, |data| ParticipantDataV0::from(data)); + SlateV0 { + num_participants, + id, + tx, + amount, + fee, + height, + lock_height, + participant_data, + } + } +} + +impl From<&ParticipantData> for ParticipantDataV0 { + fn from(data: &ParticipantData) -> ParticipantDataV0 { + let ParticipantData { + id, + public_blind_excess, + public_nonce, + part_sig, + message, + message_sig, + } = data; + let id = *id; + let public_blind_excess = *public_blind_excess; + let public_nonce = *public_nonce; + let part_sig = *part_sig; + let message: Option = message.as_ref().map(|t| String::from(&**t)); + let message_sig = *message_sig; + ParticipantDataV0 { + id, + public_blind_excess, + public_nonce, + part_sig, + message, + message_sig, + } + } +} + +impl From for TransactionV0 { + fn from(tx: Transaction) -> TransactionV0 { + let offset = tx.offset; + let body: TransactionBody = tx.into(); + let body = TransactionBodyV0::from(&body); + TransactionV0 { offset, body } + } +} + +impl From<&TransactionBody> for TransactionBodyV0 { + fn from(body: &TransactionBody) -> Self { + let TransactionBody { + inputs, + outputs, + kernels, + } = body; + + let inputs = map_vec!(inputs, |inp| InputV0::from(inp)); + let outputs = map_vec!(outputs, |out| OutputV0::from(out)); + let kernels = map_vec!(kernels, |kern| TxKernelV0::from(kern)); + TransactionBodyV0 { + inputs, + outputs, + kernels, + } + } +} + +impl From<&Input> for InputV0 { + fn from(input: &Input) -> Self { + let Input { features, commit } = *input; + InputV0 { features, commit } + } +} + +impl From<&Output> for OutputV0 { + fn from(output: &Output) -> Self { + let Output { + features, + commit, + proof, + } = *output; + OutputV0 { + features, + commit, + proof, + } + } +} + +impl From<&TxKernel> for TxKernelV0 { + fn from(kernel: &TxKernel) -> Self { + let TxKernel { + features, + fee, + lock_height, + excess, + excess_sig, + } = *kernel; + TxKernelV0 { + features, + fee, + lock_height, + excess, + excess_sig, + } + } +} diff --git a/libwallet/src/types.rs b/libwallet/src/types.rs new file mode 100644 index 0000000..4fe9315 --- /dev/null +++ b/libwallet/src/types.rs @@ -0,0 +1,721 @@ +// Copyright 2018 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Types and traits that should be provided by a wallet +//! implementation + +use crate::core::core::hash::Hash; +use crate::core::core::Transaction; +use crate::core::libtx::aggsig; +use crate::core::ser; +use crate::keychain::{Identifier, Keychain}; +use crate::error::{Error, ErrorKind}; +use crate::slate::ParticipantMessages; +use crate::util::secp::key::{PublicKey, SecretKey}; +use crate::util::secp::{self, pedersen, Secp256k1}; +use chrono::prelude::*; +use failure::ResultExt; +use serde; +use serde_json; +use std::collections::HashMap; +use std::fmt; +use std::marker::PhantomData; +use uuid::Uuid; + +/// Lock function type +pub type OutputLockFn = + Box, PhantomData) -> Result<(), Error>>; + +/// Combined trait to allow dynamic wallet dispatch +pub trait WalletInst: WalletBackend + Send + Sync + 'static +where + C: NodeClient, + K: Keychain, +{ +} +impl WalletInst for T +where + T: WalletBackend + Send + Sync + 'static, + C: NodeClient, + K: Keychain, +{ +} + +/// TODO: +/// Wallets should implement this backend for their storage. All functions +/// here expect that the wallet instance has instantiated itself or stored +/// whatever credentials it needs +pub trait WalletBackend +where + C: NodeClient, + K: Keychain, +{ + /// Initialize with whatever stored credentials we have + fn open_with_credentials(&mut self) -> Result<(), Error>; + + /// Close wallet and remove any stored credentials (TBD) + fn close(&mut self) -> Result<(), Error>; + + /// Return the keychain being used + fn keychain(&mut self) -> &mut K; + + /// Return the client being used to communicate with the node + fn w2n_client(&mut self) -> &mut C; + + /// return the commit for caching if allowed, none otherwise + fn calc_commit_for_cache( + &mut self, + amount: u64, + id: &Identifier, + ) -> Result, Error>; + + /// Set parent key id by stored account name + fn set_parent_key_id_by_name(&mut self, label: &str) -> Result<(), Error>; + + /// The BIP32 path of the parent path to use for all output-related + /// functions, (essentially 'accounts' within a wallet. + fn set_parent_key_id(&mut self, _: Identifier); + + /// return the parent path + fn parent_key_id(&mut self) -> Identifier; + + /// Iterate over all output data stored by the backend + fn iter<'a>(&'a self) -> Box + 'a>; + + /// Get output data by id + fn get(&self, id: &Identifier, mmr_index: &Option) -> Result; + + /// Get an (Optional) tx log entry by uuid + fn get_tx_log_entry(&self, uuid: &Uuid) -> Result, Error>; + + /// Retrieves the private context associated with a given slate id + fn get_private_context(&mut self, slate_id: &[u8]) -> Result; + + /// Iterate over all output data stored by the backend + fn tx_log_iter<'a>(&'a self) -> Box + 'a>; + + /// Iterate over all stored account paths + fn acct_path_iter<'a>(&'a self) -> Box + 'a>; + + /// Gets an account path for a given label + fn get_acct_path(&self, label: String) -> Result, Error>; + + /// Stores a transaction + fn store_tx(&self, uuid: &str, tx: &Transaction) -> Result<(), Error>; + + /// Retrieves a stored transaction from a TxLogEntry + fn get_stored_tx(&self, entry: &TxLogEntry) -> Result, Error>; + + /// Create a new write batch to update or remove output data + fn batch<'a>(&'a mut self) -> Result + 'a>, Error>; + + /// Next child ID when we want to create a new output, based on current parent + fn next_child<'a>(&mut self) -> Result; + + /// last verified height of outputs directly descending from the given parent key + fn last_confirmed_height<'a>(&mut self) -> Result; + + /// Attempt to restore the contents of a wallet from seed + fn restore(&mut self) -> Result<(), Error>; + + /// Attempt to check and fix wallet state + fn check_repair(&mut self) -> Result<(), Error>; +} + +/// Batch trait to update the output data backend atomically. Trying to use a +/// batch after commit MAY result in a panic. Due to this being a trait, the +/// commit method can't take ownership. +/// TODO: Should these be split into separate batch objects, for outputs, +/// tx_log entries and meta/details? +pub trait WalletOutputBatch +where + K: Keychain, +{ + /// Return the keychain being used + fn keychain(&mut self) -> &mut K; + + /// Add or update data about an output to the backend + fn save(&mut self, out: OutputData) -> Result<(), Error>; + + /// Gets output data by id + fn get(&self, id: &Identifier, mmr_index: &Option) -> Result; + + /// Iterate over all output data stored by the backend + fn iter(&self) -> Box>; + + /// Delete data about an output from the backend + fn delete(&mut self, id: &Identifier, mmr_index: &Option) -> Result<(), Error>; + + /// Save last stored child index of a given parent + fn save_child_index(&mut self, parent_key_id: &Identifier, child_n: u32) -> Result<(), Error>; + + /// Save last confirmed height of outputs for a given parent + fn save_last_confirmed_height( + &mut self, + parent_key_id: &Identifier, + height: u64, + ) -> Result<(), Error>; + + /// get next tx log entry for the parent + fn next_tx_log_id(&mut self, parent_key_id: &Identifier) -> Result; + + /// Iterate over tx log data stored by the backend + fn tx_log_iter(&self) -> Box>; + + /// save a tx log entry + fn save_tx_log_entry(&mut self, t: TxLogEntry, parent_id: &Identifier) -> Result<(), Error>; + + /// save an account label -> path mapping + fn save_acct_path(&mut self, mapping: AcctPathMapping) -> Result<(), Error>; + + /// Iterate over account names stored in backend + fn acct_path_iter(&self) -> Box>; + + /// Save an output as locked in the backend + fn lock_output(&mut self, out: &mut OutputData) -> Result<(), Error>; + + /// Saves the private context associated with a slate id + fn save_private_context(&mut self, slate_id: &[u8], ctx: &Context) -> Result<(), Error>; + + /// Delete the private context associated with the slate id + fn delete_private_context(&mut self, slate_id: &[u8]) -> Result<(), Error>; + + /// Write the wallet data to backend file + fn commit(&self) -> Result<(), Error>; +} + +/// Encapsulate all wallet-node communication functions. No functions within libwallet +/// should care about communication details +pub trait NodeClient: Sync + Send + Clone { + /// Return the URL of the check node + fn node_url(&self) -> &str; + + /// Set the node URL + fn set_node_url(&mut self, node_url: &str); + + /// Return the node api secret + fn node_api_secret(&self) -> Option; + + /// Change the API secret + fn set_node_api_secret(&mut self, node_api_secret: Option); + + /// Posts a transaction to a grin node + fn post_tx(&self, tx: &TxWrapper, fluff: bool) -> Result<(), Error>; + + /// retrieves the current tip from the specified grin node + fn get_chain_height(&self) -> Result; + + /// retrieve a list of outputs from the specified grin node + /// need "by_height" and "by_id" variants + fn get_outputs_from_node( + &self, + wallet_outputs: Vec, + ) -> Result, Error>; + + /// Get a list of outputs from the node by traversing the UTXO + /// set in PMMR index order. + /// Returns + /// (last available output index, last insertion index retrieved, + /// outputs(commit, proof, is_coinbase, height, mmr_index)) + fn get_outputs_by_pmmr_index( + &self, + start_height: u64, + max_outputs: u64, + ) -> Result< + ( + u64, + u64, + Vec<(pedersen::Commitment, pedersen::RangeProof, bool, u64, u64)>, + ), + Error, + >; +} + +/// Information about an output that's being tracked by the wallet. Must be +/// enough to reconstruct the commitment associated with the ouput when the +/// root private key is known. + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, PartialOrd, Eq, Ord)] +pub struct OutputData { + /// Root key_id that the key for this output is derived from + pub root_key_id: Identifier, + /// Derived key for this output + pub key_id: Identifier, + /// How many derivations down from the root key + pub n_child: u32, + /// The actual commit, optionally stored + pub commit: Option, + /// PMMR Index, used on restore in case of duplicate wallets using the same + /// key_id (2 wallets using same seed, for instance + pub mmr_index: Option, + /// Value of the output, necessary to rebuild the commitment + pub value: u64, + /// Current status of the output + pub status: OutputStatus, + /// Height of the output + pub height: u64, + /// Height we are locked until + pub lock_height: u64, + /// Is this a coinbase output? Is it subject to coinbase locktime? + pub is_coinbase: bool, + /// Optional corresponding internal entry in tx entry log + pub tx_log_entry: Option, +} + +impl ser::Writeable for OutputData { + fn write(&self, writer: &mut W) -> Result<(), ser::Error> { + writer.write_bytes(&serde_json::to_vec(self).map_err(|_| ser::Error::CorruptedData)?) + } +} + +impl ser::Readable for OutputData { + fn read(reader: &mut dyn ser::Reader) -> Result { + let data = reader.read_bytes_len_prefix()?; + serde_json::from_slice(&data[..]).map_err(|_| ser::Error::CorruptedData) + } +} + +impl OutputData { + /// Lock a given output to avoid conflicting use + pub fn lock(&mut self) { + self.status = OutputStatus::Locked; + } + + /// How many confirmations has this output received? + /// If height == 0 then we are either Unconfirmed or the output was + /// cut-through + /// so we do not actually know how many confirmations this output had (and + /// never will). + pub fn num_confirmations(&self, current_height: u64) -> u64 { + if self.height > current_height { + return 0; + } + if self.status == OutputStatus::Unconfirmed { + 0 + } else { + // if an output has height n and we are at block n + // then we have a single confirmation (the block it originated in) + 1 + (current_height - self.height) + } + } + + /// Check if output is eligible to spend based on state and height and + /// confirmations + pub fn eligible_to_spend(&self, current_height: u64, minimum_confirmations: u64) -> bool { + if [OutputStatus::Spent, OutputStatus::Locked].contains(&self.status) { + return false; + } else if self.status == OutputStatus::Unconfirmed && self.is_coinbase { + return false; + } else if self.lock_height > current_height { + return false; + } else if self.status == OutputStatus::Unspent + && self.num_confirmations(current_height) >= minimum_confirmations + { + return true; + } else if self.status == OutputStatus::Unconfirmed && minimum_confirmations == 0 { + return true; + } else { + return false; + } + } + + /// Marks this output as unspent if it was previously unconfirmed + pub fn mark_unspent(&mut self) { + match self.status { + OutputStatus::Unconfirmed => self.status = OutputStatus::Unspent, + _ => (), + } + } + + /// Mark an output as spent + pub fn mark_spent(&mut self) { + match self.status { + OutputStatus::Unspent => self.status = OutputStatus::Spent, + OutputStatus::Locked => self.status = OutputStatus::Spent, + _ => (), + } + } +} +/// Status of an output that's being tracked by the wallet. Can either be +/// unconfirmed, spent, unspent, or locked (when it's been used to generate +/// a transaction but we don't have confirmation that the transaction was +/// broadcasted or mined). +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Ord, PartialOrd)] +pub enum OutputStatus { + /// Unconfirmed + Unconfirmed, + /// Unspent + Unspent, + /// Locked + Locked, + /// Spent + Spent, +} + +impl fmt::Display for OutputStatus { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + OutputStatus::Unconfirmed => write!(f, "Unconfirmed"), + OutputStatus::Unspent => write!(f, "Unspent"), + OutputStatus::Locked => write!(f, "Locked"), + OutputStatus::Spent => write!(f, "Spent"), + } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +/// Holds the context for a single aggsig transaction +pub struct Context { + /// Secret key (of which public is shared) + pub sec_key: SecretKey, + /// Secret nonce (of which public is shared) + /// (basically a SecretKey) + pub sec_nonce: SecretKey, + /// store my outputs between invocations + pub output_ids: Vec<(Identifier, Option)>, + /// store my inputs + pub input_ids: Vec<(Identifier, Option)>, + /// store the calculated fee + pub fee: u64, +} + +impl Context { + /// Create a new context with defaults + pub fn new(secp: &secp::Secp256k1, sec_key: SecretKey) -> Context { + Context { + sec_key: sec_key, + sec_nonce: aggsig::create_secnonce(secp).unwrap(), + input_ids: vec![], + output_ids: vec![], + fee: 0, + } + } +} + +impl Context { + /// Tracks an output contributing to my excess value (if it needs to + /// be kept between invocations + pub fn add_output(&mut self, output_id: &Identifier, mmr_index: &Option) { + self.output_ids.push((output_id.clone(), mmr_index.clone())); + } + + /// Returns all stored outputs + pub fn get_outputs(&self) -> Vec<(Identifier, Option)> { + self.output_ids.clone() + } + + /// Tracks IDs of my inputs into the transaction + /// be kept between invocations + pub fn add_input(&mut self, input_id: &Identifier, mmr_index: &Option) { + self.input_ids.push((input_id.clone(), mmr_index.clone())); + } + + /// Returns all stored input identifiers + pub fn get_inputs(&self) -> Vec<(Identifier, Option)> { + self.input_ids.clone() + } + + /// Returns private key, private nonce + pub fn get_private_keys(&self) -> (SecretKey, SecretKey) { + (self.sec_key.clone(), self.sec_nonce.clone()) + } + + /// Returns public key, public nonce + pub fn get_public_keys(&self, secp: &Secp256k1) -> (PublicKey, PublicKey) { + ( + PublicKey::from_secret_key(secp, &self.sec_key).unwrap(), + PublicKey::from_secret_key(secp, &self.sec_nonce).unwrap(), + ) + } +} + +impl ser::Writeable for Context { + fn write(&self, writer: &mut W) -> Result<(), ser::Error> { + writer.write_bytes(&serde_json::to_vec(self).map_err(|_| ser::Error::CorruptedData)?) + } +} + +impl ser::Readable for Context { + fn read(reader: &mut dyn ser::Reader) -> Result { + let data = reader.read_bytes_len_prefix()?; + serde_json::from_slice(&data[..]).map_err(|_| ser::Error::CorruptedData) + } +} + +/// Block Identifier +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord)] +pub struct BlockIdentifier(pub Hash); + +impl BlockIdentifier { + /// return hash + pub fn hash(&self) -> Hash { + self.0 + } + + /// convert to hex string + pub fn from_hex(hex: &str) -> Result { + let hash = + Hash::from_hex(hex).context(ErrorKind::GenericError("Invalid hex".to_owned()))?; + Ok(BlockIdentifier(hash)) + } +} + +impl serde::ser::Serialize for BlockIdentifier { + fn serialize(&self, serializer: S) -> Result + where + S: serde::ser::Serializer, + { + serializer.serialize_str(&self.0.to_hex()) + } +} + +impl<'de> serde::de::Deserialize<'de> for BlockIdentifier { + fn deserialize(deserializer: D) -> Result + where + D: serde::de::Deserializer<'de>, + { + deserializer.deserialize_str(BlockIdentifierVisitor) + } +} + +struct BlockIdentifierVisitor; + +impl<'de> serde::de::Visitor<'de> for BlockIdentifierVisitor { + type Value = BlockIdentifier; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("a block hash") + } + + fn visit_str(self, s: &str) -> Result + where + E: serde::de::Error, + { + let block_hash = Hash::from_hex(s).unwrap(); + Ok(BlockIdentifier(block_hash)) + } +} + +/// Fees in block to use for coinbase amount calculation +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct BlockFees { + /// fees + pub fees: u64, + /// height + pub height: u64, + /// key id + pub key_id: Option, +} + +impl BlockFees { + /// return key id + pub fn key_id(&self) -> Option { + self.key_id.clone() + } +} + +/// Response to build a coinbase output. +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct CbData { + /// Output + pub output: String, + /// Kernel + pub kernel: String, + /// Key Id + pub key_id: String, +} + +/// a contained wallet info struct, so automated tests can parse wallet info +/// can add more fields here over time as needed +#[derive(Serialize, Eq, PartialEq, Deserialize, Debug, Clone)] +pub struct WalletInfo { + /// height from which info was taken + pub last_confirmed_height: u64, + /// Minimum number of confirmations for an output to be treated as "spendable". + pub minimum_confirmations: u64, + /// total amount in the wallet + pub total: u64, + /// amount awaiting confirmation + pub amount_awaiting_confirmation: u64, + /// coinbases waiting for lock height + pub amount_immature: u64, + /// amount currently spendable + pub amount_currently_spendable: u64, + /// amount locked via previous transactions + pub amount_locked: u64, +} + +/// Types of transactions that can be contained within a TXLog entry +#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] +pub enum TxLogEntryType { + /// A coinbase transaction becomes confirmed + ConfirmedCoinbase, + /// Outputs created when a transaction is received + TxReceived, + /// Inputs locked + change outputs when a transaction is created + TxSent, + /// Received transaction that was rolled back by user + TxReceivedCancelled, + /// Sent transaction that was rolled back by user + TxSentCancelled, +} + +impl fmt::Display for TxLogEntryType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + TxLogEntryType::ConfirmedCoinbase => write!(f, "Confirmed \nCoinbase"), + TxLogEntryType::TxReceived => write!(f, "Received Tx"), + TxLogEntryType::TxSent => write!(f, "Sent Tx"), + TxLogEntryType::TxReceivedCancelled => write!(f, "Received Tx\n- Cancelled"), + TxLogEntryType::TxSentCancelled => write!(f, "Sent Tx\n- Cancelled"), + } + } +} + +/// Optional transaction information, recorded when an event happens +/// to add or remove funds from a wallet. One Transaction log entry +/// maps to one or many outputs +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct TxLogEntry { + /// BIP32 account path used for creating this tx + pub parent_key_id: Identifier, + /// Local id for this transaction (distinct from a slate transaction id) + pub id: u32, + /// Slate transaction this entry is associated with, if any + pub tx_slate_id: Option, + /// Transaction type (as above) + pub tx_type: TxLogEntryType, + /// Time this tx entry was created + /// #[serde(with = "tx_date_format")] + pub creation_ts: DateTime, + /// Time this tx was confirmed (by this wallet) + /// #[serde(default, with = "opt_tx_date_format")] + pub confirmation_ts: Option>, + /// Whether the inputs+outputs involved in this transaction have been + /// confirmed (In all cases either all outputs involved in a tx should be + /// confirmed, or none should be; otherwise there's a deeper problem) + pub confirmed: bool, + /// number of inputs involved in TX + pub num_inputs: usize, + /// number of outputs involved in TX + pub num_outputs: usize, + /// Amount credited via this transaction + pub amount_credited: u64, + /// Amount debited via this transaction + pub amount_debited: u64, + /// Fee + pub fee: Option, + /// Message data, stored as json + pub messages: Option, + /// Location of the store transaction, (reference or resending) + pub stored_tx: Option, +} + +impl ser::Writeable for TxLogEntry { + fn write(&self, writer: &mut W) -> Result<(), ser::Error> { + writer.write_bytes(&serde_json::to_vec(self).map_err(|_| ser::Error::CorruptedData)?) + } +} + +impl ser::Readable for TxLogEntry { + fn read(reader: &mut dyn ser::Reader) -> Result { + let data = reader.read_bytes_len_prefix()?; + serde_json::from_slice(&data[..]).map_err(|_| ser::Error::CorruptedData) + } +} + +impl TxLogEntry { + /// Return a new blank with TS initialised with next entry + pub fn new(parent_key_id: Identifier, t: TxLogEntryType, id: u32) -> Self { + TxLogEntry { + parent_key_id: parent_key_id, + tx_type: t, + id: id, + tx_slate_id: None, + creation_ts: Utc::now(), + confirmation_ts: None, + confirmed: false, + amount_credited: 0, + amount_debited: 0, + num_inputs: 0, + num_outputs: 0, + fee: None, + messages: None, + stored_tx: None, + } + } + + /// Given a vec of TX log entries, return credited + debited sums + pub fn sum_confirmed(txs: &Vec) -> (u64, u64) { + txs.iter().fold((0, 0), |acc, tx| match tx.confirmed { + true => (acc.0 + tx.amount_credited, acc.1 + tx.amount_debited), + false => acc, + }) + } + + /// Update confirmation TS with now + pub fn update_confirmation_ts(&mut self) { + self.confirmation_ts = Some(Utc::now()); + } +} + +/// Map of named accounts to BIP32 paths +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct AcctPathMapping { + /// label used by user + pub label: String, + /// Corresponding parent BIP32 derivation path + pub path: Identifier, +} + +impl ser::Writeable for AcctPathMapping { + fn write(&self, writer: &mut W) -> Result<(), ser::Error> { + writer.write_bytes(&serde_json::to_vec(self).map_err(|_| ser::Error::CorruptedData)?) + } +} + +impl ser::Readable for AcctPathMapping { + fn read(reader: &mut dyn ser::Reader) -> Result { + let data = reader.read_bytes_len_prefix()?; + serde_json::from_slice(&data[..]).map_err(|_| ser::Error::CorruptedData) + } +} + +/// Dummy wrapper for the hex-encoded serialized transaction. +#[derive(Serialize, Deserialize)] +pub struct TxWrapper { + /// hex representation of transaction + pub tx_hex: String, +} + +/// Send TX API Args +#[derive(Clone, Serialize, Deserialize)] +pub struct SendTXArgs { + /// amount to send + pub amount: u64, + /// minimum confirmations + pub minimum_confirmations: u64, + /// payment method + pub method: String, + /// destination url + pub dest: String, + /// Max number of outputs + pub max_outputs: usize, + /// Number of change outputs to generate + pub num_change_outputs: usize, + /// whether to use all outputs (combine) + pub selection_strategy_is_use_all: bool, + /// Optional message, that will be signed + pub message: Option, +} diff --git a/src/bin/cmd/.server.rs.swp b/src/bin/cmd/.server.rs.swp new file mode 100644 index 0000000..6289c8e Binary files /dev/null and b/src/bin/cmd/.server.rs.swp differ diff --git a/src/bin/cmd/config.rs b/src/bin/cmd/config.rs new file mode 100644 index 0000000..61b4a05 --- /dev/null +++ b/src/bin/cmd/config.rs @@ -0,0 +1,90 @@ +// Copyright 2018 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Grin configuration file output command +use crate::config::{config, GlobalConfig, GlobalWalletConfig, GRIN_WALLET_DIR}; +use crate::core::global; +use std::env; + +/// Create a config file in the current directory +pub fn config_command_server(chain_type: &global::ChainTypes, file_name: &str) { + let mut default_config = GlobalConfig::for_chain(chain_type); + let current_dir = env::current_dir().unwrap_or_else(|e| { + panic!("Error creating config file: {}", e); + }); + let mut config_file_name = current_dir.clone(); + config_file_name.push(file_name); + if config_file_name.exists() { + panic!( + "{} already exists in the current directory. Please remove it first", + file_name + ); + } + default_config.update_paths(¤t_dir); + default_config + .write_to_file(config_file_name.to_str().unwrap()) + .unwrap_or_else(|e| { + panic!("Error creating config file: {}", e); + }); + + println!( + "{} file configured and created in current directory", + file_name + ); +} + +/// Create a config file in the current directory +pub fn config_command_wallet(chain_type: &global::ChainTypes, file_name: &str) { + let mut default_config = GlobalWalletConfig::for_chain(chain_type); + let current_dir = env::current_dir().unwrap_or_else(|e| { + panic!("Error creating config file: {}", e); + }); + let mut config_file_name = current_dir.clone(); + config_file_name.push(file_name); + + let mut data_dir_name = current_dir.clone(); + data_dir_name.push(GRIN_WALLET_DIR); + + if config_file_name.exists() && data_dir_name.exists() { + panic!( + "{} already exists in the target directory. Please remove it first", + file_name + ); + } + + // just leave as is if file exists but there's no data dir + if config_file_name.exists() { + return; + } + + default_config.update_paths(¤t_dir); + default_config + .write_to_file(config_file_name.to_str().unwrap()) + .unwrap_or_else(|e| { + panic!("Error creating config file: {}", e); + }); + + println!( + "File {} configured and created", + config_file_name.to_str().unwrap(), + ); + + let mut api_secret_path = current_dir.clone(); + api_secret_path.push(config::API_SECRET_FILE_NAME); + if !api_secret_path.exists() { + config::init_api_secret(&api_secret_path).unwrap(); + } else { + config::check_api_secret(&api_secret_path).unwrap(); + } +} diff --git a/src/bin/cmd/mod.rs b/src/bin/cmd/mod.rs new file mode 100644 index 0000000..0df1f5f --- /dev/null +++ b/src/bin/cmd/mod.rs @@ -0,0 +1,21 @@ +// Copyright 2018 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +mod config; +mod wallet; +mod wallet_args; +mod wallet_tests; + +pub use self::config::{config_command_server, config_command_wallet}; +pub use self::wallet::{seed_exists, wallet_command}; diff --git a/src/bin/cmd/wallet.rs b/src/bin/cmd/wallet.rs new file mode 100644 index 0000000..31328ec --- /dev/null +++ b/src/bin/cmd/wallet.rs @@ -0,0 +1,68 @@ +// Copyright 2018 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::cmd::wallet_args; +use crate::config::GlobalWalletConfig; +use clap::ArgMatches; +use grin_wallet::{self, HTTPNodeClient, WalletConfig, WalletSeed}; +use std::path::PathBuf; +use std::thread; +use std::time::Duration; + +pub fn _init_wallet_seed(wallet_config: WalletConfig, password: &str) { + if let Err(_) = WalletSeed::from_file(&wallet_config, password) { + WalletSeed::init_file(&wallet_config, 32, None, password) + .expect("Failed to create wallet seed file."); + }; +} + +pub fn seed_exists(wallet_config: WalletConfig) -> bool { + let mut data_file_dir = PathBuf::new(); + data_file_dir.push(wallet_config.data_file_dir); + data_file_dir.push(grin_wallet::SEED_FILE); + if data_file_dir.exists() { + true + } else { + false + } +} + +pub fn wallet_command(wallet_args: &ArgMatches<'_>, config: GlobalWalletConfig) -> i32 { + // just get defaults from the global config + let wallet_config = config.members.unwrap().wallet; + + // web wallet http server must be started from here + // NB: Turned off for the time being + /*let _ = match wallet_args.subcommand() { + ("web", Some(_)) => start_webwallet_server(), + _ => {} + };*/ + + let node_client = HTTPNodeClient::new(&wallet_config.check_node_api_http_addr, None); + let res = wallet_args::wallet_command(wallet_args, wallet_config, node_client); + + // we need to give log output a chance to catch up before exiting + thread::sleep(Duration::from_millis(100)); + + if let Err(e) = res { + println!("Wallet command failed: {}", e); + 1 + } else { + println!( + "Command '{}' completed successfully", + wallet_args.subcommand().0 + ); + 0 + } +} diff --git a/src/bin/cmd/wallet_args.rs b/src/bin/cmd/wallet_args.rs new file mode 100644 index 0000000..4ae2f19 --- /dev/null +++ b/src/bin/cmd/wallet_args.rs @@ -0,0 +1,634 @@ +// Copyright 2018 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::api::TLSConfig; +use crate::util::file::get_first_line; +use crate::util::{Mutex, ZeroingString}; +/// Argument parsing and error handling for wallet commands +use clap::ArgMatches; +use failure::Fail; +use grin_core as core; +use grin_keychain as keychain; +use grin_wallet::{command, instantiate_wallet, NodeClient, WalletConfig, WalletInst, WalletSeed}; +use grin_wallet::{Error, ErrorKind}; +use linefeed::terminal::Signal; +use linefeed::{Interface, ReadResult}; +use rpassword; +use std::path::Path; +use std::sync::Arc; + +// define what to do on argument error +macro_rules! arg_parse { + ( $r:expr ) => { + match $r { + Ok(res) => res, + Err(e) => { + return Err(ErrorKind::ArgumentError(format!("{}", e)).into()); + } + } + }; +} +/// Simple error definition, just so we can return errors from all commands +/// and let the caller figure out what to do +#[derive(Clone, Eq, PartialEq, Debug, Fail)] +pub enum ParseError { + #[fail(display = "Invalid Arguments: {}", _0)] + ArgumentError(String), + #[fail(display = "Parsing IO error: {}", _0)] + IOError(String), + #[fail(display = "User Cancelled")] + CancelledError, +} + +impl From for ParseError { + fn from(e: std::io::Error) -> ParseError { + ParseError::IOError(format!("{}", e)) + } +} + +fn prompt_password_stdout(prompt: &str) -> ZeroingString { + ZeroingString::from(rpassword::prompt_password_stdout(prompt).unwrap()) +} + +pub fn prompt_password(password: &Option) -> ZeroingString { + match password { + None => prompt_password_stdout("Password: "), + Some(p) => p.clone(), + } +} + +fn prompt_password_confirm() -> ZeroingString { + let mut first = ZeroingString::from("first"); + let mut second = ZeroingString::from("second"); + while first != second { + first = prompt_password_stdout("Password: "); + second = prompt_password_stdout("Confirm Password: "); + } + first +} + +fn prompt_replace_seed() -> Result { + let interface = Arc::new(Interface::new("replace_seed")?); + interface.set_report_signal(Signal::Interrupt, true); + interface.set_prompt("Replace seed? (y/n)> ")?; + println!(); + println!("Existing wallet.seed file already exists. Continue?"); + println!("Continuing will back up your existing 'wallet.seed' file as 'wallet.seed.bak'"); + println!(); + loop { + let res = interface.read_line()?; + match res { + ReadResult::Eof => return Ok(false), + ReadResult::Signal(sig) => { + if sig == Signal::Interrupt { + interface.cancel_read_line()?; + return Err(ParseError::CancelledError); + } + } + ReadResult::Input(line) => match line.trim() { + "Y" | "y" => return Ok(true), + "N" | "n" => return Ok(false), + _ => println!("Please respond y or n"), + }, + } + } +} + +fn prompt_recovery_phrase() -> Result { + let interface = Arc::new(Interface::new("recover")?); + let mut phrase = ZeroingString::from(""); + interface.set_report_signal(Signal::Interrupt, true); + interface.set_prompt("phrase> ")?; + loop { + println!("Please enter your recovery phrase:"); + let res = interface.read_line()?; + match res { + ReadResult::Eof => break, + ReadResult::Signal(sig) => { + if sig == Signal::Interrupt { + interface.cancel_read_line()?; + return Err(ParseError::CancelledError); + } + } + ReadResult::Input(line) => { + if WalletSeed::from_mnemonic(&line).is_ok() { + phrase = ZeroingString::from(line); + break; + } else { + println!(); + println!("Recovery word phrase is invalid."); + println!(); + interface.set_buffer(&line)?; + } + } + } + } + Ok(phrase) +} + +// instantiate wallet (needed by most functions) + +pub fn inst_wallet( + config: WalletConfig, + g_args: &command::GlobalArgs, + node_client: impl NodeClient + 'static, +) -> Result>>, ParseError> { + let passphrase = prompt_password(&g_args.password); + let res = instantiate_wallet(config.clone(), node_client, &passphrase, &g_args.account); + match res { + Ok(p) => Ok(p), + Err(e) => { + let msg = { + match e.kind() { + ErrorKind::Encryption => { + format!("Error decrypting wallet seed (check provided password)") + } + _ => format!("Error instantiating wallet: {}", e), + } + }; + Err(ParseError::ArgumentError(msg)) + } + } +} + +// parses a required value, or throws error with message otherwise +fn parse_required<'a>(args: &'a ArgMatches, name: &str) -> Result<&'a str, ParseError> { + let arg = args.value_of(name); + match arg { + Some(ar) => Ok(ar), + None => { + let msg = format!("Value for argument '{}' is required in this context", name,); + Err(ParseError::ArgumentError(msg)) + } + } +} + +// parses a number, or throws error with message otherwise +fn parse_u64(arg: &str, name: &str) -> Result { + let val = arg.parse::(); + match val { + Ok(v) => Ok(v), + Err(e) => { + let msg = format!("Could not parse {} as a whole number. e={}", name, e); + Err(ParseError::ArgumentError(msg)) + } + } +} + +pub fn parse_global_args( + config: &WalletConfig, + args: &ArgMatches, +) -> Result { + let account = parse_required(args, "account")?; + let mut show_spent = false; + if args.is_present("show_spent") { + show_spent = true; + } + let node_api_secret = get_first_line(config.node_api_secret_path.clone()); + let password = match args.value_of("pass") { + None => None, + Some(p) => Some(ZeroingString::from(p)), + }; + + let tls_conf = match config.tls_certificate_file.clone() { + None => None, + Some(file) => { + let key = match config.tls_certificate_key.clone() { + Some(k) => k, + None => { + let msg = format!("Private key for certificate is not set"); + return Err(ParseError::ArgumentError(msg)); + } + }; + Some(TLSConfig::new(file, key)) + } + }; + + Ok(command::GlobalArgs { + account: account.to_owned(), + show_spent: show_spent, + node_api_secret: node_api_secret, + password: password, + tls_conf: tls_conf, + }) +} + +pub fn parse_init_args( + config: &WalletConfig, + g_args: &command::GlobalArgs, + args: &ArgMatches, +) -> Result { + if let Err(e) = WalletSeed::seed_file_exists(config) { + let msg = format!("Not creating wallet - {}", e.inner); + return Err(ParseError::ArgumentError(msg)); + } + let list_length = match args.is_present("short_wordlist") { + false => 32, + true => 16, + }; + let recovery_phrase = match args.is_present("recover") { + true => Some(prompt_recovery_phrase()?), + false => None, + }; + + if recovery_phrase.is_some() { + println!("Please provide a new password for the recovered wallet"); + } else { + println!("Please enter a password for your new wallet"); + } + + let password = match g_args.password.clone() { + Some(p) => p, + None => prompt_password_confirm(), + }; + + Ok(command::InitArgs { + list_length: list_length, + password: password, + config: config.clone(), + recovery_phrase: recovery_phrase, + restore: false, + }) +} + +pub fn parse_recover_args( + config: &WalletConfig, + g_args: &command::GlobalArgs, + args: &ArgMatches, +) -> Result { + let (passphrase, recovery_phrase) = { + match args.is_present("display") { + true => (prompt_password(&g_args.password), None), + false => { + let cont = { + if command::wallet_seed_exists(config).is_err() { + prompt_replace_seed()? + } else { + true + } + }; + if !cont { + return Err(ParseError::CancelledError); + } + let phrase = prompt_recovery_phrase()?; + println!("Please provide a new password for the recovered wallet"); + (prompt_password_confirm(), Some(phrase.to_owned())) + } + } + }; + Ok(command::RecoverArgs { + passphrase: passphrase, + recovery_phrase: recovery_phrase, + }) +} + +pub fn parse_listen_args( + config: &mut WalletConfig, + g_args: &mut command::GlobalArgs, + args: &ArgMatches, +) -> Result { + // listen args + let pass = match g_args.password.clone() { + Some(p) => Some(p.to_owned()), + None => Some(prompt_password(&None)), + }; + g_args.password = pass; + if let Some(port) = args.value_of("port") { + config.api_listen_port = port.parse().unwrap(); + } + let method = parse_required(args, "method")?; + Ok(command::ListenArgs { + method: method.to_owned(), + }) +} + +pub fn parse_account_args(account_args: &ArgMatches) -> Result { + let create = match account_args.value_of("create") { + None => None, + Some(s) => Some(s.to_owned()), + }; + Ok(command::AccountArgs { create: create }) +} + +pub fn parse_send_args(args: &ArgMatches) -> Result { + // amount + let amount = parse_required(args, "amount")?; + let amount = core::core::amount_from_hr_string(amount); + let amount = match amount { + Ok(a) => a, + Err(e) => { + let msg = format!( + "Could not parse amount as a number with optional decimal point. e={}", + e + ); + return Err(ParseError::ArgumentError(msg)); + } + }; + + // message + let message = match args.is_present("message") { + true => Some(args.value_of("message").unwrap().to_owned()), + false => None, + }; + + // minimum_confirmations + let min_c = parse_required(args, "minimum_confirmations")?; + let min_c = parse_u64(min_c, "minimum_confirmations")?; + + // selection_strategy + let selection_strategy = parse_required(args, "selection_strategy")?; + + // estimate_selection_strategies + let estimate_selection_strategies = args.is_present("estimate_selection_strategies"); + + // method + let method = parse_required(args, "method")?; + + // dest + let dest = { + if method == "self" { + match args.value_of("dest") { + Some(d) => d, + None => "default", + } + } else { + if !estimate_selection_strategies { + parse_required(args, "dest")? + } else { + "" + } + } + }; + if !estimate_selection_strategies + && method == "http" + && !dest.starts_with("http://") + && !dest.starts_with("https://") + { + let msg = format!( + "HTTP Destination should start with http://: or https://: {}", + dest, + ); + return Err(ParseError::ArgumentError(msg)); + } + + // change_outputs + let change_outputs = parse_required(args, "change_outputs")?; + let change_outputs = parse_u64(change_outputs, "change_outputs")? as usize; + + // fluff + let fluff = args.is_present("fluff"); + + // max_outputs + let max_outputs = 500; + + Ok(command::SendArgs { + amount: amount, + message: message, + minimum_confirmations: min_c, + selection_strategy: selection_strategy.to_owned(), + estimate_selection_strategies, + method: method.to_owned(), + dest: dest.to_owned(), + change_outputs: change_outputs, + fluff: fluff, + max_outputs: max_outputs, + }) +} + +pub fn parse_receive_args(receive_args: &ArgMatches) -> Result { + // message + let message = match receive_args.is_present("message") { + true => Some(receive_args.value_of("message").unwrap().to_owned()), + false => None, + }; + + // input + let tx_file = parse_required(receive_args, "input")?; + + // validate input + if !Path::new(&tx_file).is_file() { + let msg = format!("File {} not found.", &tx_file); + return Err(ParseError::ArgumentError(msg)); + } + + Ok(command::ReceiveArgs { + input: tx_file.to_owned(), + message: message, + }) +} + +pub fn parse_finalize_args(args: &ArgMatches) -> Result { + let fluff = args.is_present("fluff"); + let tx_file = parse_required(args, "input")?; + + if !Path::new(&tx_file).is_file() { + let msg = format!("File {} not found.", tx_file); + return Err(ParseError::ArgumentError(msg)); + } + Ok(command::FinalizeArgs { + input: tx_file.to_owned(), + fluff: fluff, + }) +} + +pub fn parse_info_args(args: &ArgMatches) -> Result { + // minimum_confirmations + let mc = parse_required(args, "minimum_confirmations")?; + let mc = parse_u64(mc, "minimum_confirmations")?; + Ok(command::InfoArgs { + minimum_confirmations: mc, + }) +} + +pub fn parse_txs_args(args: &ArgMatches) -> Result { + let tx_id = match args.value_of("id") { + None => None, + Some(tx) => Some(parse_u64(tx, "id")? as u32), + }; + Ok(command::TxsArgs { id: tx_id }) +} + +pub fn parse_repost_args(args: &ArgMatches) -> Result { + let tx_id = match args.value_of("id") { + None => None, + Some(tx) => Some(parse_u64(tx, "id")? as u32), + }; + + let fluff = args.is_present("fluff"); + let dump_file = match args.value_of("dumpfile") { + None => None, + Some(d) => Some(d.to_owned()), + }; + + Ok(command::RepostArgs { + id: tx_id.unwrap(), + dump_file: dump_file, + fluff: fluff, + }) +} + +pub fn parse_cancel_args(args: &ArgMatches) -> Result { + let mut tx_id_string = ""; + let tx_id = match args.value_of("id") { + None => None, + Some(tx) => Some(parse_u64(tx, "id")? as u32), + }; + let tx_slate_id = match args.value_of("txid") { + None => None, + Some(tx) => match tx.parse() { + Ok(t) => { + tx_id_string = tx; + Some(t) + } + Err(e) => { + let msg = format!("Could not parse txid parameter. e={}", e); + return Err(ParseError::ArgumentError(msg)); + } + }, + }; + if (tx_id.is_none() && tx_slate_id.is_none()) || (tx_id.is_some() && tx_slate_id.is_some()) { + let msg = format!("'id' (-i) or 'txid' (-t) argument is required."); + return Err(ParseError::ArgumentError(msg)); + } + Ok(command::CancelArgs { + tx_id: tx_id, + tx_slate_id: tx_slate_id, + tx_id_string: tx_id_string.to_owned(), + }) +} + +pub fn wallet_command( + wallet_args: &ArgMatches, + mut wallet_config: WalletConfig, + mut node_client: impl NodeClient + 'static, +) -> Result { + if let Some(t) = wallet_config.chain_type.clone() { + core::global::set_mining_mode(t); + } + + if wallet_args.is_present("external") { + wallet_config.api_listen_interface = "0.0.0.0".to_string(); + } + + if let Some(dir) = wallet_args.value_of("data_dir") { + wallet_config.data_file_dir = dir.to_string().clone(); + } + + if let Some(sa) = wallet_args.value_of("api_server_address") { + wallet_config.check_node_api_http_addr = sa.to_string().clone(); + } + + let global_wallet_args = arg_parse!(parse_global_args(&wallet_config, &wallet_args)); + + node_client.set_node_url(&wallet_config.check_node_api_http_addr); + node_client.set_node_api_secret(global_wallet_args.node_api_secret.clone()); + + // closure to instantiate wallet as needed by each subcommand + let inst_wallet = || { + let res = inst_wallet(wallet_config.clone(), &global_wallet_args, node_client); + res.unwrap_or_else(|e| { + println!("{}", e); + std::process::exit(1); + }) + }; + + let res = match wallet_args.subcommand() { + ("init", Some(args)) => { + let a = arg_parse!(parse_init_args(&wallet_config, &global_wallet_args, &args)); + command::init(&global_wallet_args, a) + } + ("recover", Some(args)) => { + let a = arg_parse!(parse_recover_args( + &wallet_config, + &global_wallet_args, + &args + )); + command::recover(&wallet_config, a) + } + ("listen", Some(args)) => { + let mut c = wallet_config.clone(); + let mut g = global_wallet_args.clone(); + let a = arg_parse!(parse_listen_args(&mut c, &mut g, &args)); + command::listen(&wallet_config, &a, &g) + } + ("owner_api", Some(_)) => { + let mut g = global_wallet_args.clone(); + g.tls_conf = None; + command::owner_api(inst_wallet(), &wallet_config, &g) + } + ("web", Some(_)) => command::owner_api(inst_wallet(), &wallet_config, &global_wallet_args), + ("account", Some(args)) => { + let a = arg_parse!(parse_account_args(&args)); + command::account(inst_wallet(), a) + } + ("send", Some(args)) => { + let a = arg_parse!(parse_send_args(&args)); + command::send( + inst_wallet(), + a, + wallet_config.dark_background_color_scheme.unwrap_or(true), + ) + } + ("receive", Some(args)) => { + let a = arg_parse!(parse_receive_args(&args)); + command::receive(inst_wallet(), &global_wallet_args, a) + } + ("finalize", Some(args)) => { + let a = arg_parse!(parse_finalize_args(&args)); + command::finalize(inst_wallet(), a) + } + ("info", Some(args)) => { + let a = arg_parse!(parse_info_args(&args)); + command::info( + inst_wallet(), + &global_wallet_args, + a, + wallet_config.dark_background_color_scheme.unwrap_or(true), + ) + } + ("outputs", Some(_)) => command::outputs( + inst_wallet(), + &global_wallet_args, + wallet_config.dark_background_color_scheme.unwrap_or(true), + ), + ("txs", Some(args)) => { + let a = arg_parse!(parse_txs_args(&args)); + command::txs( + inst_wallet(), + &global_wallet_args, + a, + wallet_config.dark_background_color_scheme.unwrap_or(true), + ) + } + ("repost", Some(args)) => { + let a = arg_parse!(parse_repost_args(&args)); + command::repost(inst_wallet(), a) + } + ("cancel", Some(args)) => { + let a = arg_parse!(parse_cancel_args(&args)); + command::cancel(inst_wallet(), a) + } + ("restore", Some(_)) => command::restore(inst_wallet()), + ("check", Some(_)) => command::check_repair(inst_wallet()), + _ => { + let msg = format!("Unknown wallet command, use 'grin help wallet' for details"); + return Err(ErrorKind::ArgumentError(msg).into()); + } + }; + if let Err(e) = res { + Err(e) + } else { + Ok(wallet_args.subcommand().0.to_owned()) + } +} diff --git a/src/bin/cmd/wallet_tests.rs b/src/bin/cmd/wallet_tests.rs new file mode 100644 index 0000000..189dffb --- /dev/null +++ b/src/bin/cmd/wallet_tests.rs @@ -0,0 +1,528 @@ +// Copyright 2018 The Grin Developers +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Test wallet command line works as expected +#[cfg(test)] +mod wallet_tests { + use clap; + use grin_util as util; + use grin_wallet; + + use grin_wallet::test_framework::{self, LocalWalletClient, WalletProxy}; + + use clap::{App, ArgMatches}; + use grin_util::Mutex; + use std::sync::Arc; + use std::thread; + use std::time::Duration; + use std::{env, fs}; + + use grin_config::GlobalWalletConfig; + use grin_core::global; + use grin_core::global::ChainTypes; + use grin_keychain::ExtKeychain; + use grin_wallet::{LMDBBackend, WalletBackend, WalletConfig, WalletInst, WalletSeed}; + + use super::super::wallet_args; + + fn clean_output_dir(test_dir: &str) { + let _ = fs::remove_dir_all(test_dir); + } + + fn setup(test_dir: &str) { + util::init_test_logger(); + clean_output_dir(test_dir); + global::set_mining_mode(ChainTypes::AutomatedTesting); + } + + /// Create a wallet config file in the given current directory + pub fn config_command_wallet( + dir_name: &str, + wallet_name: &str, + ) -> Result<(), grin_wallet::Error> { + let mut current_dir; + let mut default_config = GlobalWalletConfig::default(); + current_dir = env::current_dir().unwrap_or_else(|e| { + panic!("Error creating config file: {}", e); + }); + current_dir.push(dir_name); + current_dir.push(wallet_name); + let _ = fs::create_dir_all(current_dir.clone()); + let mut config_file_name = current_dir.clone(); + config_file_name.push("grin-wallet.toml"); + if config_file_name.exists() { + return Err(grin_wallet::ErrorKind::ArgumentError( + "grin-wallet.toml already exists in the target directory. Please remove it first" + .to_owned(), + ))?; + } + default_config.update_paths(¤t_dir); + default_config + .write_to_file(config_file_name.to_str().unwrap()) + .unwrap_or_else(|e| { + panic!("Error creating config file: {}", e); + }); + + println!( + "File {} configured and created", + config_file_name.to_str().unwrap(), + ); + Ok(()) + } + + /// Handles setup and detection of paths for wallet + pub fn initial_setup_wallet(dir_name: &str, wallet_name: &str) -> WalletConfig { + let mut current_dir; + current_dir = env::current_dir().unwrap_or_else(|e| { + panic!("Error creating config file: {}", e); + }); + current_dir.push(dir_name); + current_dir.push(wallet_name); + let _ = fs::create_dir_all(current_dir.clone()); + let mut config_file_name = current_dir.clone(); + config_file_name.push("grin-wallet.toml"); + GlobalWalletConfig::new(config_file_name.to_str().unwrap()) + .unwrap() + .members + .unwrap() + .wallet + } + + fn get_wallet_subcommand<'a>( + wallet_dir: &str, + wallet_name: &str, + args: ArgMatches<'a>, + ) -> ArgMatches<'a> { + match args.subcommand() { + ("wallet", Some(wallet_args)) => { + // wallet init command should spit out its config file then continue + // (if desired) + if let ("init", Some(init_args)) = wallet_args.subcommand() { + if init_args.is_present("here") { + let _ = config_command_wallet(wallet_dir, wallet_name); + } + } + wallet_args.to_owned() + } + _ => ArgMatches::new(), + } + } + // + // Helper to create an instance of the LMDB wallet + fn instantiate_wallet( + mut wallet_config: WalletConfig, + node_client: LocalWalletClient, + passphrase: &str, + account: &str, + ) -> Result>>, grin_wallet::Error> { + wallet_config.chain_type = None; + // First test decryption, so we can abort early if we have the wrong password + let _ = WalletSeed::from_file(&wallet_config, passphrase)?; + let mut db_wallet = LMDBBackend::new(wallet_config.clone(), passphrase, node_client)?; + db_wallet.set_parent_key_id_by_name(account)?; + info!("Using LMDB Backend for wallet"); + Ok(Arc::new(Mutex::new(db_wallet))) + } + + fn execute_command( + app: &App, + test_dir: &str, + wallet_name: &str, + client: &LocalWalletClient, + arg_vec: Vec<&str>, + ) -> Result { + let args = app.clone().get_matches_from(arg_vec); + let args = get_wallet_subcommand(test_dir, wallet_name, args.clone()); + let mut config = initial_setup_wallet(test_dir, wallet_name); + //unset chain type so it doesn't get reset + config.chain_type = None; + wallet_args::wallet_command(&args, config.clone(), client.clone()) + } + + /// command line tests + fn command_line_test_impl(test_dir: &str) -> Result<(), grin_wallet::Error> { + setup(test_dir); + // Create a new proxy to simulate server and wallet responses + let mut wallet_proxy: WalletProxy = + WalletProxy::new(test_dir); + let chain = wallet_proxy.chain.clone(); + + // load app yaml. If it don't exist, just say so and exit + let yml = load_yaml!("../grin.yml"); + let app = App::from_yaml(yml); + + // wallet init + let arg_vec = vec!["grin", "wallet", "-p", "password", "init", "-h"]; + // should create new wallet file + let client1 = LocalWalletClient::new("wallet1", wallet_proxy.tx.clone()); + execute_command(&app, test_dir, "wallet1", &client1, arg_vec.clone())?; + + // trying to init twice - should fail + assert!(execute_command(&app, test_dir, "wallet1", &client1, arg_vec.clone()).is_err()); + let client1 = LocalWalletClient::new("wallet1", wallet_proxy.tx.clone()); + + // add wallet to proxy + //let wallet1 = test_framework::create_wallet(&format!("{}/wallet1", test_dir), client1.clone()); + let config1 = initial_setup_wallet(test_dir, "wallet1"); + let wallet1 = instantiate_wallet(config1.clone(), client1.clone(), "password", "default")?; + wallet_proxy.add_wallet("wallet1", client1.get_send_instance(), wallet1.clone()); + + // Create wallet 2 + let client2 = LocalWalletClient::new("wallet2", wallet_proxy.tx.clone()); + execute_command(&app, test_dir, "wallet2", &client2, arg_vec.clone())?; + + let config2 = initial_setup_wallet(test_dir, "wallet2"); + let wallet2 = instantiate_wallet(config2.clone(), client2.clone(), "password", "default")?; + wallet_proxy.add_wallet("wallet2", client2.get_send_instance(), wallet2.clone()); + + // Set the wallet proxy listener running + thread::spawn(move || { + if let Err(e) = wallet_proxy.run() { + error!("Wallet Proxy error: {}", e); + } + }); + + // Create some accounts in wallet 1 + let arg_vec = vec![ + "grin", "wallet", "-p", "password", "account", "-c", "mining", + ]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + let arg_vec = vec![ + "grin", + "wallet", + "-p", + "password", + "account", + "-c", + "account_1", + ]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + // Create some accounts in wallet 2 + let arg_vec = vec![ + "grin", + "wallet", + "-p", + "password", + "account", + "-c", + "account_1", + ]; + execute_command(&app, test_dir, "wallet2", &client2, arg_vec.clone())?; + // already exists + assert!(execute_command(&app, test_dir, "wallet2", &client2, arg_vec).is_err()); + + let arg_vec = vec![ + "grin", + "wallet", + "-p", + "password", + "account", + "-c", + "account_2", + ]; + execute_command(&app, test_dir, "wallet2", &client2, arg_vec)?; + + // let's see those accounts + let arg_vec = vec!["grin", "wallet", "-p", "password", "account"]; + execute_command(&app, test_dir, "wallet2", &client2, arg_vec)?; + + // let's see those accounts + let arg_vec = vec!["grin", "wallet", "-p", "password", "account"]; + execute_command(&app, test_dir, "wallet2", &client2, arg_vec)?; + + // Mine a bit into wallet 1 so we have something to send + // (TODO: Be able to stop listeners so we can test this better) + let wallet1 = instantiate_wallet(config1.clone(), client1.clone(), "password", "default")?; + grin_wallet::controller::owner_single_use(wallet1.clone(), |api| { + api.set_active_account("mining")?; + Ok(()) + })?; + + let mut bh = 10u64; + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), bh as usize); + + let very_long_message = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef\ + ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef\ + ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef\ + ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef\ + ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef\ + ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef\ + ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef\ + ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef\ + This part should all be truncated"; + + // Update info and check + let arg_vec = vec!["grin", "wallet", "-p", "password", "-a", "mining", "info"]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + // try a file exchange + let file_name = format!("{}/tx1.part_tx", test_dir); + let response_file_name = format!("{}/tx1.part_tx.response", test_dir); + let arg_vec = vec![ + "grin", + "wallet", + "-p", + "password", + "-a", + "mining", + "send", + "-m", + "file", + "-d", + &file_name, + "-g", + very_long_message, + "10", + ]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + let arg_vec = vec![ + "grin", + "wallet", + "-p", + "password", + "-a", + "account_1", + "receive", + "-i", + &file_name, + "-g", + "Thanks, Yeast!", + ]; + execute_command(&app, test_dir, "wallet2", &client2, arg_vec.clone())?; + + // shouldn't be allowed to receive twice + assert!(execute_command(&app, test_dir, "wallet2", &client2, arg_vec).is_err()); + + let arg_vec = vec![ + "grin", + "wallet", + "-p", + "password", + "finalize", + "-i", + &response_file_name, + ]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + bh += 1; + + let wallet1 = instantiate_wallet(config1.clone(), client1.clone(), "password", "default")?; + + // Check our transaction log, should have 10 entries + grin_wallet::controller::owner_single_use(wallet1.clone(), |api| { + api.set_active_account("mining")?; + let (refreshed, txs) = api.retrieve_txs(true, None, None)?; + assert!(refreshed); + assert_eq!(txs.len(), bh as usize); + Ok(()) + })?; + + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), 10); + bh += 10; + + // update info for each + let arg_vec = vec!["grin", "wallet", "-p", "password", "-a", "mining", "info"]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + let arg_vec = vec![ + "grin", + "wallet", + "-p", + "password", + "-a", + "account_1", + "info", + ]; + execute_command(&app, test_dir, "wallet2", &client1, arg_vec)?; + + // check results in wallet 2 + let wallet2 = instantiate_wallet(config2.clone(), client2.clone(), "password", "default")?; + grin_wallet::controller::owner_single_use(wallet2.clone(), |api| { + api.set_active_account("account_1")?; + let (_, wallet1_info) = api.retrieve_summary_info(true, 1)?; + assert_eq!(wallet1_info.last_confirmed_height, bh); + assert_eq!(wallet1_info.amount_currently_spendable, 10_000_000_000); + Ok(()) + })?; + + // Self-send to same account, using smallest strategy + let arg_vec = vec![ + "grin", + "wallet", + "-p", + "password", + "-a", + "mining", + "send", + "-m", + "file", + "-d", + &file_name, + "-g", + "Love, Yeast, Smallest", + "-s", + "smallest", + "10", + ]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + let arg_vec = vec![ + "grin", + "wallet", + "-p", + "password", + "-a", + "mining", + "receive", + "-i", + &file_name, + "-g", + "Thanks, Yeast!", + ]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec.clone())?; + + let arg_vec = vec![ + "grin", + "wallet", + "-p", + "password", + "finalize", + "-i", + &response_file_name, + ]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + bh += 1; + + // Check our transaction log, should have bh entries + one for the self receive + let wallet1 = instantiate_wallet(config1.clone(), client1.clone(), "password", "default")?; + + grin_wallet::controller::owner_single_use(wallet1.clone(), |api| { + api.set_active_account("mining")?; + let (refreshed, txs) = api.retrieve_txs(true, None, None)?; + assert!(refreshed); + assert_eq!(txs.len(), bh as usize + 1); + Ok(()) + })?; + + // Try using the self-send method, splitting up outputs for the fun of it + let arg_vec = vec![ + "grin", + "wallet", + "-p", + "password", + "-a", + "mining", + "send", + "-m", + "self", + "-d", + "mining", + "-g", + "Self love", + "-o", + "3", + "-s", + "smallest", + "10", + ]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + bh += 1; + + // Check our transaction log, should have bh entries + 2 for the self receives + let wallet1 = instantiate_wallet(config1.clone(), client1.clone(), "password", "default")?; + + grin_wallet::controller::owner_single_use(wallet1.clone(), |api| { + api.set_active_account("mining")?; + let (refreshed, txs) = api.retrieve_txs(true, None, None)?; + assert!(refreshed); + assert_eq!(txs.len(), bh as usize + 2); + Ok(()) + })?; + + // Another file exchange, don't send, but unlock with repair command + let arg_vec = vec![ + "grin", + "wallet", + "-p", + "password", + "-a", + "mining", + "send", + "-m", + "file", + "-d", + &file_name, + "-g", + "Ain't sending", + "10", + ]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + let arg_vec = vec!["grin", "wallet", "-p", "password", "check"]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + // Another file exchange, cancel this time + let arg_vec = vec![ + "grin", + "wallet", + "-p", + "password", + "-a", + "mining", + "send", + "-m", + "file", + "-d", + &file_name, + "-g", + "Ain't sending 2", + "10", + ]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + let arg_vec = vec![ + "grin", "wallet", "-p", "password", "-a", "mining", "cancel", "-i", "26", + ]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + // txs and outputs (mostly spit out for a visual in test logs) + let arg_vec = vec!["grin", "wallet", "-p", "password", "-a", "mining", "txs"]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + // message output (mostly spit out for a visual in test logs) + let arg_vec = vec![ + "grin", "wallet", "-p", "password", "-a", "mining", "txs", "-i", "10", + ]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + // txs and outputs (mostly spit out for a visual in test logs) + let arg_vec = vec![ + "grin", "wallet", "-p", "password", "-a", "mining", "outputs", + ]; + execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + + // let logging finish + thread::sleep(Duration::from_millis(200)); + Ok(()) + } + + #[test] + fn wallet_command_line() { + let test_dir = "target/test_output/command_line"; + if let Err(e) = command_line_test_impl(test_dir) { + panic!("Libwallet Error: {} - {}", e, e.backtrace().unwrap()); + } + } +} diff --git a/src/bin/grin-wallet.rs b/src/bin/grin-wallet.rs new file mode 100644 index 0000000..8b2b71c --- /dev/null +++ b/src/bin/grin-wallet.rs @@ -0,0 +1,187 @@ +// Copyright 2018 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Main for building the binary of a Grin peer-to-peer node. + +#[macro_use] +extern crate clap; + +#[macro_use] +extern crate log; +use crate::config::config::{SERVER_CONFIG_FILE_NAME, WALLET_CONFIG_FILE_NAME}; +use crate::core::global; +use crate::util::init_logger; +use clap::App; +use grin_api as api; +use grin_config as config; +use grin_core as core; +use grin_p2p as p2p; +use grin_servers as servers; +use grin_util as util; +use std::process::exit; + +mod cmd; + +// include build information +pub mod built_info { + include!(concat!(env!("OUT_DIR"), "/built.rs")); +} + +pub fn info_strings() -> (String, String) { + ( + format!( + "This is Grin version {}{}, built for {} by {}.", + built_info::PKG_VERSION, + built_info::GIT_VERSION.map_or_else(|| "".to_owned(), |v| format!(" (git {})", v)), + built_info::TARGET, + built_info::RUSTC_VERSION, + ) + .to_string(), + format!( + "Built with profile \"{}\", features \"{}\".", + built_info::PROFILE, + built_info::FEATURES_STR, + ) + .to_string(), + ) +} + +fn log_build_info() { + let (basic_info, detailed_info) = info_strings(); + info!("{}", basic_info); + debug!("{}", detailed_info); +} + +fn main() { + let exit_code = real_main(); + std::process::exit(exit_code); +} + +fn real_main() -> i32 { + let yml = load_yaml!("grin-wallet.yml"); + let args = App::from_yaml(yml).get_matches(); + let mut wallet_config = None; + let mut node_config = None; + + let chain_type = if args.is_present("floonet") { + global::ChainTypes::Floonet + } else if args.is_present("usernet") { + global::ChainTypes::UserTesting + } else { + global::ChainTypes::Mainnet + }; + + // Deal with configuration file creation + match args.subcommand() { + ("wallet", Some(wallet_args)) => { + // wallet init command should spit out its config file then continue + // (if desired) + if let ("init", Some(init_args)) = wallet_args.subcommand() { + if init_args.is_present("here") { + cmd::config_command_wallet(&chain_type, WALLET_CONFIG_FILE_NAME); + } + } + } + _ => {} + } + + // Load relevant config + match args.subcommand() { + // If it's a wallet command, try and load a wallet config file + ("wallet", Some(wallet_args)) => { + let mut w = config::initial_setup_wallet(&chain_type).unwrap_or_else(|e| { + panic!("Error loading wallet configuration: {}", e); + }); + if !cmd::seed_exists(w.members.as_ref().unwrap().wallet.clone()) { + if "init" == wallet_args.subcommand().0 || "recover" == wallet_args.subcommand().0 { + } else { + println!("Wallet seed file doesn't exist. Run `grin wallet init` first"); + exit(1); + } + } + let mut l = w.members.as_mut().unwrap().logging.clone().unwrap(); + l.tui_running = Some(false); + init_logger(Some(l)); + info!( + "Using wallet configuration file at {}", + w.config_file_path.as_ref().unwrap().to_str().unwrap() + ); + wallet_config = Some(w); + } + // When the subscommand is 'server' take into account the 'config_file' flag + ("server", Some(server_args)) => { + if let Some(_path) = server_args.value_of("config_file") { + node_config = Some(config::GlobalConfig::new(_path).unwrap_or_else(|e| { + panic!("Error loading server configuration: {}", e); + })); + } else { + node_config = Some( + config::initial_setup_server(&chain_type).unwrap_or_else(|e| { + panic!("Error loading server configuration: {}", e); + }), + ); + } + } + // Otherwise load up the node config as usual + _ => { + node_config = Some( + config::initial_setup_server(&chain_type).unwrap_or_else(|e| { + panic!("Error loading server configuration: {}", e); + }), + ); + } + } + + if let Some(mut config) = node_config.clone() { + let mut l = config.members.as_mut().unwrap().logging.clone().unwrap(); + let run_tui = config.members.as_mut().unwrap().server.run_tui; + if let Some(true) = run_tui { + l.log_to_stdout = false; + l.tui_running = Some(true); + } + init_logger(Some(l)); + + global::set_mining_mode(config.members.unwrap().server.clone().chain_type); + + if let Some(file_path) = &config.config_file_path { + info!( + "Using configuration file at {}", + file_path.to_str().unwrap() + ); + } else { + info!("Node configuration file not found, using default"); + } + } + + log_build_info(); + + // Execute subcommand + match args.subcommand() { + // server commands and options + ("server", Some(server_args)) => { + cmd::server_command(Some(server_args), node_config.unwrap()) + } + + // client commands and options + ("client", Some(client_args)) => cmd::client_command(client_args, node_config.unwrap()), + + // client commands and options + ("wallet", Some(wallet_args)) => cmd::wallet_command(wallet_args, wallet_config.unwrap()), + + // If nothing is specified, try to just use the config file instead + // this could possibly become the way to configure most things + // with most command line options being phased out + _ => cmd::server_command(None, node_config.unwrap()), + } +} diff --git a/src/bin/grin-wallet.yml b/src/bin/grin-wallet.yml new file mode 100644 index 0000000..94a18e0 --- /dev/null +++ b/src/bin/grin-wallet.yml @@ -0,0 +1,303 @@ +name: grin +version: "1.0.1" +about: Lightweight implementation of the MimbleWimble protocol. +author: The Grin Team + +args: + - floonet: + help: Run grin against the Floonet (as opposed to mainnet) + long: floonet + takes_value: false + - usernet: + help: Run grin as a local-only network. Doesn't block peer connections but will not connect to any peer or seed + long: usernet + takes_value: false +subcommands: + - server: + about: Control the Grin server + args: + - config_file: + help: Path to a grin-server.toml configuration file + short: c + long: config_file + takes_value: true + - port: + help: Port to start the P2P server on + short: p + long: port + takes_value: true + - api_port: + help: Port on which to start the api server (e.g. transaction pool api) + short: api + long: api_port + takes_value: true + - seed: + help: Override seed node(s) to connect to + short: s + long: seed + takes_value: true + - wallet_url: + help: The wallet listener to which mining rewards will be sent + short: w + long: wallet_url + takes_value: true + subcommands: + - config: + about: Generate a configuration grin-server.toml file in the current directory + - run: + about: Run the Grin server in this console + - client: + about: Communicates with the Grin server + subcommands: + - status: + about: Current status of the Grin chain + - listconnectedpeers: + about: Print a list of currently connected peers + - ban: + about: Ban peer + args: + - peer: + help: Peer ip and port (e.g. 10.12.12.13:13414) + short: p + long: peer + required: true + takes_value: true + - unban: + about: Unban peer + args: + - peer: + help: Peer ip and port (e.g. 10.12.12.13:13414) + short: p + long: peer + required: true + takes_value: true + - wallet: + about: Wallet software for Grin + args: + - pass: + help: Wallet passphrase used to encrypt wallet seed + short: p + long: pass + takes_value: true + - account: + help: Wallet account to use for this operation + short: a + long: account + takes_value: true + default_value: default + - data_dir: + help: Directory in which to store wallet files + short: dd + long: data_dir + takes_value: true + - external: + help: Listen on 0.0.0.0 interface to allow external connections (default is 127.0.0.1) + short: e + long: external + takes_value: false + - show_spent: + help: Show spent outputs on wallet output commands + short: s + long: show_spent + takes_value: false + - api_server_address: + help: Api address of running node on which to check inputs and post transactions + short: r + long: api_server_address + takes_value: true + subcommands: + - account: + about: List wallet accounts or create a new account + args: + - create: + help: Create a new wallet account with provided name + short: c + long: create + takes_value: true + - listen: + about: Runs the wallet in listening mode waiting for transactions + args: + - port: + help: Port on which to run the wallet listener + short: l + long: port + takes_value: true + - method: + help: Which method to use for communication + short: m + long: method + possible_values: + - http + - keybase + default_value: http + takes_value: true + - owner_api: + about: Runs the wallet's local web API +# Turned off, for now +# - web: +# about: Runs the local web wallet which can be accessed through a browser + - send: + about: Builds a transaction to send coins and sends to the specified listener directly + args: + - amount: + help: Number of coins to send with optional fraction, e.g. 12.423 + index: 1 + - minimum_confirmations: + help: Minimum number of confirmations required for an output to be spendable + short: c + long: min_conf + default_value: "10" + takes_value: true + - selection_strategy: + help: Coin/Output selection strategy. + short: s + long: selection + possible_values: + - all + - smallest + default_value: all + takes_value: true + - estimate_selection_strategies: + help: Estimates all possible Coin/Output selection strategies. + short: e + long: estimate-selection + - change_outputs: + help: Number of change outputs to generate (mainly for testing) + short: o + long: change_outputs + default_value: "1" + takes_value: true + - method: + help: Method for sending this transaction + short: m + long: method + possible_values: + - http + - file + - self + - keybase + default_value: http + takes_value: true + - dest: + help: Send the transaction to the provided server (start with http://) or save as file. + short: d + long: dest + takes_value: true + - fluff: + help: Fluff the transaction (ignore Dandelion relay protocol) + short: f + long: fluff + - message: + help: Optional participant message to include + short: g + long: message + takes_value: true + - stored_tx: + help: If present, use the previously stored Unconfirmed transaction with given id + short: t + long: stored_tx + takes_value: true + - receive: + about: Processes a transaction file to accept a transfer from a sender + args: + - message: + help: Optional participant message to include + short: g + long: message + takes_value: true + - input: + help: Partial transaction to process, expects the sender's transaction file. + short: i + long: input + takes_value: true + - finalize: + about: Processes a receiver's transaction file to finalize a transfer. + args: + - input: + help: Partial transaction to process, expects the receiver's transaction file. + short: i + long: input + takes_value: true + - fluff: + help: Fluff the transaction (ignore Dandelion relay protocol) + short: f + long: fluff + - outputs: + about: Raw wallet output info (list of outputs) + - txs: + about: Display transaction information + args: + - id: + help: If specified, display transaction with given Id and all associated Inputs/Outputs + short: i + long: id + takes_value: true + - repost: + about: Reposts a stored, completed but unconfirmed transaction to the chain, or dumps it to a file + args: + - id: + help: Transaction ID containing the stored completed transaction + short: i + long: id + takes_value: true + - dumpfile: + help: File name to duMp the transaction to instead of posting + short: m + long: dumpfile + takes_value: true + - fluff: + help: Fluff the transaction (ignore Dandelion relay protocol) + short: f + long: fluff + - cancel: + about: Cancels an previously created transaction, freeing previously locked outputs for use again + args: + - id: + help: The ID of the transaction to cancel + short: i + long: id + takes_value: true + - txid: + help: The TxID UUID of the transaction to cancel + short: t + long: txid + takes_value: true + - info: + about: Basic wallet contents summary + args: + - minimum_confirmations: + help: Minimum number of confirmations required for an output to be spendable + short: c + long: min_conf + default_value: "10" + takes_value: true + - init: + about: Initialize a new wallet seed file and database + args: + - here: + help: Create wallet files in the current directory instead of the default ~/.grin directory + short: h + long: here + takes_value: false + - short_wordlist: + help: Generate a 12-word recovery phrase/seed instead of default 24 + short: s + long: short_wordlist + takes_value: false + - recover: + help: Initialize new wallet using a recovery phrase + short: r + long: recover + takes_value: false + - recover: + about: Recover a wallet.seed file from a recovery phrase (default) or displays a recovery phrase for an existing seed file + args: + - display: + help: Display wallet recovery phrase + short: d + long: display + takes_value: false + - restore: + about: Restores a wallet contents from a seed file + - check: + about: Checks a wallet's outputs against a live node, repairing and restoring missing outputs if required diff --git a/src/bin/wallet/adapters/file.rs b/src/bin/wallet/adapters/file.rs new file mode 100644 index 0000000..12fe459 --- /dev/null +++ b/src/bin/wallet/adapters/file.rs @@ -0,0 +1,69 @@ +// Copyright 2018 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// File Output 'plugin' implementation +use std::fs::File; +use std::io::{Read, Write}; + +use crate::adapters::util::{deserialize_slate, serialize_slate}; +use crate::libwallet::slate::Slate; +use crate::libwallet::Error; +use crate::{WalletCommAdapter, WalletConfig}; +use std::collections::HashMap; + +#[derive(Clone)] +pub struct FileWalletCommAdapter {} + +impl FileWalletCommAdapter { + /// Create + pub fn new() -> Box { + Box::new(FileWalletCommAdapter {}) + } +} + +impl WalletCommAdapter for FileWalletCommAdapter { + fn supports_sync(&self) -> bool { + false + } + + fn send_tx_sync(&self, _dest: &str, _slate: &Slate) -> Result { + unimplemented!(); + } + + fn send_tx_async(&self, dest: &str, slate: &Slate) -> Result<(), Error> { + let mut pub_tx = File::create(dest)?; + let slate_string = serialize_slate(slate); + pub_tx.write_all(slate_string.as_bytes())?; + pub_tx.sync_all()?; + Ok(()) + } + + fn receive_tx_async(&self, params: &str) -> Result { + let mut pub_tx_f = File::open(params)?; + let mut content = String::new(); + pub_tx_f.read_to_string(&mut content)?; + Ok(deserialize_slate(&content)) + } + + fn listen( + &self, + _params: HashMap, + _config: WalletConfig, + _passphrase: &str, + _account: &str, + _node_api_secret: Option, + ) -> Result<(), Error> { + unimplemented!(); + } +} diff --git a/src/bin/wallet/adapters/http.rs b/src/bin/wallet/adapters/http.rs new file mode 100644 index 0000000..c8988af --- /dev/null +++ b/src/bin/wallet/adapters/http.rs @@ -0,0 +1,93 @@ +// Copyright 2018 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::adapters::util::get_versioned_slate; +use crate::api; +use crate::controller; +use crate::libwallet::slate::{Slate, VersionedSlate}; +use crate::libwallet::{Error, ErrorKind}; +use crate::{instantiate_wallet, HTTPNodeClient, WalletCommAdapter, WalletConfig}; +/// HTTP Wallet 'plugin' implementation +use failure::ResultExt; +use std::collections::HashMap; + +#[derive(Clone)] +pub struct HTTPWalletCommAdapter {} + +impl HTTPWalletCommAdapter { + /// Create + pub fn new() -> Box { + Box::new(HTTPWalletCommAdapter {}) + } +} + +impl WalletCommAdapter for HTTPWalletCommAdapter { + fn supports_sync(&self) -> bool { + true + } + + fn send_tx_sync(&self, dest: &str, slate: &Slate) -> Result { + if &dest[..4] != "http" { + let err_str = format!( + "dest formatted as {} but send -d expected stdout or http://IP:port", + dest + ); + error!("{}", err_str,); + Err(ErrorKind::Uri)? + } + let url = format!("{}/v1/wallet/foreign/receive_tx", dest); + debug!("Posting transaction slate to {}", url); + let slate = get_versioned_slate(slate); + let res: Result = api::client::post(url.as_str(), None, &slate); + match res { + Err(e) => { + let report = format!("Posting transaction slate (is recipient listening?): {}", e); + error!("{}", report); + Err(ErrorKind::ClientCallback(report).into()) + } + Ok(r) => Ok(r.into()), + } + } + + fn send_tx_async(&self, _dest: &str, _slate: &Slate) -> Result<(), Error> { + unimplemented!(); + } + + fn receive_tx_async(&self, _params: &str) -> Result { + unimplemented!(); + } + + fn listen( + &self, + params: HashMap, + config: WalletConfig, + passphrase: &str, + account: &str, + node_api_secret: Option, + ) -> Result<(), Error> { + let node_client = HTTPNodeClient::new(&config.check_node_api_http_addr, node_api_secret); + let wallet = instantiate_wallet(config.clone(), node_client, passphrase, account) + .context(ErrorKind::WalletSeedDecryption)?; + let listen_addr = params.get("api_listen_addr").unwrap(); + let tls_conf = match params.get("certificate") { + Some(s) => Some(api::TLSConfig::new( + s.to_owned(), + params.get("private_key").unwrap().to_owned(), + )), + None => None, + }; + controller::foreign_listener(wallet.clone(), &listen_addr, tls_conf)?; + Ok(()) + } +} diff --git a/src/bin/wallet/adapters/keybase.rs b/src/bin/wallet/adapters/keybase.rs new file mode 100644 index 0000000..f34e635 --- /dev/null +++ b/src/bin/wallet/adapters/keybase.rs @@ -0,0 +1,463 @@ +// Copyright 2018 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Keybase Wallet Plugin + +use crate::controller; +use crate::libwallet::slate::{Slate, VersionedSlate}; +use crate::libwallet::slate_versions::v0::SlateV0; +use crate::libwallet::{Error, ErrorKind}; +use crate::{instantiate_wallet, HTTPNodeClient, WalletCommAdapter, WalletConfig}; +use failure::ResultExt; +use serde::Serialize; +use serde_json::{from_str, json, to_string, Value}; +use std::collections::{HashMap, HashSet}; +use std::process::{Command, Stdio}; +use std::str::from_utf8; +use std::thread::sleep; +use std::time::{Duration, Instant}; + +const TTL: u16 = 60; // TODO: Pass this as a parameter +const LISTEN_SLEEP_DURATION: Duration = Duration::from_millis(5000); +const POLL_SLEEP_DURATION: Duration = Duration::from_millis(1000); + +// Which topic names to use for communication +const SLATE_NEW: &str = "grin_slate_new"; +const SLATE_SIGNED: &str = "grin_slate_signed"; + +#[derive(Clone)] +pub struct KeybaseWalletCommAdapter {} + +impl KeybaseWalletCommAdapter { + /// Check if keybase is installed and return an adapter object. + pub fn new() -> Box { + let mut proc = if cfg!(target_os = "windows") { + Command::new("where") + } else { + Command::new("which") + }; + proc.arg("keybase") + .stdout(Stdio::null()) + .status() + .expect("Keybase executable not found, make sure it is installed and in your PATH"); + + Box::new(KeybaseWalletCommAdapter {}) + } +} + +/// Send a json object to the keybase process. Type `keybase chat api --help` for a list of available methods. +fn api_send(payload: &str) -> Result { + let mut proc = Command::new("keybase"); + proc.args(&["chat", "api", "-m", &payload]); + let output = proc.output().expect("No output"); + if !output.status.success() { + error!( + "keybase api fail: {} {}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + Err(ErrorKind::GenericError("keybase api fail".to_owned()))? + } else { + let response: Value = + from_str(from_utf8(&output.stdout).expect("Bad output")).expect("Bad output"); + let err_msg = format!("{}", response["error"]["message"]); + if err_msg.len() > 0 && err_msg != "null" { + error!("api_send got error: {}", err_msg); + } + + Ok(response) + } +} + +/// Get keybase username +fn whoami() -> Result { + let mut proc = Command::new("keybase"); + proc.args(&["status", "-json"]); + let output = proc.output().expect("No output"); + if !output.status.success() { + error!( + "keybase api fail: {} {}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + Err(ErrorKind::GenericError("keybase api fail".to_owned()))? + } else { + let response: Value = + from_str(from_utf8(&output.stdout).expect("Bad output")).expect("Bad output"); + let err_msg = format!("{}", response["error"]["message"]); + if err_msg.len() > 0 && err_msg != "null" { + error!("status query got error: {}", err_msg); + } + + let username = response["Username"].as_str(); + if let Some(s) = username { + Ok(s.to_string()) + } else { + error!("keybase username query fail"); + Err(ErrorKind::GenericError( + "keybase username query fail".to_owned(), + ))? + } + } +} + +/// Get all unread messages from a specific channel/topic and mark as read. +fn read_from_channel(channel: &str, topic: &str) -> Result, Error> { + let payload = to_string(&json!({ + "method": "read", + "params": { + "options": { + "channel": { + "name": channel, "topic_type": "dev", "topic_name": topic + }, + "unread_only": true, "peek": false + }, + } + } + )) + .unwrap(); + + let response = api_send(&payload); + if let Ok(res) = response { + let mut unread: Vec = Vec::new(); + for msg in res["result"]["messages"] + .as_array() + .unwrap_or(&vec![json!({})]) + .iter() + { + if (msg["msg"]["content"]["type"] == "text") && (msg["msg"]["unread"] == true) { + let message = msg["msg"]["content"]["text"]["body"].as_str().unwrap_or(""); + unread.push(message.to_owned()); + } + } + Ok(unread) + } else { + Err(ErrorKind::GenericError("keybase api fail".to_owned()))? + } +} + +/// Get unread messages from all channels and mark as read. +fn get_unread(topic: &str) -> Result, Error> { + let payload = to_string(&json!({ + "method": "list", + "params": { + "options": { + "topic_type": "dev", + }, + } + })) + .unwrap(); + let response = api_send(&payload); + + if let Ok(res) = response { + let mut channels = HashSet::new(); + // Unfortunately the response does not contain the message body + // and a separate call is needed for each channel + for msg in res["result"]["conversations"] + .as_array() + .unwrap_or(&vec![json!({})]) + .iter() + { + if (msg["unread"] == true) && (msg["channel"]["topic_name"] == topic) { + let channel = msg["channel"]["name"].as_str().unwrap(); + channels.insert(channel.to_string()); + } + } + let mut unread: HashMap = HashMap::new(); + for channel in channels.iter() { + let messages = read_from_channel(channel, topic); + if messages.is_err() { + break; + } + for msg in messages.unwrap() { + unread.insert(msg, channel.to_string()); + } + } + Ok(unread) + } else { + Err(ErrorKind::GenericError("keybase api fail".to_owned()))? + } +} + +/// Send a message to a keybase channel that self-destructs after ttl seconds. +fn send(message: T, channel: &str, topic: &str, ttl: u16) -> bool { + let seconds = format!("{}s", ttl); + let serialized = to_string(&message).unwrap(); + let payload = to_string(&json!({ + "method": "send", + "params": { + "options": { + "channel": { + "name": channel, "topic_name": topic, "topic_type": "dev" + }, + "message": { + "body": serialized + }, + "exploding_lifetime": seconds + } + } + })) + .unwrap(); + let response = api_send(&payload); + if let Ok(res) = response { + match res["result"]["message"].as_str() { + Some("message sent") => { + debug!("Message sent to {}: {}", channel, serialized); + true + } + _ => false, + } + } else { + false + } +} + +/// Send a notify to self that self-destructs after ttl minutes. +fn notify(message: &str, channel: &str, ttl: u16) -> bool { + let minutes = format!("{}m", ttl); + let payload = to_string(&json!({ + "method": "send", + "params": { + "options": { + "channel": { + "name": channel + }, + "message": { + "body": message + }, + "exploding_lifetime": minutes + } + } + })) + .unwrap(); + let response = api_send(&payload); + if let Ok(res) = response { + match res["result"]["message"].as_str() { + Some("message sent") => true, + _ => false, + } + } else { + false + } +} + +/// Listen for a message from a specific channel with topic SLATE_SIGNED for nseconds and return the first valid slate. +fn poll(nseconds: u64, channel: &str) -> Option { + let start = Instant::now(); + info!("Waiting for response message from @{}...", channel); + while start.elapsed().as_secs() < nseconds { + let unread = read_from_channel(channel, SLATE_SIGNED); + for msg in unread.unwrap().iter() { + let blob = from_str::(msg); + match blob { + Ok(slate) => { + let slate: Slate = slate.into(); + info!( + "keybase response message received from @{}, tx uuid: {}", + channel, slate.id, + ); + return Some(slate); + } + Err(_) => (), + } + } + sleep(POLL_SLEEP_DURATION); + } + error!( + "No response from @{} in {} seconds. Grin send failed!", + channel, nseconds + ); + None +} + +impl WalletCommAdapter for KeybaseWalletCommAdapter { + fn supports_sync(&self) -> bool { + true + } + + // Send a slate to a keybase username then wait for a response for TTL seconds. + fn send_tx_sync(&self, addr: &str, slate: &Slate) -> Result { + // Limit only one recipient + if addr.matches(",").count() > 0 { + error!("Only one recipient is supported!"); + return Err(ErrorKind::GenericError("Tx rejected".to_owned()))?; + } + + let id = slate.id; + + // Send original slate to recipient with the SLATE_NEW topic + match send(&slate, addr, SLATE_NEW, TTL) { + true => (), + false => { + return Err(ErrorKind::ClientCallback( + "Posting transaction slate".to_owned(), + ))? + } + } + info!("tx request has been sent to @{}, tx uuid: {}", addr, id); + // Wait for response from recipient with SLATE_SIGNED topic + match poll(TTL as u64, addr) { + Some(slate) => return Ok(slate), + None => { + return Err(ErrorKind::ClientCallback( + "Receiving reply from recipient".to_owned(), + ))? + } + } + } + + /// Send a transaction asynchronously (result will be returned via the listener) + fn send_tx_async(&self, _addr: &str, _slate: &Slate) -> Result<(), Error> { + unimplemented!(); + } + + /// Receive a transaction async. (Actually just read it from wherever and return the slate) + fn receive_tx_async(&self, _params: &str) -> Result { + unimplemented!(); + } + + /// Start a listener, passing received messages to the wallet api directly + #[allow(unreachable_code)] + fn listen( + &self, + _params: HashMap, + config: WalletConfig, + passphrase: &str, + account: &str, + node_api_secret: Option, + ) -> Result<(), Error> { + let node_client = HTTPNodeClient::new(&config.check_node_api_http_addr, node_api_secret); + let wallet = instantiate_wallet(config.clone(), node_client, passphrase, account) + .context(ErrorKind::WalletSeedDecryption)?; + + info!("Listening for transactions on keybase ..."); + loop { + // listen for messages from all channels with topic SLATE_NEW + let unread = get_unread(SLATE_NEW); + if unread.is_err() { + error!("Listening exited for some keybase api failure"); + break; + } + for (msg, channel) in &unread.unwrap() { + let blob = from_str::(msg); + match blob { + Ok(message) => { + let mut slate: Slate = message.clone().into(); + let tx_uuid = slate.id; + + // Reject multiple recipients channel for safety + { + if channel.matches(",").count() > 1 { + error!( + "Incoming tx initiated on channel \"{}\" is rejected, multiple recipients channel! amount: {}(g), tx uuid: {}", + channel, + slate.amount as f64 / 1000000000.0, + tx_uuid, + ); + continue; + } + } + + info!( + "tx initiated on channel \"{}\", to send you {}(g). tx uuid: {}", + channel, + slate.amount as f64 / 1000000000.0, + tx_uuid, + ); + match controller::foreign_single_use(wallet.clone(), |api| { + if let Err(e) = api.verify_slate_messages(&slate) { + error!("Error validating participant messages: {}", e); + return Err(e); + } + api.receive_tx(&mut slate, None, None)?; + Ok(()) + }) { + // Reply to the same channel with topic SLATE_SIGNED + Ok(_) => { + let success = match message { + // Send the same version of slate that was sent to us + VersionedSlate::V0(_) => { + send(SlateV0::from(slate), channel, SLATE_SIGNED, TTL) + } + VersionedSlate::V1(_) => { + send(slate, channel, SLATE_SIGNED, TTL) + } + }; + + if success { + notify_on_receive( + config.keybase_notify_ttl.unwrap_or(1440), + channel.to_string(), + tx_uuid.to_string(), + ); + debug!("Returned slate to @{} via keybase", channel); + } else { + error!("Failed to return slate to @{} via keybase. Incoming tx failed", channel); + } + } + + Err(e) => { + error!( + "Error on receiving tx via keybase: {}. Incoming tx failed", + e + ); + } + } + } + Err(_) => debug!("Failed to deserialize keybase message: {}", msg), + } + } + sleep(LISTEN_SLEEP_DURATION); + } + Ok(()) + } +} + +/// Notify in keybase on receiving a transaction +fn notify_on_receive(keybase_notify_ttl: u16, channel: String, tx_uuid: String) { + if keybase_notify_ttl > 0 { + let my_username = whoami(); + if let Ok(username) = my_username { + let split = channel.split(","); + let vec: Vec<&str> = split.collect(); + if vec.len() > 1 { + let receiver = username; + let sender = if vec[0] == receiver { + vec[1] + } else { + if vec[1] != receiver { + error!("keybase - channel doesn't include my username! channel: {}, username: {}", + channel, receiver + ); + } + vec[0] + }; + + let msg = format!( + "[grin wallet notice]: \ + you could have some coins received from @{}\n\ + Transaction Id: {}", + sender, tx_uuid + ); + notify(&msg, &receiver, keybase_notify_ttl); + info!( + "tx from @{} is done, please check on grin wallet. tx uuid: {}", + sender, tx_uuid, + ); + } + } else { + error!("keybase notification fail on whoami query"); + } + } +} diff --git a/src/bin/wallet/adapters/mod.rs b/src/bin/wallet/adapters/mod.rs new file mode 100644 index 0000000..d1c53ef --- /dev/null +++ b/src/bin/wallet/adapters/mod.rs @@ -0,0 +1,57 @@ +// Copyright 2018 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +mod file; +mod http; +mod keybase; +mod null; +pub mod util; + +pub use self::file::FileWalletCommAdapter; +pub use self::http::HTTPWalletCommAdapter; +pub use self::keybase::KeybaseWalletCommAdapter; +pub use self::null::NullWalletCommAdapter; + +use crate::libwallet::slate::Slate; +use crate::libwallet::Error; +use crate::WalletConfig; +use std::collections::HashMap; + +/// Encapsulate wallet to wallet communication functions +pub trait WalletCommAdapter { + /// Whether this adapter supports sync mode + fn supports_sync(&self) -> bool; + + /// Send a transaction slate to another listening wallet and return result + /// TODO: Probably need a slate wrapper type + fn send_tx_sync(&self, addr: &str, slate: &Slate) -> Result; + + /// Send a transaction asynchronously (result will be returned via the listener) + fn send_tx_async(&self, addr: &str, slate: &Slate) -> Result<(), Error>; + + /// Receive a transaction async. (Actually just read it from wherever and return the slate) + fn receive_tx_async(&self, params: &str) -> Result; + + /// Start a listener, passing received messages to the wallet api directly + /// Takes a wallet config for now to avoid needing all sorts of awkward + /// type parameters on this trait + fn listen( + &self, + params: HashMap, + config: WalletConfig, + passphrase: &str, + account: &str, + node_api_secret: Option, + ) -> Result<(), Error>; +} diff --git a/src/bin/wallet/adapters/null.rs b/src/bin/wallet/adapters/null.rs new file mode 100644 index 0000000..9ac7500 --- /dev/null +++ b/src/bin/wallet/adapters/null.rs @@ -0,0 +1,59 @@ +// Copyright 2018 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Null Output 'plugin' implementation +use crate::libwallet::slate::Slate; +use crate::libwallet::Error; +use crate::{WalletCommAdapter, WalletConfig}; + +use std::collections::HashMap; + +#[derive(Clone)] +pub struct NullWalletCommAdapter {} + +impl NullWalletCommAdapter { + /// Create + pub fn new() -> Box { + Box::new(NullWalletCommAdapter {}) + } +} + +impl WalletCommAdapter for NullWalletCommAdapter { + fn supports_sync(&self) -> bool { + true + } + + fn send_tx_sync(&self, _dest: &str, slate: &Slate) -> Result { + Ok(slate.clone()) + } + + fn send_tx_async(&self, _dest: &str, _slate: &Slate) -> Result<(), Error> { + Ok(()) + } + + fn receive_tx_async(&self, _params: &str) -> Result { + unimplemented!(); + } + + fn listen( + &self, + _params: HashMap, + _config: WalletConfig, + _passphrase: &str, + _account: &str, + _node_api_secret: Option, + ) -> Result<(), Error> { + unimplemented!(); + } +} diff --git a/src/bin/wallet/adapters/util.rs b/src/bin/wallet/adapters/util.rs new file mode 100644 index 0000000..86ac7f0 --- /dev/null +++ b/src/bin/wallet/adapters/util.rs @@ -0,0 +1,23 @@ +use crate::libwallet::slate::{Slate, VersionedSlate}; +use crate::libwallet::slate_versions::v0::SlateV0; +use crate::libwallet::ErrorKind; +use serde_json as json; + +pub fn get_versioned_slate(slate: &Slate) -> VersionedSlate { + let slate = slate.clone(); + match slate.version { + 0 => VersionedSlate::V0(SlateV0::from(slate)), + _ => VersionedSlate::V1(slate), + } +} + +pub fn serialize_slate(slate: &Slate) -> String { + json::to_string(&get_versioned_slate(slate)).unwrap() +} + +pub fn deserialize_slate(raw_slate: &str) -> Slate { + let versioned_slate: VersionedSlate = json::from_str(&raw_slate) + .map_err(|err| ErrorKind::Format(err.to_string())) + .unwrap(); + versioned_slate.into() +} diff --git a/src/bin/wallet/command.rs b/src/bin/wallet/command.rs new file mode 100644 index 0000000..593a042 --- /dev/null +++ b/src/bin/wallet/command.rs @@ -0,0 +1,553 @@ +// Copyright 2018 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::util::{Mutex, ZeroingString}; +use std::collections::HashMap; +/// Grin wallet command-line function implementations +use std::fs::File; +use std::io::Write; +use std::sync::Arc; +use std::thread; +use std::time::Duration; + +use serde_json as json; +use uuid::Uuid; + +use crate::api::TLSConfig; +use crate::core::core; +use crate::keychain; + +use crate::error::{Error, ErrorKind}; +use crate::{controller, display, HTTPNodeClient, WalletConfig, WalletInst, WalletSeed}; +use crate::{ + FileWalletCommAdapter, HTTPWalletCommAdapter, KeybaseWalletCommAdapter, LMDBBackend, + NodeClient, NullWalletCommAdapter, +}; + +/// Arguments common to all wallet commands +#[derive(Clone)] +pub struct GlobalArgs { + pub account: String, + pub node_api_secret: Option, + pub show_spent: bool, + pub password: Option, + pub tls_conf: Option, +} + +/// Arguments for init command +pub struct InitArgs { + /// BIP39 recovery phrase length + pub list_length: usize, + pub password: ZeroingString, + pub config: WalletConfig, + pub recovery_phrase: Option, + pub restore: bool, +} + +pub fn init(g_args: &GlobalArgs, args: InitArgs) -> Result<(), Error> { + WalletSeed::init_file( + &args.config, + args.list_length, + args.recovery_phrase, + &args.password, + )?; + info!("Wallet seed file created"); + let client_n = HTTPNodeClient::new( + &args.config.check_node_api_http_addr, + g_args.node_api_secret.clone(), + ); + let _: LMDBBackend = + LMDBBackend::new(args.config.clone(), &args.password, client_n)?; + info!("Wallet database backend created"); + Ok(()) +} + +/// Argument for recover +pub struct RecoverArgs { + pub recovery_phrase: Option, + pub passphrase: ZeroingString, +} + +/// Check whether seed file exists +pub fn wallet_seed_exists(config: &WalletConfig) -> Result<(), Error> { + let res = WalletSeed::seed_file_exists(&config)?; + Ok(res) +} + +pub fn recover(config: &WalletConfig, args: RecoverArgs) -> Result<(), Error> { + if args.recovery_phrase.is_none() { + let res = WalletSeed::from_file(config, &args.passphrase); + if let Err(e) = res { + error!("Error loading wallet seed (check password): {}", e); + return Err(e); + } + let _ = res.unwrap().show_recovery_phrase(); + } else { + let res = WalletSeed::recover_from_phrase( + &config, + &args.recovery_phrase.as_ref().unwrap(), + &args.passphrase, + ); + if let Err(e) = res { + error!("Error recovering seed - {}", e); + return Err(e); + } + } + Ok(()) +} + +/// Arguments for listen command +pub struct ListenArgs { + pub method: String, +} + +pub fn listen(config: &WalletConfig, args: &ListenArgs, g_args: &GlobalArgs) -> Result<(), Error> { + let mut params = HashMap::new(); + params.insert("api_listen_addr".to_owned(), config.api_listen_addr()); + if let Some(t) = g_args.tls_conf.as_ref() { + params.insert("certificate".to_owned(), t.certificate.clone()); + params.insert("private_key".to_owned(), t.private_key.clone()); + } + let adapter = match args.method.as_str() { + "http" => HTTPWalletCommAdapter::new(), + "keybase" => KeybaseWalletCommAdapter::new(), + _ => NullWalletCommAdapter::new(), + }; + + let res = adapter.listen( + params, + config.clone(), + &g_args.password.clone().unwrap(), + &g_args.account, + g_args.node_api_secret.clone(), + ); + if let Err(e) = res { + return Err(ErrorKind::LibWallet(e.kind(), e.cause_string()).into()); + } + Ok(()) +} + +pub fn owner_api( + wallet: Arc>>, + config: &WalletConfig, + g_args: &GlobalArgs, +) -> Result<(), Error> { + let res = controller::owner_listener( + wallet, + config.owner_api_listen_addr().as_str(), + g_args.node_api_secret.clone(), + g_args.tls_conf.clone(), + config.owner_api_include_foreign.clone(), + ); + if let Err(e) = res { + return Err(ErrorKind::LibWallet(e.kind(), e.cause_string()).into()); + } + Ok(()) +} + +/// Arguments for account command +pub struct AccountArgs { + pub create: Option, +} + +pub fn account( + wallet: Arc>>, + args: AccountArgs, +) -> Result<(), Error> { + if args.create.is_none() { + let res = controller::owner_single_use(wallet, |api| { + let acct_mappings = api.accounts()?; + // give logging thread a moment to catch up + thread::sleep(Duration::from_millis(200)); + display::accounts(acct_mappings); + Ok(()) + }); + if let Err(e) = res { + error!("Error listing accounts: {}", e); + return Err(ErrorKind::LibWallet(e.kind(), e.cause_string()).into()); + } + } else { + let label = args.create.unwrap(); + let res = controller::owner_single_use(wallet, |api| { + api.create_account_path(&label)?; + thread::sleep(Duration::from_millis(200)); + info!("Account: '{}' Created!", label); + Ok(()) + }); + if let Err(e) = res { + thread::sleep(Duration::from_millis(200)); + error!("Error creating account '{}': {}", label, e); + return Err(ErrorKind::LibWallet(e.kind(), e.cause_string()).into()); + } + } + Ok(()) +} + +/// Arguments for the send command +pub struct SendArgs { + pub amount: u64, + pub message: Option, + pub minimum_confirmations: u64, + pub selection_strategy: String, + pub estimate_selection_strategies: bool, + pub method: String, + pub dest: String, + pub change_outputs: usize, + pub fluff: bool, + pub max_outputs: usize, +} + +pub fn send( + wallet: Arc>>, + args: SendArgs, + dark_scheme: bool, +) -> Result<(), Error> { + controller::owner_single_use(wallet.clone(), |api| { + if args.estimate_selection_strategies { + let strategies = vec!["smallest", "all"] + .into_iter() + .map(|strategy| { + let (total, fee) = api + .estimate_initiate_tx( + None, + args.amount, + args.minimum_confirmations, + args.max_outputs, + args.change_outputs, + strategy == "all", + ) + .unwrap(); + (strategy, total, fee) + }) + .collect(); + display::estimate(args.amount, strategies, dark_scheme); + } else { + let result = api.initiate_tx( + None, + args.amount, + args.minimum_confirmations, + args.max_outputs, + args.change_outputs, + args.selection_strategy == "all", + args.message.clone(), + ); + let (mut slate, lock_fn) = match result { + Ok(s) => { + info!( + "Tx created: {} grin to {} (strategy '{}')", + core::amount_to_hr_string(args.amount, false), + args.dest, + args.selection_strategy, + ); + s + } + Err(e) => { + info!("Tx not created: {}", e); + return Err(e); + } + }; + let adapter = match args.method.as_str() { + "http" => HTTPWalletCommAdapter::new(), + "file" => FileWalletCommAdapter::new(), + "keybase" => KeybaseWalletCommAdapter::new(), + "self" => NullWalletCommAdapter::new(), + _ => NullWalletCommAdapter::new(), + }; + if adapter.supports_sync() { + slate = adapter.send_tx_sync(&args.dest, &slate)?; + api.tx_lock_outputs(&slate, lock_fn)?; + if args.method == "self" { + controller::foreign_single_use(wallet, |api| { + api.receive_tx(&mut slate, Some(&args.dest), None)?; + Ok(()) + })?; + } + if let Err(e) = api.verify_slate_messages(&slate) { + error!("Error validating participant messages: {}", e); + return Err(e); + } + api.finalize_tx(&mut slate)?; + } else { + adapter.send_tx_async(&args.dest, &slate)?; + api.tx_lock_outputs(&slate, lock_fn)?; + } + if adapter.supports_sync() { + let result = api.post_tx(&slate.tx, args.fluff); + match result { + Ok(_) => { + info!("Tx sent ok",); + return Ok(()); + } + Err(e) => { + error!("Tx sent fail: {}", e); + return Err(e); + } + } + } + } + Ok(()) + })?; + Ok(()) +} + +/// Receive command argument +pub struct ReceiveArgs { + pub input: String, + pub message: Option, +} + +pub fn receive( + wallet: Arc>>, + g_args: &GlobalArgs, + args: ReceiveArgs, +) -> Result<(), Error> { + let adapter = FileWalletCommAdapter::new(); + let mut slate = adapter.receive_tx_async(&args.input)?; + controller::foreign_single_use(wallet, |api| { + if let Err(e) = api.verify_slate_messages(&slate) { + error!("Error validating participant messages: {}", e); + return Err(e); + } + api.receive_tx(&mut slate, Some(&g_args.account), args.message.clone())?; + Ok(()) + })?; + let send_tx = format!("{}.response", args.input); + adapter.send_tx_async(&send_tx, &slate)?; + info!( + "Response file {}.response generated, sending it back to the transaction originator.", + args.input + ); + Ok(()) +} + +/// Finalize command args +pub struct FinalizeArgs { + pub input: String, + pub fluff: bool, +} + +pub fn finalize( + wallet: Arc>>, + args: FinalizeArgs, +) -> Result<(), Error> { + let adapter = FileWalletCommAdapter::new(); + let mut slate = adapter.receive_tx_async(&args.input)?; + controller::owner_single_use(wallet.clone(), |api| { + if let Err(e) = api.verify_slate_messages(&slate) { + error!("Error validating participant messages: {}", e); + return Err(e); + } + let _ = api.finalize_tx(&mut slate).expect("Finalize failed"); + + let result = api.post_tx(&slate.tx, args.fluff); + match result { + Ok(_) => { + info!("Transaction sent successfully, check the wallet again for confirmation."); + Ok(()) + } + Err(e) => { + error!("Tx not sent: {}", e); + Err(e) + } + } + })?; + Ok(()) +} + +/// Info command args +pub struct InfoArgs { + pub minimum_confirmations: u64, +} + +pub fn info( + wallet: Arc>>, + g_args: &GlobalArgs, + args: InfoArgs, + dark_scheme: bool, +) -> Result<(), Error> { + controller::owner_single_use(wallet.clone(), |api| { + let (validated, wallet_info) = + api.retrieve_summary_info(true, args.minimum_confirmations)?; + display::info(&g_args.account, &wallet_info, validated, dark_scheme); + Ok(()) + })?; + Ok(()) +} + +pub fn outputs( + wallet: Arc>>, + g_args: &GlobalArgs, + dark_scheme: bool, +) -> Result<(), Error> { + controller::owner_single_use(wallet.clone(), |api| { + let (height, _) = api.node_height()?; + let (validated, outputs) = api.retrieve_outputs(g_args.show_spent, true, None)?; + display::outputs(&g_args.account, height, validated, outputs, dark_scheme)?; + Ok(()) + })?; + Ok(()) +} + +/// Txs command args +pub struct TxsArgs { + pub id: Option, +} + +pub fn txs( + wallet: Arc>>, + g_args: &GlobalArgs, + args: TxsArgs, + dark_scheme: bool, +) -> Result<(), Error> { + controller::owner_single_use(wallet.clone(), |api| { + let (height, _) = api.node_height()?; + let (validated, txs) = api.retrieve_txs(true, args.id, None)?; + let include_status = !args.id.is_some(); + display::txs( + &g_args.account, + height, + validated, + &txs, + include_status, + dark_scheme, + )?; + // if given a particular transaction id, also get and display associated + // inputs/outputs and messages + if args.id.is_some() { + let (_, outputs) = api.retrieve_outputs(true, false, args.id)?; + display::outputs(&g_args.account, height, validated, outputs, dark_scheme)?; + // should only be one here, but just in case + for tx in txs { + display::tx_messages(&tx, dark_scheme)?; + } + }; + Ok(()) + })?; + Ok(()) +} + +/// Repost +pub struct RepostArgs { + pub id: u32, + pub dump_file: Option, + pub fluff: bool, +} + +pub fn repost( + wallet: Arc>>, + args: RepostArgs, +) -> Result<(), Error> { + controller::owner_single_use(wallet.clone(), |api| { + let (_, txs) = api.retrieve_txs(true, Some(args.id), None)?; + let stored_tx = api.get_stored_tx(&txs[0])?; + if stored_tx.is_none() { + error!( + "Transaction with id {} does not have transaction data. Not reposting.", + args.id + ); + return Ok(()); + } + match args.dump_file { + None => { + if txs[0].confirmed { + error!( + "Transaction with id {} is confirmed. Not reposting.", + args.id + ); + return Ok(()); + } + api.post_tx(&stored_tx.unwrap(), args.fluff)?; + info!("Reposted transaction at {}", args.id); + return Ok(()); + } + Some(f) => { + let mut tx_file = File::create(f.clone())?; + tx_file.write_all(json::to_string(&stored_tx).unwrap().as_bytes())?; + tx_file.sync_all()?; + info!("Dumped transaction data for tx {} to {}", args.id, f); + return Ok(()); + } + } + })?; + Ok(()) +} + +/// Cancel +pub struct CancelArgs { + pub tx_id: Option, + pub tx_slate_id: Option, + pub tx_id_string: String, +} + +pub fn cancel( + wallet: Arc>>, + args: CancelArgs, +) -> Result<(), Error> { + controller::owner_single_use(wallet.clone(), |api| { + let result = api.cancel_tx(args.tx_id, args.tx_slate_id); + match result { + Ok(_) => { + info!("Transaction {} Cancelled", args.tx_id_string); + Ok(()) + } + Err(e) => { + error!("TX Cancellation failed: {}", e); + Err(e) + } + } + })?; + Ok(()) +} + +pub fn restore( + wallet: Arc>>, +) -> Result<(), Error> { + controller::owner_single_use(wallet.clone(), |api| { + let result = api.restore(); + match result { + Ok(_) => { + warn!("Wallet restore complete",); + Ok(()) + } + Err(e) => { + error!("Wallet restore failed: {}", e); + error!("Backtrace: {}", e.backtrace().unwrap()); + Err(e) + } + } + })?; + Ok(()) +} + +pub fn check_repair( + wallet: Arc>>, +) -> Result<(), Error> { + controller::owner_single_use(wallet.clone(), |api| { + warn!("Starting wallet check...",); + warn!("Updating all wallet outputs, please wait ...",); + let result = api.check_repair(); + match result { + Ok(_) => { + warn!("Wallet check complete",); + Ok(()) + } + Err(e) => { + error!("Wallet check failed: {}", e); + error!("Backtrace: {}", e.backtrace().unwrap()); + Err(e) + } + } + })?; + Ok(()) +} diff --git a/src/bin/wallet/controller.rs b/src/bin/wallet/controller.rs new file mode 100644 index 0000000..18e6493 --- /dev/null +++ b/src/bin/wallet/controller.rs @@ -0,0 +1,822 @@ +// Copyright 2018 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Controller for wallet.. instantiates and handles listeners (or single-run +//! invocations) as needed. +//! Still experimental +use crate::adapters::util::get_versioned_slate; +use crate::adapters::{FileWalletCommAdapter, HTTPWalletCommAdapter, KeybaseWalletCommAdapter}; +use crate::api::{ApiServer, BasicAuthMiddleware, Handler, ResponseFuture, Router, TLSConfig}; +use crate::core::core; +use crate::core::core::Transaction; +use crate::keychain::Keychain; +use crate::libwallet::api::{APIForeign, APIOwner}; +use crate::libwallet::slate::{Slate, VersionedSlate}; +use crate::libwallet::types::{ + CbData, NodeClient, OutputData, SendTXArgs, TxLogEntry, WalletBackend, WalletInfo, +}; +use crate::libwallet::{Error, ErrorKind}; +use crate::util::secp::pedersen; +use crate::util::to_base64; +use crate::util::Mutex; +use failure::ResultExt; +use futures::future::{err, ok}; +use futures::{Future, Stream}; +use hyper::{Body, Request, Response, StatusCode}; +use serde::{Deserialize, Serialize}; +use serde_json; +use std::collections::HashMap; +use std::marker::PhantomData; +use std::net::SocketAddr; +use std::sync::Arc; +use url::form_urlencoded; +use uuid::Uuid; + +/// Instantiate wallet Owner API for a single-use (command line) call +/// Return a function containing a loaded API context to call +pub fn owner_single_use(wallet: Arc>, f: F) -> Result<(), Error> +where + T: WalletBackend, + F: FnOnce(&mut APIOwner) -> Result<(), Error>, + C: NodeClient, + K: Keychain, +{ + f(&mut APIOwner::new(wallet.clone()))?; + Ok(()) +} + +/// Instantiate wallet Foreign API for a single-use (command line) call +/// Return a function containing a loaded API context to call +pub fn foreign_single_use(wallet: Arc>, f: F) -> Result<(), Error> +where + T: WalletBackend, + F: FnOnce(&mut APIForeign) -> Result<(), Error>, + C: NodeClient, + K: Keychain, +{ + f(&mut APIForeign::new(wallet.clone()))?; + Ok(()) +} + +/// Listener version, providing same API but listening for requests on a +/// port and wrapping the calls +pub fn owner_listener( + wallet: Arc>, + addr: &str, + api_secret: Option, + tls_config: Option, + owner_api_include_foreign: Option, +) -> Result<(), Error> +where + T: WalletBackend + Send + Sync + 'static, + OwnerAPIHandler: Handler, + C: NodeClient + 'static, + K: Keychain + 'static, +{ + let api_handler = OwnerAPIHandler::new(wallet.clone()); + + let mut router = Router::new(); + if api_secret.is_some() { + let api_basic_auth = + "Basic ".to_string() + &to_base64(&("grin:".to_string() + &api_secret.unwrap())); + let basic_realm = "Basic realm=GrinOwnerAPI".to_string(); + let basic_auth_middleware = Arc::new(BasicAuthMiddleware::new(api_basic_auth, basic_realm)); + router.add_middleware(basic_auth_middleware); + } + router + .add_route("/v1/wallet/owner/**", Arc::new(api_handler)) + .map_err(|_| ErrorKind::GenericError("Router failed to add route".to_string()))?; + + // If so configured, add the foreign API to the same port + if owner_api_include_foreign.unwrap_or(false) { + info!("Starting HTTP Foreign API on Owner server at {}.", addr); + let foreign_api_handler = ForeignAPIHandler::new(wallet.clone()); + router + .add_route("/v1/wallet/foreign/**", Arc::new(foreign_api_handler)) + .map_err(|_| ErrorKind::GenericError("Router failed to add route".to_string()))?; + } + + let mut apis = ApiServer::new(); + info!("Starting HTTP Owner API server at {}.", addr); + let socket_addr: SocketAddr = addr.parse().expect("unable to parse socket address"); + let api_thread = + apis.start(socket_addr, router, tls_config) + .context(ErrorKind::GenericError( + "API thread failed to start".to_string(), + ))?; + api_thread + .join() + .map_err(|e| ErrorKind::GenericError(format!("API thread panicked :{:?}", e)).into()) +} + +/// Listener version, providing same API but listening for requests on a +/// port and wrapping the calls +pub fn foreign_listener( + wallet: Arc>, + addr: &str, + tls_config: Option, +) -> Result<(), Error> +where + T: WalletBackend + Send + Sync + 'static, + C: NodeClient + 'static, + K: Keychain + 'static, +{ + let api_handler = ForeignAPIHandler::new(wallet); + + let mut router = Router::new(); + router + .add_route("/v1/wallet/foreign/**", Arc::new(api_handler)) + .map_err(|_| ErrorKind::GenericError("Router failed to add route".to_string()))?; + + let mut apis = ApiServer::new(); + warn!("Starting HTTP Foreign listener API server at {}.", addr); + let socket_addr: SocketAddr = addr.parse().expect("unable to parse socket address"); + let api_thread = + apis.start(socket_addr, router, tls_config) + .context(ErrorKind::GenericError( + "API thread failed to start".to_string(), + ))?; + warn!("HTTP Foreign listener started."); + + api_thread + .join() + .map_err(|e| ErrorKind::GenericError(format!("API thread panicked :{:?}", e)).into()) +} + +type WalletResponseFuture = Box, Error = Error> + Send>; + +/// API Handler/Wrapper for owner functions +pub struct OwnerAPIHandler +where + T: WalletBackend + Send + Sync + 'static, + C: NodeClient + 'static, + K: Keychain + 'static, +{ + /// Wallet instance + pub wallet: Arc>, + phantom: PhantomData, + phantom_c: PhantomData, +} + +impl OwnerAPIHandler +where + T: WalletBackend + Send + Sync + 'static, + C: NodeClient + 'static, + K: Keychain + 'static, +{ + /// Create a new owner API handler for GET methods + pub fn new(wallet: Arc>) -> OwnerAPIHandler { + OwnerAPIHandler { + wallet, + phantom: PhantomData, + phantom_c: PhantomData, + } + } + + pub fn retrieve_outputs( + &self, + req: &Request, + api: APIOwner, + ) -> Result<(bool, Vec<(OutputData, pedersen::Commitment)>), Error> { + let mut update_from_node = false; + let mut id = None; + let mut show_spent = false; + let params = parse_params(req); + + if let Some(_) = params.get("refresh") { + update_from_node = true; + } + if let Some(_) = params.get("show_spent") { + show_spent = true; + } + if let Some(ids) = params.get("tx_id") { + if let Some(x) = ids.first() { + id = Some(x.parse().unwrap()); + } + } + api.retrieve_outputs(show_spent, update_from_node, id) + } + + pub fn retrieve_txs( + &self, + req: &Request, + api: APIOwner, + ) -> Result<(bool, Vec), Error> { + let mut tx_id = None; + let mut tx_slate_id = None; + let mut update_from_node = false; + + let params = parse_params(req); + + if let Some(_) = params.get("refresh") { + update_from_node = true; + } + if let Some(ids) = params.get("id") { + if let Some(x) = ids.first() { + tx_id = Some(x.parse().unwrap()); + } + } + if let Some(tx_slate_ids) = params.get("tx_id") { + if let Some(x) = tx_slate_ids.first() { + tx_slate_id = Some(x.parse().unwrap()); + } + } + api.retrieve_txs(update_from_node, tx_id, tx_slate_id) + } + + pub fn retrieve_stored_tx( + &self, + req: &Request, + api: APIOwner, + ) -> Result<(bool, Option), Error> { + let params = parse_params(req); + if let Some(id_string) = params.get("id") { + match id_string[0].parse() { + Ok(id) => match api.retrieve_txs(true, Some(id), None) { + Ok((_, txs)) => { + let stored_tx = api.get_stored_tx(&txs[0])?; + Ok((txs[0].confirmed, stored_tx)) + } + Err(e) => { + error!("retrieve_stored_tx: failed with error: {}", e); + Err(e) + } + }, + Err(e) => { + error!("retrieve_stored_tx: could not parse id: {}", e); + Err(ErrorKind::TransactionDumpError( + "retrieve_stored_tx: cannot dump transaction. Could not parse id in request.", + ).into()) + } + } + } else { + Err(ErrorKind::TransactionDumpError( + "retrieve_stored_tx: Cannot retrieve transaction. Missing id param in request.", + ) + .into()) + } + } + + pub fn retrieve_summary_info( + &self, + req: &Request, + mut api: APIOwner, + ) -> Result<(bool, WalletInfo), Error> { + let mut minimum_confirmations = 1; // TODO - default needed here + let params = parse_params(req); + let update_from_node = params.get("refresh").is_some(); + + if let Some(confs) = params.get("minimum_confirmations") { + if let Some(x) = confs.first() { + minimum_confirmations = x.parse().unwrap(); + } + } + + api.retrieve_summary_info(update_from_node, minimum_confirmations) + } + + pub fn node_height( + &self, + _req: &Request, + mut api: APIOwner, + ) -> Result<(u64, bool), Error> { + api.node_height() + } + + fn handle_get_request(&self, req: &Request) -> Result, Error> { + let api = APIOwner::new(self.wallet.clone()); + + Ok( + match req + .uri() + .path() + .trim_right_matches("/") + .rsplit("/") + .next() + .unwrap() + { + "retrieve_outputs" => json_response(&self.retrieve_outputs(req, api)?), + "retrieve_summary_info" => json_response(&self.retrieve_summary_info(req, api)?), + "node_height" => json_response(&self.node_height(req, api)?), + "retrieve_txs" => json_response(&self.retrieve_txs(req, api)?), + "retrieve_stored_tx" => json_response(&self.retrieve_stored_tx(req, api)?), + _ => response(StatusCode::BAD_REQUEST, ""), + }, + ) + } + + pub fn issue_send_tx( + &self, + req: Request, + mut api: APIOwner, + ) -> Box + Send> { + Box::new(parse_body(req).and_then(move |args: SendTXArgs| { + let result = api.initiate_tx( + None, + args.amount, + args.minimum_confirmations, + args.max_outputs, + args.num_change_outputs, + args.selection_strategy_is_use_all, + args.message, + ); + let (mut slate, lock_fn) = match result { + Ok(s) => { + info!( + "Tx created: {} grin to {} (strategy '{}')", + core::amount_to_hr_string(args.amount, false), + &args.dest, + args.selection_strategy_is_use_all, + ); + s + } + Err(e) => { + error!("Tx not created: {}", e); + match e.kind() { + // user errors, don't backtrace + ErrorKind::NotEnoughFunds { .. } => {} + ErrorKind::Fee { .. } => {} + _ => { + // otherwise give full dump + error!("Backtrace: {}", e.backtrace().unwrap()); + } + }; + return Err(e); + } + }; + match args.method.as_ref() { + "http" => slate = HTTPWalletCommAdapter::new().send_tx_sync(&args.dest, &slate)?, + "file" => { + FileWalletCommAdapter::new().send_tx_async(&args.dest, &slate)?; + } + "keybase" => { + //TODO: in case of keybase, the response might take 60s and leave the service hanging + slate = KeybaseWalletCommAdapter::new().send_tx_sync(&args.dest, &slate)?; + } + _ => { + error!("unsupported payment method: {}", args.method); + return Err(ErrorKind::ClientCallback( + "unsupported payment method".to_owned(), + ))?; + } + } + api.tx_lock_outputs(&slate, lock_fn)?; + if args.method != "file" { + api.finalize_tx(&mut slate)?; + } + Ok(slate) + })) + } + + pub fn finalize_tx( + &self, + req: Request, + mut api: APIOwner, + ) -> Box + Send> { + Box::new( + parse_body(req).and_then(move |mut slate| match api.finalize_tx(&mut slate) { + Ok(_) => ok(slate.clone()), + Err(e) => { + error!("finalize_tx: failed with error: {}", e); + err(e) + } + }), + ) + } + + pub fn cancel_tx( + &self, + req: Request, + mut api: APIOwner, + ) -> Box + Send> { + let params = parse_params(&req); + if let Some(id_string) = params.get("id") { + Box::new(match id_string[0].parse() { + Ok(id) => match api.cancel_tx(Some(id), None) { + Ok(_) => ok(()), + Err(e) => { + error!("cancel_tx: failed with error: {}", e); + err(e) + } + }, + Err(e) => { + error!("cancel_tx: could not parse id: {}", e); + err(ErrorKind::TransactionCancellationError( + "cancel_tx: cannot cancel transaction. Could not parse id in request.", + ) + .into()) + } + }) + } else if let Some(tx_id_string) = params.get("tx_id") { + Box::new(match tx_id_string[0].parse() { + Ok(tx_id) => match api.cancel_tx(None, Some(tx_id)) { + Ok(_) => ok(()), + Err(e) => { + error!("cancel_tx: failed with error: {}", e); + err(e) + } + }, + Err(e) => { + error!("cancel_tx: could not parse tx_id: {}", e); + err(ErrorKind::TransactionCancellationError( + "cancel_tx: cannot cancel transaction. Could not parse tx_id in request.", + ) + .into()) + } + }) + } else { + Box::new(err(ErrorKind::TransactionCancellationError( + "cancel_tx: Cannot cancel transaction. Missing id or tx_id param in request.", + ) + .into())) + } + } + + pub fn post_tx( + &self, + req: Request, + api: APIOwner, + ) -> Box + Send> { + let params = match req.uri().query() { + Some(query_string) => form_urlencoded::parse(query_string.as_bytes()) + .into_owned() + .fold(HashMap::new(), |mut hm, (k, v)| { + hm.entry(k).or_insert(vec![]).push(v); + hm + }), + None => HashMap::new(), + }; + let fluff = params.get("fluff").is_some(); + Box::new(parse_body(req).and_then( + move |slate: Slate| match api.post_tx(&slate.tx, fluff) { + Ok(_) => ok(()), + Err(e) => { + error!("post_tx: failed with error: {}", e); + err(e) + } + }, + )) + } + + pub fn repost( + &self, + req: Request, + api: APIOwner, + ) -> Box + Send> { + let params = parse_params(&req); + let mut id_int: Option = None; + let mut tx_uuid: Option = None; + + if let Some(id_string) = params.get("id") { + match id_string[0].parse() { + Ok(id) => id_int = Some(id), + Err(e) => { + error!("repost: could not parse id: {}", e); + return Box::new(err(ErrorKind::GenericError( + "repost: cannot repost transaction. Could not parse id in request." + .to_owned(), + ) + .into())); + } + } + } else if let Some(tx_id_string) = params.get("tx_id") { + match tx_id_string[0].parse() { + Ok(tx_id) => tx_uuid = Some(tx_id), + Err(e) => { + error!("repost: could not parse tx_id: {}", e); + return Box::new(err(ErrorKind::GenericError( + "repost: cannot repost transaction. Could not parse tx_id in request." + .to_owned(), + ) + .into())); + } + } + } else { + return Box::new(err(ErrorKind::GenericError( + "repost: Cannot repost transaction. Missing id or tx_id param in request." + .to_owned(), + ) + .into())); + } + + let res = api.retrieve_txs(true, id_int, tx_uuid); + if let Err(e) = res { + return Box::new(err(ErrorKind::GenericError(format!( + "repost: cannot repost transaction. retrieve_txs failed, err: {:?}", + e + )) + .into())); + } + let (_, txs) = res.unwrap(); + let res = api.get_stored_tx(&txs[0]); + if let Err(e) = res { + return Box::new(err(ErrorKind::GenericError(format!( + "repost: cannot repost transaction. get_stored_tx failed, err: {:?}", + e + )) + .into())); + } + let stored_tx = res.unwrap(); + if stored_tx.is_none() { + error!( + "Transaction with id {:?}/{:?} does not have transaction data. Not reposting.", + id_int, tx_uuid, + ); + return Box::new(err(ErrorKind::GenericError( + "repost: Cannot repost transaction. Missing id or tx_id param in request." + .to_owned(), + ) + .into())); + } + + let fluff = params.get("fluff").is_some(); + Box::new(match api.post_tx(&stored_tx.unwrap(), fluff) { + Ok(_) => ok(()), + Err(e) => { + error!("repost: failed with error: {}", e); + err(e) + } + }) + } + + fn handle_post_request(&self, req: Request) -> WalletResponseFuture { + let api = APIOwner::new(self.wallet.clone()); + match req + .uri() + .path() + .trim_right_matches("/") + .rsplit("/") + .next() + .unwrap() + { + "issue_send_tx" => Box::new( + self.issue_send_tx(req, api) + .and_then(|slate| ok(json_response_pretty(&slate))), + ), + "finalize_tx" => Box::new( + self.finalize_tx(req, api) + .and_then(|slate| ok(json_response_pretty(&slate))), + ), + "cancel_tx" => Box::new( + self.cancel_tx(req, api) + .and_then(|_| ok(response(StatusCode::OK, "{}"))), + ), + "post_tx" => Box::new( + self.post_tx(req, api) + .and_then(|_| ok(response(StatusCode::OK, "{}"))), + ), + "repost" => Box::new( + self.repost(req, api) + .and_then(|_| ok(response(StatusCode::OK, ""))), + ), + _ => Box::new(err(ErrorKind::GenericError( + "Unknown error handling post request".to_owned(), + ) + .into())), + } + } +} + +impl Handler for OwnerAPIHandler +where + T: WalletBackend + Send + Sync + 'static, + C: NodeClient + 'static, + K: Keychain + 'static, +{ + fn get(&self, req: Request) -> ResponseFuture { + match self.handle_get_request(&req) { + Ok(r) => Box::new(ok(r)), + Err(e) => { + error!("Request Error: {:?}", e); + Box::new(ok(create_error_response(e))) + } + } + } + + fn post(&self, req: Request) -> ResponseFuture { + Box::new( + self.handle_post_request(req) + .and_then(|r| ok(r)) + .or_else(|e| { + error!("Request Error: {:?}", e); + ok(create_error_response(e)) + }), + ) + } + + fn options(&self, _req: Request) -> ResponseFuture { + Box::new(ok(create_ok_response("{}"))) + } +} + +/// API Handler/Wrapper for foreign functions + +pub struct ForeignAPIHandler +where + T: WalletBackend + Send + Sync + 'static, + C: NodeClient + 'static, + K: Keychain + 'static, +{ + /// Wallet instance + pub wallet: Arc>, + phantom: PhantomData, + phantom_c: PhantomData, +} + +impl ForeignAPIHandler +where + T: WalletBackend + Send + Sync + 'static, + C: NodeClient + 'static, + K: Keychain + 'static, +{ + /// create a new api handler + pub fn new(wallet: Arc>) -> ForeignAPIHandler { + ForeignAPIHandler { + wallet, + phantom: PhantomData, + phantom_c: PhantomData, + } + } + + fn build_coinbase( + &self, + req: Request, + mut api: APIForeign, + ) -> Box + Send> { + Box::new(parse_body(req).and_then(move |block_fees| api.build_coinbase(&block_fees))) + } + + fn receive_tx( + &self, + req: Request, + mut api: APIForeign, + ) -> Box + Send> { + Box::new(parse_body(req).and_then( + //TODO: No way to insert a message from the params + move |slate: VersionedSlate| { + let mut slate: Slate = slate.into(); + if let Err(e) = api.verify_slate_messages(&slate) { + error!("Error validating participant messages: {}", e); + err(e) + } else { + match api.receive_tx(&mut slate, None, None) { + Ok(_) => ok(get_versioned_slate(&slate.clone())), + Err(e) => { + error!("receive_tx: failed with error: {}", e); + err(e) + } + } + } + }, + )) + } + + fn handle_request(&self, req: Request) -> WalletResponseFuture { + let api = *APIForeign::new(self.wallet.clone()); + match req + .uri() + .path() + .trim_right_matches("/") + .rsplit("/") + .next() + .unwrap() + { + "build_coinbase" => Box::new( + self.build_coinbase(req, api) + .and_then(|res| ok(json_response(&res))), + ), + "receive_tx" => Box::new( + self.receive_tx(req, api) + .and_then(|res| ok(json_response(&res))), + ), + _ => Box::new(ok(response(StatusCode::BAD_REQUEST, "unknown action"))), + } + } +} +impl Handler for ForeignAPIHandler +where + T: WalletBackend + Send + Sync + 'static, + C: NodeClient + Send + Sync + 'static, + K: Keychain + 'static, +{ + fn post(&self, req: Request) -> ResponseFuture { + Box::new(self.handle_request(req).and_then(|r| ok(r)).or_else(|e| { + error!("Request Error: {:?}", e); + ok(create_error_response(e)) + })) + } + + fn options(&self, _req: Request) -> ResponseFuture { + Box::new(ok(create_ok_response("{}"))) + } +} + +// Utility to serialize a struct into JSON and produce a sensible Response +// out of it. +fn json_response(s: &T) -> Response +where + T: Serialize, +{ + match serde_json::to_string(s) { + Ok(json) => response(StatusCode::OK, json), + Err(_) => response(StatusCode::INTERNAL_SERVER_ERROR, ""), + } +} + +// pretty-printed version of above +fn json_response_pretty(s: &T) -> Response +where + T: Serialize, +{ + match serde_json::to_string_pretty(s) { + Ok(json) => response(StatusCode::OK, json), + Err(_) => response(StatusCode::INTERNAL_SERVER_ERROR, ""), + } +} + +fn create_error_response(e: Error) -> Response { + Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .header("access-control-allow-origin", "*") + .header( + "access-control-allow-headers", + "Content-Type, Authorization", + ) + .body(format!("{}", e).into()) + .unwrap() +} + +fn create_ok_response(json: &str) -> Response { + Response::builder() + .status(StatusCode::OK) + .header("access-control-allow-origin", "*") + .header( + "access-control-allow-headers", + "Content-Type, Authorization", + ) + .header(hyper::header::CONTENT_TYPE, "application/json") + .body(json.to_string().into()) + .unwrap() +} + +/// Build a new hyper Response with the status code and body provided. +/// +/// Whenever the status code is `StatusCode::OK` the text parameter should be +/// valid JSON as the content type header will be set to `application/json' +fn response>(status: StatusCode, text: T) -> Response { + let mut builder = &mut Response::builder(); + + builder = builder + .status(status) + .header("access-control-allow-origin", "*") + .header( + "access-control-allow-headers", + "Content-Type, Authorization", + ); + + if status == StatusCode::OK { + builder = builder.header(hyper::header::CONTENT_TYPE, "application/json"); + } + + builder.body(text.into()).unwrap() +} + +fn parse_params(req: &Request) -> HashMap> { + match req.uri().query() { + Some(query_string) => form_urlencoded::parse(query_string.as_bytes()) + .into_owned() + .fold(HashMap::new(), |mut hm, (k, v)| { + hm.entry(k).or_insert(vec![]).push(v); + hm + }), + None => HashMap::new(), + } +} + +fn parse_body(req: Request) -> Box + Send> +where + for<'de> T: Deserialize<'de> + Send + 'static, +{ + Box::new( + req.into_body() + .concat2() + .map_err(|_| ErrorKind::GenericError("Failed to read request".to_owned()).into()) + .and_then(|body| match serde_json::from_reader(&body.to_vec()[..]) { + Ok(obj) => ok(obj), + Err(e) => { + err(ErrorKind::GenericError(format!("Invalid request body: {}", e)).into()) + } + }), + ) +} diff --git a/src/bin/wallet/display.rs b/src/bin/wallet/display.rs new file mode 100644 index 0000000..041494f --- /dev/null +++ b/src/bin/wallet/display.rs @@ -0,0 +1,476 @@ +// Copyright 2018 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::core::core::{self, amount_to_hr_string}; +use crate::core::global; +use crate::libwallet::types::{AcctPathMapping, OutputData, OutputStatus, TxLogEntry, WalletInfo}; +use crate::libwallet::Error; +use crate::util; +use crate::util::secp::pedersen; +use prettytable; +use std::io::prelude::Write; +use term; + +/// Display outputs in a pretty way +pub fn outputs( + account: &str, + cur_height: u64, + validated: bool, + outputs: Vec<(OutputData, pedersen::Commitment)>, + dark_background_color_scheme: bool, +) -> Result<(), Error> { + let title = format!( + "Wallet Outputs - Account '{}' - Block Height: {}", + account, cur_height + ); + println!(); + let mut t = term::stdout().unwrap(); + t.fg(term::color::MAGENTA).unwrap(); + writeln!(t, "{}", title).unwrap(); + t.reset().unwrap(); + + let mut table = table!(); + + table.set_titles(row![ + bMG->"Output Commitment", + bMG->"MMR Index", + bMG->"Block Height", + bMG->"Locked Until", + bMG->"Status", + bMG->"Coinbase?", + bMG->"# Confirms", + bMG->"Value", + bMG->"Tx" + ]); + + for (out, commit) in outputs { + let commit = format!("{}", util::to_hex(commit.as_ref().to_vec())); + let index = match out.mmr_index { + None => "None".to_owned(), + Some(t) => t.to_string(), + }; + let height = format!("{}", out.height); + let lock_height = format!("{}", out.lock_height); + let is_coinbase = format!("{}", out.is_coinbase); + + // Mark unconfirmed coinbase outputs as "Mining" instead of "Unconfirmed" + let status = match out.status { + OutputStatus::Unconfirmed if out.is_coinbase => "Mining".to_string(), + _ => format!("{}", out.status), + }; + + let num_confirmations = format!("{}", out.num_confirmations(cur_height)); + let value = format!("{}", core::amount_to_hr_string(out.value, false)); + let tx = match out.tx_log_entry { + None => "".to_owned(), + Some(t) => t.to_string(), + }; + + if dark_background_color_scheme { + table.add_row(row![ + bFC->commit, + bFB->index, + bFB->height, + bFB->lock_height, + bFR->status, + bFY->is_coinbase, + bFB->num_confirmations, + bFG->value, + bFC->tx, + ]); + } else { + table.add_row(row![ + bFD->commit, + bFB->index, + bFB->height, + bFB->lock_height, + bFR->status, + bFD->is_coinbase, + bFB->num_confirmations, + bFG->value, + bFD->tx, + ]); + } + } + + table.set_format(*prettytable::format::consts::FORMAT_NO_COLSEP); + table.printstd(); + println!(); + + if !validated { + println!( + "\nWARNING: Wallet failed to verify data. \ + The above is from local cache and possibly invalid! \ + (is your `grin server` offline or broken?)" + ); + } + Ok(()) +} + +/// Display transaction log in a pretty way +pub fn txs( + account: &str, + cur_height: u64, + validated: bool, + txs: &Vec, + include_status: bool, + dark_background_color_scheme: bool, +) -> Result<(), Error> { + let title = format!( + "Transaction Log - Account '{}' - Block Height: {}", + account, cur_height + ); + println!(); + let mut t = term::stdout().unwrap(); + t.fg(term::color::MAGENTA).unwrap(); + writeln!(t, "{}", title).unwrap(); + t.reset().unwrap(); + + let mut table = table!(); + + table.set_titles(row![ + bMG->"Id", + bMG->"Type", + bMG->"Shared Transaction Id", + bMG->"Creation Time", + bMG->"Confirmed?", + bMG->"Confirmation Time", + bMG->"Num. \nInputs", + bMG->"Num. \nOutputs", + bMG->"Amount \nCredited", + bMG->"Amount \nDebited", + bMG->"Fee", + bMG->"Net \nDifference", + bMG->"Tx \nData", + ]); + + for t in txs { + let id = format!("{}", t.id); + let slate_id = match t.tx_slate_id { + Some(m) => format!("{}", m), + None => "None".to_owned(), + }; + let entry_type = format!("{}", t.tx_type); + let creation_ts = format!("{}", t.creation_ts.format("%Y-%m-%d %H:%M:%S")); + let confirmation_ts = match t.confirmation_ts { + Some(m) => format!("{}", m.format("%Y-%m-%d %H:%M:%S")), + None => "None".to_owned(), + }; + let confirmed = format!("{}", t.confirmed); + let num_inputs = format!("{}", t.num_inputs); + let num_outputs = format!("{}", t.num_outputs); + let amount_debited_str = core::amount_to_hr_string(t.amount_debited, true); + let amount_credited_str = core::amount_to_hr_string(t.amount_credited, true); + let fee = match t.fee { + Some(f) => format!("{}", core::amount_to_hr_string(f, true)), + None => "None".to_owned(), + }; + let net_diff = if t.amount_credited >= t.amount_debited { + core::amount_to_hr_string(t.amount_credited - t.amount_debited, true) + } else { + format!( + "-{}", + core::amount_to_hr_string(t.amount_debited - t.amount_credited, true) + ) + }; + let tx_data = match t.stored_tx { + Some(_) => "Yes".to_owned(), + None => "None".to_owned(), + }; + if dark_background_color_scheme { + table.add_row(row![ + bFC->id, + bFC->entry_type, + bFC->slate_id, + bFB->creation_ts, + bFC->confirmed, + bFB->confirmation_ts, + bFC->num_inputs, + bFC->num_outputs, + bFG->amount_credited_str, + bFR->amount_debited_str, + bFR->fee, + bFY->net_diff, + bFb->tx_data, + ]); + } else { + if t.confirmed { + table.add_row(row![ + bFD->id, + bFb->entry_type, + bFD->slate_id, + bFB->creation_ts, + bFg->confirmed, + bFB->confirmation_ts, + bFD->num_inputs, + bFD->num_outputs, + bFG->amount_credited_str, + bFD->amount_debited_str, + bFD->fee, + bFG->net_diff, + bFB->tx_data, + ]); + } else { + table.add_row(row![ + bFD->id, + bFb->entry_type, + bFD->slate_id, + bFB->creation_ts, + bFR->confirmed, + bFB->confirmation_ts, + bFD->num_inputs, + bFD->num_outputs, + bFG->amount_credited_str, + bFD->amount_debited_str, + bFD->fee, + bFG->net_diff, + bFB->tx_data, + ]); + } + } + } + + table.set_format(*prettytable::format::consts::FORMAT_NO_COLSEP); + table.printstd(); + println!(); + + if !validated && include_status { + println!( + "\nWARNING: Wallet failed to verify data. \ + The above is from local cache and possibly invalid! \ + (is your `grin server` offline or broken?)" + ); + } + Ok(()) +} +/// Display summary info in a pretty way +pub fn info( + account: &str, + wallet_info: &WalletInfo, + validated: bool, + dark_background_color_scheme: bool, +) { + println!( + "\n____ Wallet Summary Info - Account '{}' as of height {} ____\n", + account, wallet_info.last_confirmed_height, + ); + + let mut table = table!(); + + if dark_background_color_scheme { + table.add_row(row![ + bFG->"Total", + FG->amount_to_hr_string(wallet_info.total, false) + ]); + // Only dispay "Immature Coinbase" if we have related outputs in the wallet. + // This row just introduces confusion if the wallet does not receive coinbase rewards. + if wallet_info.amount_immature > 0 { + table.add_row(row![ + bFY->format!("Immature Coinbase (< {})", global::coinbase_maturity()), + FY->amount_to_hr_string(wallet_info.amount_immature, false) + ]); + } + table.add_row(row![ + bFY->format!("Awaiting Confirmation (< {})", wallet_info.minimum_confirmations), + FY->amount_to_hr_string(wallet_info.amount_awaiting_confirmation, false) + ]); + table.add_row(row![ + Fr->"Locked by previous transaction", + Fr->amount_to_hr_string(wallet_info.amount_locked, false) + ]); + table.add_row(row![ + Fw->"--------------------------------", + Fw->"-------------" + ]); + table.add_row(row![ + bFG->"Currently Spendable", + FG->amount_to_hr_string(wallet_info.amount_currently_spendable, false) + ]); + } else { + table.add_row(row![ + bFG->"Total", + FG->amount_to_hr_string(wallet_info.total, false) + ]); + // Only dispay "Immature Coinbase" if we have related outputs in the wallet. + // This row just introduces confusion if the wallet does not receive coinbase rewards. + if wallet_info.amount_immature > 0 { + table.add_row(row![ + bFB->format!("Immature Coinbase (< {})", global::coinbase_maturity()), + FB->amount_to_hr_string(wallet_info.amount_immature, false) + ]); + } + table.add_row(row![ + bFB->format!("Awaiting Confirmation (< {})", wallet_info.minimum_confirmations), + FB->amount_to_hr_string(wallet_info.amount_awaiting_confirmation, false) + ]); + table.add_row(row![ + Fr->"Locked by previous transaction", + Fr->amount_to_hr_string(wallet_info.amount_locked, false) + ]); + table.add_row(row![ + Fw->"--------------------------------", + Fw->"-------------" + ]); + table.add_row(row![ + bFG->"Currently Spendable", + FG->amount_to_hr_string(wallet_info.amount_currently_spendable, false) + ]); + }; + table.set_format(*prettytable::format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); + table.printstd(); + println!(); + if !validated { + println!( + "\nWARNING: Wallet failed to verify data against a live chain. \ + The above is from local cache and only valid up to the given height! \ + (is your `grin server` offline or broken?)" + ); + } +} + +/// Display summary info in a pretty way +pub fn estimate( + amount: u64, + strategies: Vec<( + &str, // strategy + u64, // total amount to be locked + u64, // fee + )>, + dark_background_color_scheme: bool, +) { + println!( + "\nEstimation for sending {}:\n", + amount_to_hr_string(amount, false) + ); + + let mut table = table!(); + + table.set_titles(row![ + bMG->"Selection strategy", + bMG->"Fee", + bMG->"Will be locked", + ]); + + for (strategy, total, fee) in strategies { + if dark_background_color_scheme { + table.add_row(row![ + bFC->strategy, + FR->amount_to_hr_string(fee, false), + FY->amount_to_hr_string(total, false), + ]); + } else { + table.add_row(row![ + bFD->strategy, + FR->amount_to_hr_string(fee, false), + FY->amount_to_hr_string(total, false), + ]); + } + } + table.printstd(); + println!(); +} + +/// Display list of wallet accounts in a pretty way +pub fn accounts(acct_mappings: Vec) { + println!("\n____ Wallet Accounts ____\n",); + let mut table = table!(); + + table.set_titles(row![ + mMG->"Name", + bMG->"Parent BIP-32 Derivation Path", + ]); + for m in acct_mappings { + table.add_row(row![ + bFC->m.label, + bGC->m.path.to_bip_32_string(), + ]); + } + table.set_format(*prettytable::format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); + table.printstd(); + println!(); +} + +/// Display transaction log messages +pub fn tx_messages(tx: &TxLogEntry, dark_background_color_scheme: bool) -> Result<(), Error> { + let title = format!("Transaction Messages - Transaction '{}'", tx.id,); + println!(); + let mut t = term::stdout().unwrap(); + t.fg(term::color::MAGENTA).unwrap(); + writeln!(t, "{}", title).unwrap(); + t.reset().unwrap(); + + let msgs = match tx.messages.clone() { + None => { + writeln!(t, "{}", "None").unwrap(); + t.reset().unwrap(); + return Ok(()); + } + Some(m) => m.clone(), + }; + + if msgs.messages.is_empty() { + writeln!(t, "{}", "None").unwrap(); + t.reset().unwrap(); + return Ok(()); + } + + let mut table = table!(); + + table.set_titles(row![ + bMG->"Participant Id", + bMG->"Message", + bMG->"Public Key", + bMG->"Signature", + ]); + + let secp = util::static_secp_instance(); + let secp_lock = secp.lock(); + + for m in msgs.messages { + let id = format!("{}", m.id); + let public_key = format!( + "{}", + util::to_hex(m.public_key.serialize_vec(&secp_lock, true).to_vec()) + ); + let message = match m.message { + Some(m) => format!("{}", m), + None => "None".to_owned(), + }; + let message_sig = match m.message_sig { + Some(s) => format!("{}", util::to_hex(s.serialize_der(&secp_lock))), + None => "None".to_owned(), + }; + if dark_background_color_scheme { + table.add_row(row![ + bFC->id, + bFC->message, + bFC->public_key, + bFB->message_sig, + ]); + } else { + table.add_row(row![ + bFD->id, + bFb->message, + bFD->public_key, + bFB->message_sig, + ]); + } + } + + table.set_format(*prettytable::format::consts::FORMAT_NO_COLSEP); + table.printstd(); + println!(); + + Ok(()) +} diff --git a/src/bin/wallet/error.rs b/src/bin/wallet/error.rs new file mode 100644 index 0000000..cb676cc --- /dev/null +++ b/src/bin/wallet/error.rs @@ -0,0 +1,210 @@ +// Copyright 2018 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Implementation specific error types +use crate::api; +use crate::core::core::transaction; +use crate::core::libtx; +use crate::keychain; +use crate::libwallet; +use failure::{Backtrace, Context, Fail}; +use std::env; +use std::fmt::{self, Display}; + +/// Error definition +#[derive(Debug)] +pub struct Error { + pub inner: Context, +} + +/// Wallet errors, mostly wrappers around underlying crypto or I/O errors. +#[derive(Clone, Eq, PartialEq, Debug, Fail)] +pub enum ErrorKind { + /// LibTX Error + #[fail(display = "LibTx Error")] + LibTX(libtx::ErrorKind), + + /// LibWallet Error + #[fail(display = "LibWallet Error: {}", _1)] + LibWallet(libwallet::ErrorKind, String), + + /// Keychain error + #[fail(display = "Keychain error")] + Keychain(keychain::Error), + + /// Transaction Error + #[fail(display = "Transaction error")] + Transaction(transaction::Error), + + /// Secp Error + #[fail(display = "Secp error")] + Secp, + + /// Filewallet error + #[fail(display = "Wallet data error: {}", _0)] + FileWallet(&'static str), + + /// Error when formatting json + #[fail(display = "IO error")] + IO, + + /// Error when formatting json + #[fail(display = "Serde JSON error")] + Format, + + /// Error when contacting a node through its API + #[fail(display = "Node API error")] + Node(api::ErrorKind), + + /// Error originating from hyper. + #[fail(display = "Hyper error")] + Hyper, + + /// Error originating from hyper uri parsing. + #[fail(display = "Uri parsing error")] + Uri, + + /// Attempt to use duplicate transaction id in separate transactions + #[fail(display = "Duplicate transaction ID error")] + DuplicateTransactionId, + + /// Wallet seed already exists + #[fail(display = "Wallet seed file exists: {}", _0)] + WalletSeedExists(String), + + /// Wallet seed doesn't exist + #[fail(display = "Wallet seed doesn't exist error")] + WalletSeedDoesntExist, + + /// Enc/Decryption Error + #[fail(display = "Enc/Decryption error (check password?)")] + Encryption, + + /// BIP 39 word list + #[fail(display = "BIP39 Mnemonic (word list) Error")] + Mnemonic, + + /// Command line argument error + #[fail(display = "{}", _0)] + ArgumentError(String), + + /// Other + #[fail(display = "Generic error: {}", _0)] + GenericError(String), +} + +impl Fail for Error { + fn cause(&self) -> Option<&dyn Fail> { + self.inner.cause() + } + + fn backtrace(&self) -> Option<&Backtrace> { + self.inner.backtrace() + } +} + +impl Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let show_bt = match env::var("RUST_BACKTRACE") { + Ok(r) => { + if r == "1" { + true + } else { + false + } + } + Err(_) => false, + }; + let backtrace = match self.backtrace() { + Some(b) => format!("{}", b), + None => String::from("Unknown"), + }; + let inner_output = format!("{}", self.inner,); + let backtrace_output = format!("\nBacktrace: {}", backtrace); + let mut output = inner_output.clone(); + if show_bt { + output.push_str(&backtrace_output); + } + Display::fmt(&output, f) + } +} + +impl Error { + /// get kind + pub fn kind(&self) -> ErrorKind { + self.inner.get_context().clone() + } + /// get cause + pub fn cause(&self) -> Option<&dyn Fail> { + self.inner.cause() + } + /// get backtrace + pub fn backtrace(&self) -> Option<&Backtrace> { + self.inner.backtrace() + } +} + +impl From for Error { + fn from(kind: ErrorKind) -> Error { + Error { + inner: Context::new(kind), + } + } +} + +impl From> for Error { + fn from(inner: Context) -> Error { + Error { inner: inner } + } +} + +impl From for Error { + fn from(error: api::Error) -> Error { + Error { + inner: Context::new(ErrorKind::Node(error.kind().clone())), + } + } +} + +impl From for Error { + fn from(error: keychain::Error) -> Error { + Error { + inner: Context::new(ErrorKind::Keychain(error)), + } + } +} + +impl From for Error { + fn from(error: transaction::Error) -> Error { + Error { + inner: Context::new(ErrorKind::Transaction(error)), + } + } +} + +impl From for Error { + fn from(error: libwallet::Error) -> Error { + Error { + inner: Context::new(ErrorKind::LibWallet(error.kind(), format!("{}", error))), + } + } +} + +impl From for Error { + fn from(error: libtx::Error) -> Error { + Error { + inner: Context::new(ErrorKind::LibTX(error.kind())), + } + } +} diff --git a/src/bin/wallet/lib.rs b/src/bin/wallet/lib.rs new file mode 100644 index 0000000..72325fe --- /dev/null +++ b/src/bin/wallet/lib.rs @@ -0,0 +1,74 @@ +// Copyright 2018 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Library module for the main wallet functionalities provided by Grin. + +use blake2_rfc as blake2; + +#[macro_use] +extern crate prettytable; + +#[macro_use] +extern crate serde_derive; +#[macro_use] +extern crate log; +use failure; +use grin_api as api; +#[macro_use] +extern crate grin_core as core; +use grin_keychain as keychain; +use grin_store as store; +use grin_util as util; + +mod adapters; +pub mod command; +pub mod controller; +pub mod display; +mod error; +pub mod libwallet; +pub mod lmdb_wallet; +mod node_clients; +pub mod test_framework; +mod types; + +pub use crate::adapters::{ + FileWalletCommAdapter, HTTPWalletCommAdapter, KeybaseWalletCommAdapter, NullWalletCommAdapter, + WalletCommAdapter, +}; +pub use crate::error::{Error, ErrorKind}; +pub use crate::libwallet::slate::Slate; +pub use crate::libwallet::types::{ + BlockFees, CbData, NodeClient, WalletBackend, WalletInfo, WalletInst, +}; +pub use crate::lmdb_wallet::{wallet_db_exists, LMDBBackend}; +pub use crate::node_clients::{create_coinbase, HTTPNodeClient}; +pub use crate::types::{EncryptedWalletSeed, WalletConfig, WalletSeed, SEED_FILE}; + +use crate::util::Mutex; +use std::sync::Arc; + +/// Helper to create an instance of the LMDB wallet +pub fn instantiate_wallet( + wallet_config: WalletConfig, + node_client: impl NodeClient + 'static, + passphrase: &str, + account: &str, +) -> Result>>, Error> { + // First test decryption, so we can abort early if we have the wrong password + let _ = WalletSeed::from_file(&wallet_config, passphrase)?; + let mut db_wallet = LMDBBackend::new(wallet_config.clone(), passphrase, node_client)?; + db_wallet.set_parent_key_id_by_name(account)?; + info!("Using LMDB Backend for wallet"); + Ok(Arc::new(Mutex::new(db_wallet))) +} diff --git a/src/bin/wallet/lmdb_wallet.rs b/src/bin/wallet/lmdb_wallet.rs new file mode 100644 index 0000000..39eabde --- /dev/null +++ b/src/bin/wallet/lmdb_wallet.rs @@ -0,0 +1,570 @@ +// Copyright 2018 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::cell::RefCell; +use std::sync::Arc; +use std::{fs, path}; + +// for writing storedtransaction files +use std::fs::File; +use std::io::{Read, Write}; +use std::path::Path; + +use failure::ResultExt; +use uuid::Uuid; + +use crate::blake2::blake2b::Blake2b; + +use crate::keychain::{ChildNumber, ExtKeychain, Identifier, Keychain}; +use crate::store::{self, option_to_not_found, to_key, to_key_u64}; + +use crate::core::core::Transaction; +use crate::core::{global, ser}; +use crate::libwallet::types::*; +use crate::libwallet::{internal, Error, ErrorKind}; +use crate::types::{WalletConfig, WalletSeed}; +use crate::util; +use crate::util::secp::constants::SECRET_KEY_SIZE; +use crate::util::ZeroingString; + +pub const DB_DIR: &'static str = "db"; +pub const TX_SAVE_DIR: &'static str = "saved_txs"; + +const OUTPUT_PREFIX: u8 = 'o' as u8; +const DERIV_PREFIX: u8 = 'd' as u8; +const CONFIRMED_HEIGHT_PREFIX: u8 = 'c' as u8; +const PRIVATE_TX_CONTEXT_PREFIX: u8 = 'p' as u8; +const TX_LOG_ENTRY_PREFIX: u8 = 't' as u8; +const TX_LOG_ID_PREFIX: u8 = 'i' as u8; +const ACCOUNT_PATH_MAPPING_PREFIX: u8 = 'a' as u8; + +impl From for Error { + fn from(error: store::Error) -> Error { + Error::from(ErrorKind::Backend(format!("{}", error))) + } +} + +/// test to see if database files exist in the current directory. If so, +/// use a DB backend for all operations +pub fn wallet_db_exists(config: WalletConfig) -> bool { + let db_path = path::Path::new(&config.data_file_dir).join(DB_DIR); + db_path.exists() +} + +/// Helper to derive XOR keys for storing private transaction keys in the DB +/// (blind_xor_key, nonce_xor_key) +fn private_ctx_xor_keys( + keychain: &K, + slate_id: &[u8], +) -> Result<([u8; SECRET_KEY_SIZE], [u8; SECRET_KEY_SIZE]), Error> +where + K: Keychain, +{ + let root_key = keychain.derive_key(0, &K::root_key_id())?; + + // derive XOR values for storing secret values in DB + // h(root_key|slate_id|"blind") + let mut hasher = Blake2b::new(SECRET_KEY_SIZE); + hasher.update(&root_key.0[..]); + hasher.update(&slate_id[..]); + hasher.update(&"blind".as_bytes()[..]); + let blind_xor_key = hasher.finalize(); + let mut ret_blind = [0; SECRET_KEY_SIZE]; + ret_blind.copy_from_slice(&blind_xor_key.as_bytes()[0..SECRET_KEY_SIZE]); + + // h(root_key|slate_id|"nonce") + let mut hasher = Blake2b::new(SECRET_KEY_SIZE); + hasher.update(&root_key.0[..]); + hasher.update(&slate_id[..]); + hasher.update(&"nonce".as_bytes()[..]); + let nonce_xor_key = hasher.finalize(); + let mut ret_nonce = [0; SECRET_KEY_SIZE]; + ret_nonce.copy_from_slice(&nonce_xor_key.as_bytes()[0..SECRET_KEY_SIZE]); + + Ok((ret_blind, ret_nonce)) +} + +pub struct LMDBBackend { + db: store::Store, + config: WalletConfig, + /// passphrase: TODO better ways of dealing with this other than storing + passphrase: ZeroingString, + /// Keychain + pub keychain: Option, + /// Parent path to use by default for output operations + parent_key_id: Identifier, + /// wallet to node client + w2n_client: C, +} + +impl LMDBBackend { + pub fn new(config: WalletConfig, passphrase: &str, n_client: C) -> Result { + let db_path = path::Path::new(&config.data_file_dir).join(DB_DIR); + fs::create_dir_all(&db_path).expect("Couldn't create wallet backend directory!"); + + let stored_tx_path = path::Path::new(&config.data_file_dir).join(TX_SAVE_DIR); + fs::create_dir_all(&stored_tx_path) + .expect("Couldn't create wallet backend tx storage directory!"); + + let lmdb_env = Arc::new(store::new_env(db_path.to_str().unwrap().to_string())); + let store = store::Store::open(lmdb_env, DB_DIR); + + // Make sure default wallet derivation path always exists + // as well as path (so it can be retrieved by batches to know where to store + // completed transactions, for reference + let default_account = AcctPathMapping { + label: "default".to_owned(), + path: LMDBBackend::::default_path(), + }; + let acct_key = to_key( + ACCOUNT_PATH_MAPPING_PREFIX, + &mut default_account.label.as_bytes().to_vec(), + ); + + { + let batch = store.batch()?; + batch.put_ser(&acct_key, &default_account)?; + batch.commit()?; + } + + let res = LMDBBackend { + db: store, + config: config.clone(), + passphrase: ZeroingString::from(passphrase), + keychain: None, + parent_key_id: LMDBBackend::::default_path(), + w2n_client: n_client, + }; + Ok(res) + } + + fn default_path() -> Identifier { + // return the default parent wallet path, corresponding to the default account + // in the BIP32 spec. Parent is account 0 at level 2, child output identifiers + // are all at level 3 + ExtKeychain::derive_key_id(2, 0, 0, 0, 0) + } + + /// Just test to see if database files exist in the current directory. If + /// so, use a DB backend for all operations + pub fn exists(config: WalletConfig) -> bool { + let db_path = path::Path::new(&config.data_file_dir).join(DB_DIR); + db_path.exists() + } +} + +impl WalletBackend for LMDBBackend +where + C: NodeClient, + K: Keychain, +{ + /// Initialise with whatever stored credentials we have + fn open_with_credentials(&mut self) -> Result<(), Error> { + let wallet_seed = WalletSeed::from_file(&self.config, &self.passphrase) + .context(ErrorKind::CallbackImpl("Error opening wallet"))?; + self.keychain = Some( + wallet_seed + .derive_keychain(global::is_floonet()) + .context(ErrorKind::CallbackImpl("Error deriving keychain"))?, + ); + Ok(()) + } + + /// Close wallet and remove any stored credentials (TBD) + fn close(&mut self) -> Result<(), Error> { + self.keychain = None; + Ok(()) + } + + /// Return the keychain being used + fn keychain(&mut self) -> &mut K { + self.keychain.as_mut().unwrap() + } + + /// Return the node client being used + fn w2n_client(&mut self) -> &mut C { + &mut self.w2n_client + } + + /// return the version of the commit for caching + fn calc_commit_for_cache( + &mut self, + amount: u64, + id: &Identifier, + ) -> Result, Error> { + if self.config.no_commit_cache == Some(true) { + Ok(None) + } else { + Ok(Some(util::to_hex( + self.keychain().commit(amount, &id)?.0.to_vec(), + ))) + } + } + + /// Set parent path by account name + fn set_parent_key_id_by_name(&mut self, label: &str) -> Result<(), Error> { + let label = label.to_owned(); + let res = self.acct_path_iter().find(|l| l.label == label); + if let Some(a) = res { + self.set_parent_key_id(a.path); + Ok(()) + } else { + return Err(ErrorKind::UnknownAccountLabel(label.clone()).into()); + } + } + + /// set parent path + fn set_parent_key_id(&mut self, id: Identifier) { + self.parent_key_id = id; + } + + fn parent_key_id(&mut self) -> Identifier { + self.parent_key_id.clone() + } + + fn get(&self, id: &Identifier, mmr_index: &Option) -> Result { + let key = match mmr_index { + Some(i) => to_key_u64(OUTPUT_PREFIX, &mut id.to_bytes().to_vec(), *i), + None => to_key(OUTPUT_PREFIX, &mut id.to_bytes().to_vec()), + }; + option_to_not_found(self.db.get_ser(&key), &format!("Key Id: {}", id)).map_err(|e| e.into()) + } + + fn iter<'a>(&'a self) -> Box + 'a> { + Box::new(self.db.iter(&[OUTPUT_PREFIX]).unwrap()) + } + + fn get_tx_log_entry(&self, u: &Uuid) -> Result, Error> { + let key = to_key(TX_LOG_ENTRY_PREFIX, &mut u.as_bytes().to_vec()); + self.db.get_ser(&key).map_err(|e| e.into()) + } + + fn tx_log_iter<'a>(&'a self) -> Box + 'a> { + Box::new(self.db.iter(&[TX_LOG_ENTRY_PREFIX]).unwrap()) + } + + fn get_private_context(&mut self, slate_id: &[u8]) -> Result { + let ctx_key = to_key(PRIVATE_TX_CONTEXT_PREFIX, &mut slate_id.to_vec()); + let (blind_xor_key, nonce_xor_key) = private_ctx_xor_keys(self.keychain(), slate_id)?; + + let mut ctx: Context = option_to_not_found( + self.db.get_ser(&ctx_key), + &format!("Slate id: {:x?}", slate_id.to_vec()), + )?; + + for i in 0..SECRET_KEY_SIZE { + ctx.sec_key.0[i] = ctx.sec_key.0[i] ^ blind_xor_key[i]; + ctx.sec_nonce.0[i] = ctx.sec_nonce.0[i] ^ nonce_xor_key[i]; + } + + Ok(ctx) + } + + fn acct_path_iter<'a>(&'a self) -> Box + 'a> { + Box::new(self.db.iter(&[ACCOUNT_PATH_MAPPING_PREFIX]).unwrap()) + } + + fn get_acct_path(&self, label: String) -> Result, Error> { + let acct_key = to_key(ACCOUNT_PATH_MAPPING_PREFIX, &mut label.as_bytes().to_vec()); + self.db.get_ser(&acct_key).map_err(|e| e.into()) + } + + fn store_tx(&self, uuid: &str, tx: &Transaction) -> Result<(), Error> { + let filename = format!("{}.grintx", uuid); + let path = path::Path::new(&self.config.data_file_dir) + .join(TX_SAVE_DIR) + .join(filename); + let path_buf = Path::new(&path).to_path_buf(); + let mut stored_tx = File::create(path_buf)?; + let tx_hex = util::to_hex(ser::ser_vec(tx).unwrap());; + stored_tx.write_all(&tx_hex.as_bytes())?; + stored_tx.sync_all()?; + Ok(()) + } + + fn get_stored_tx(&self, entry: &TxLogEntry) -> Result, Error> { + let filename = match entry.stored_tx.clone() { + Some(f) => f, + None => return Ok(None), + }; + let path = path::Path::new(&self.config.data_file_dir) + .join(TX_SAVE_DIR) + .join(filename); + let tx_file = Path::new(&path).to_path_buf(); + let mut tx_f = File::open(tx_file)?; + let mut content = String::new(); + tx_f.read_to_string(&mut content)?; + let tx_bin = util::from_hex(content).unwrap(); + Ok(Some( + ser::deserialize::(&mut &tx_bin[..]).unwrap(), + )) + } + + fn batch<'a>(&'a mut self) -> Result + 'a>, Error> { + Ok(Box::new(Batch { + _store: self, + db: RefCell::new(Some(self.db.batch()?)), + keychain: self.keychain.clone(), + })) + } + + fn next_child<'a>(&mut self) -> Result { + let parent_key_id = self.parent_key_id.clone(); + let mut deriv_idx = { + let batch = self.db.batch()?; + let deriv_key = to_key(DERIV_PREFIX, &mut self.parent_key_id.to_bytes().to_vec()); + match batch.get_ser(&deriv_key)? { + Some(idx) => idx, + None => 0, + } + }; + let mut return_path = self.parent_key_id.to_path(); + return_path.depth = return_path.depth + 1; + return_path.path[return_path.depth as usize - 1] = ChildNumber::from(deriv_idx); + deriv_idx = deriv_idx + 1; + let mut batch = self.batch()?; + batch.save_child_index(&parent_key_id, deriv_idx)?; + batch.commit()?; + Ok(Identifier::from_path(&return_path)) + } + + fn last_confirmed_height<'a>(&mut self) -> Result { + let batch = self.db.batch()?; + let height_key = to_key( + CONFIRMED_HEIGHT_PREFIX, + &mut self.parent_key_id.to_bytes().to_vec(), + ); + let last_confirmed_height = match batch.get_ser(&height_key)? { + Some(h) => h, + None => 0, + }; + Ok(last_confirmed_height) + } + + fn restore(&mut self) -> Result<(), Error> { + internal::restore::restore(self).context(ErrorKind::Restore)?; + Ok(()) + } + + fn check_repair(&mut self) -> Result<(), Error> { + internal::restore::check_repair(self).context(ErrorKind::Restore)?; + Ok(()) + } +} + +/// An atomic batch in which all changes can be committed all at once or +/// discarded on error. +pub struct Batch<'a, C, K> +where + C: NodeClient, + K: Keychain, +{ + _store: &'a LMDBBackend, + db: RefCell>>, + /// Keychain + keychain: Option, +} + +#[allow(missing_docs)] +impl<'a, C, K> WalletOutputBatch for Batch<'a, C, K> +where + C: NodeClient, + K: Keychain, +{ + fn keychain(&mut self) -> &mut K { + self.keychain.as_mut().unwrap() + } + + fn save(&mut self, out: OutputData) -> Result<(), Error> { + // Save the output data to the db. + { + let key = match out.mmr_index { + Some(i) => to_key_u64(OUTPUT_PREFIX, &mut out.key_id.to_bytes().to_vec(), i), + None => to_key(OUTPUT_PREFIX, &mut out.key_id.to_bytes().to_vec()), + }; + self.db.borrow().as_ref().unwrap().put_ser(&key, &out)?; + } + + Ok(()) + } + + fn get(&self, id: &Identifier, mmr_index: &Option) -> Result { + let key = match mmr_index { + Some(i) => to_key_u64(OUTPUT_PREFIX, &mut id.to_bytes().to_vec(), *i), + None => to_key(OUTPUT_PREFIX, &mut id.to_bytes().to_vec()), + }; + option_to_not_found( + self.db.borrow().as_ref().unwrap().get_ser(&key), + &format!("Key ID: {}", id), + ) + .map_err(|e| e.into()) + } + + fn iter(&self) -> Box> { + Box::new( + self.db + .borrow() + .as_ref() + .unwrap() + .iter(&[OUTPUT_PREFIX]) + .unwrap(), + ) + } + + fn delete(&mut self, id: &Identifier, mmr_index: &Option) -> Result<(), Error> { + // Delete the output data. + { + let key = match mmr_index { + Some(i) => to_key_u64(OUTPUT_PREFIX, &mut id.to_bytes().to_vec(), *i), + None => to_key(OUTPUT_PREFIX, &mut id.to_bytes().to_vec()), + }; + let _ = self.db.borrow().as_ref().unwrap().delete(&key); + } + + Ok(()) + } + + fn next_tx_log_id(&mut self, parent_key_id: &Identifier) -> Result { + let tx_id_key = to_key(TX_LOG_ID_PREFIX, &mut parent_key_id.to_bytes().to_vec()); + let last_tx_log_id = match self.db.borrow().as_ref().unwrap().get_ser(&tx_id_key)? { + Some(t) => t, + None => 0, + }; + self.db + .borrow() + .as_ref() + .unwrap() + .put_ser(&tx_id_key, &(last_tx_log_id + 1))?; + Ok(last_tx_log_id) + } + + fn tx_log_iter(&self) -> Box> { + Box::new( + self.db + .borrow() + .as_ref() + .unwrap() + .iter(&[TX_LOG_ENTRY_PREFIX]) + .unwrap(), + ) + } + + fn save_last_confirmed_height( + &mut self, + parent_key_id: &Identifier, + height: u64, + ) -> Result<(), Error> { + let height_key = to_key( + CONFIRMED_HEIGHT_PREFIX, + &mut parent_key_id.to_bytes().to_vec(), + ); + self.db + .borrow() + .as_ref() + .unwrap() + .put_ser(&height_key, &height)?; + Ok(()) + } + + fn save_child_index(&mut self, parent_id: &Identifier, child_n: u32) -> Result<(), Error> { + let deriv_key = to_key(DERIV_PREFIX, &mut parent_id.to_bytes().to_vec()); + self.db + .borrow() + .as_ref() + .unwrap() + .put_ser(&deriv_key, &child_n)?; + Ok(()) + } + + fn save_tx_log_entry( + &mut self, + tx_in: TxLogEntry, + parent_id: &Identifier, + ) -> Result<(), Error> { + let tx_log_key = to_key_u64( + TX_LOG_ENTRY_PREFIX, + &mut parent_id.to_bytes().to_vec(), + tx_in.id as u64, + ); + self.db + .borrow() + .as_ref() + .unwrap() + .put_ser(&tx_log_key, &tx_in)?; + Ok(()) + } + + fn save_acct_path(&mut self, mapping: AcctPathMapping) -> Result<(), Error> { + let acct_key = to_key( + ACCOUNT_PATH_MAPPING_PREFIX, + &mut mapping.label.as_bytes().to_vec(), + ); + self.db + .borrow() + .as_ref() + .unwrap() + .put_ser(&acct_key, &mapping)?; + Ok(()) + } + + fn acct_path_iter(&self) -> Box> { + Box::new( + self.db + .borrow() + .as_ref() + .unwrap() + .iter(&[ACCOUNT_PATH_MAPPING_PREFIX]) + .unwrap(), + ) + } + + fn lock_output(&mut self, out: &mut OutputData) -> Result<(), Error> { + out.lock(); + self.save(out.clone()) + } + + fn save_private_context(&mut self, slate_id: &[u8], ctx: &Context) -> Result<(), Error> { + let ctx_key = to_key(PRIVATE_TX_CONTEXT_PREFIX, &mut slate_id.to_vec()); + let (blind_xor_key, nonce_xor_key) = private_ctx_xor_keys(self.keychain(), slate_id)?; + + let mut s_ctx = ctx.clone(); + for i in 0..SECRET_KEY_SIZE { + s_ctx.sec_key.0[i] = s_ctx.sec_key.0[i] ^ blind_xor_key[i]; + s_ctx.sec_nonce.0[i] = s_ctx.sec_nonce.0[i] ^ nonce_xor_key[i]; + } + + self.db + .borrow() + .as_ref() + .unwrap() + .put_ser(&ctx_key, &s_ctx)?; + Ok(()) + } + + fn delete_private_context(&mut self, slate_id: &[u8]) -> Result<(), Error> { + let ctx_key = to_key(PRIVATE_TX_CONTEXT_PREFIX, &mut slate_id.to_vec()); + self.db + .borrow() + .as_ref() + .unwrap() + .delete(&ctx_key) + .map_err(|e| e.into()) + } + + fn commit(&self) -> Result<(), Error> { + let db = self.db.replace(None); + db.unwrap().commit()?; + Ok(()) + } +} diff --git a/src/bin/wallet/node_clients/http.rs b/src/bin/wallet/node_clients/http.rs new file mode 100644 index 0000000..e07a9c6 --- /dev/null +++ b/src/bin/wallet/node_clients/http.rs @@ -0,0 +1,219 @@ +// Copyright 2018 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Client functions, implementations of the NodeClient trait +//! specific to the FileWallet + +use failure::ResultExt; +use futures::{stream, Stream}; + +use crate::libwallet::types::*; +use std::collections::HashMap; +use tokio::runtime::Runtime; + +use crate::api; +use crate::error::{Error, ErrorKind}; +use crate::libwallet; +use crate::util; +use crate::util::secp::pedersen; + +#[derive(Clone)] +pub struct HTTPNodeClient { + node_url: String, + node_api_secret: Option, +} + +impl HTTPNodeClient { + /// Create a new client that will communicate with the given grin node + pub fn new(node_url: &str, node_api_secret: Option) -> HTTPNodeClient { + HTTPNodeClient { + node_url: node_url.to_owned(), + node_api_secret: node_api_secret, + } + } +} + +impl NodeClient for HTTPNodeClient { + fn node_url(&self) -> &str { + &self.node_url + } + fn node_api_secret(&self) -> Option { + self.node_api_secret.clone() + } + + fn set_node_url(&mut self, node_url: &str) { + self.node_url = node_url.to_owned(); + } + + fn set_node_api_secret(&mut self, node_api_secret: Option) { + self.node_api_secret = node_api_secret; + } + + /// Posts a transaction to a grin node + fn post_tx(&self, tx: &TxWrapper, fluff: bool) -> Result<(), libwallet::Error> { + let url; + let dest = self.node_url(); + if fluff { + url = format!("{}/v1/pool/push?fluff", dest); + } else { + url = format!("{}/v1/pool/push", dest); + } + let res = api::client::post_no_ret(url.as_str(), self.node_api_secret(), tx); + if let Err(e) = res { + let report = format!("Posting transaction to node: {}", e); + error!("Post TX Error: {}", e); + return Err(libwallet::ErrorKind::ClientCallback(report).into()); + } + Ok(()) + } + + /// Return the chain tip from a given node + fn get_chain_height(&self) -> Result { + let addr = self.node_url(); + let url = format!("{}/v1/chain", addr); + let res = api::client::get::(url.as_str(), self.node_api_secret()); + match res { + Err(e) => { + let report = format!("Getting chain height from node: {}", e); + error!("Get chain height error: {}", e); + Err(libwallet::ErrorKind::ClientCallback(report).into()) + } + Ok(r) => Ok(r.height), + } + } + + /// Retrieve outputs from node + fn get_outputs_from_node( + &self, + wallet_outputs: Vec, + ) -> Result, libwallet::Error> { + let addr = self.node_url(); + // build the necessary query params - + // ?id=xxx&id=yyy&id=zzz + let query_params: Vec = wallet_outputs + .iter() + .map(|commit| format!("id={}", util::to_hex(commit.as_ref().to_vec()))) + .collect(); + + // build a map of api outputs by commit so we can look them up efficiently + let mut api_outputs: HashMap = HashMap::new(); + let mut tasks = Vec::new(); + + for query_chunk in query_params.chunks(500) { + let url = format!("{}/v1/chain/outputs/byids?{}", addr, query_chunk.join("&"),); + tasks.push(api::client::get_async::>( + url.as_str(), + self.node_api_secret(), + )); + } + + let task = stream::futures_unordered(tasks).collect(); + + let mut rt = Runtime::new().unwrap(); + let results = match rt.block_on(task) { + Ok(outputs) => outputs, + Err(e) => { + let report = format!("Getting outputs by id: {}", e); + error!("Outputs by id failed: {}", e); + return Err(libwallet::ErrorKind::ClientCallback(report).into()); + } + }; + + for res in results { + for out in res { + api_outputs.insert( + out.commit.commit(), + (util::to_hex(out.commit.to_vec()), out.height, out.mmr_index), + ); + } + } + Ok(api_outputs) + } + + fn get_outputs_by_pmmr_index( + &self, + start_height: u64, + max_outputs: u64, + ) -> Result< + ( + u64, + u64, + Vec<(pedersen::Commitment, pedersen::RangeProof, bool, u64, u64)>, + ), + libwallet::Error, + > { + let addr = self.node_url(); + let query_param = format!("start_index={}&max={}", start_height, max_outputs); + + let url = format!("{}/v1/txhashset/outputs?{}", addr, query_param,); + + let mut api_outputs: Vec<(pedersen::Commitment, pedersen::RangeProof, bool, u64, u64)> = + Vec::new(); + + match api::client::get::(url.as_str(), self.node_api_secret()) { + Ok(o) => { + for out in o.outputs { + let is_coinbase = match out.output_type { + api::OutputType::Coinbase => true, + api::OutputType::Transaction => false, + }; + api_outputs.push(( + out.commit, + out.range_proof().unwrap(), + is_coinbase, + out.block_height.unwrap(), + out.mmr_index, + )); + } + + Ok((o.highest_index, o.last_retrieved_index, api_outputs)) + } + Err(e) => { + // if we got anything other than 200 back from server, bye + error!( + "get_outputs_by_pmmr_index: error contacting {}. Error: {}", + addr, e + ); + let report = format!("outputs by pmmr index: {}", e); + Err(libwallet::ErrorKind::ClientCallback(report))? + } + } + } +} + +/// Call the wallet API to create a coinbase output for the given block_fees. +/// Will retry based on default "retry forever with backoff" behavior. +pub fn create_coinbase(dest: &str, block_fees: &BlockFees) -> Result { + let url = format!("{}/v1/wallet/foreign/build_coinbase", dest); + match single_create_coinbase(&url, &block_fees) { + Err(e) => { + error!( + "Failed to get coinbase from {}. Run grin wallet listen?", + url + ); + error!("Underlying Error: {}", e.cause().unwrap()); + error!("Backtrace: {}", e.backtrace().unwrap()); + Err(e)? + } + Ok(res) => Ok(res), + } +} + +/// Makes a single request to the wallet API to create a new coinbase output. +fn single_create_coinbase(url: &str, block_fees: &BlockFees) -> Result { + let res = api::client::post(url, None, block_fees).context(ErrorKind::GenericError( + "Posting create coinbase".to_string(), + ))?; + Ok(res) +} diff --git a/src/bin/wallet/node_clients/mod.rs b/src/bin/wallet/node_clients/mod.rs new file mode 100644 index 0000000..ffe0c71 --- /dev/null +++ b/src/bin/wallet/node_clients/mod.rs @@ -0,0 +1,17 @@ +// Copyright 2018 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +mod http; + +pub use self::http::{create_coinbase, HTTPNodeClient}; diff --git a/src/bin/wallet/test_framework/mod.rs b/src/bin/wallet/test_framework/mod.rs new file mode 100644 index 0000000..029dff5 --- /dev/null +++ b/src/bin/wallet/test_framework/mod.rs @@ -0,0 +1,225 @@ +// Copyright 2018 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use self::chain::Chain; +use self::core::core::{OutputFeatures, OutputIdentifier, Transaction}; +use self::core::{consensus, global, pow, ser}; +use self::util::secp::pedersen; +use self::util::Mutex; +use crate::libwallet::api::APIOwner; +use crate::libwallet::types::{BlockFees, CbData, NodeClient, WalletInfo, WalletInst}; +use crate::lmdb_wallet::LMDBBackend; +use crate::{controller, libwallet, WalletSeed}; +use crate::{WalletBackend, WalletConfig}; +use chrono::Duration; +use grin_api as api; +use grin_chain as chain; +use grin_core as core; +use grin_keychain as keychain; +use grin_util as util; +use std::sync::Arc; + +mod testclient; + +pub use self::{testclient::LocalWalletClient, testclient::WalletProxy}; + +/// types of backends tests should iterate through +//#[derive(Clone)] +//pub enum BackendType { +// /// File +// FileBackend, +// /// LMDB +// LMDBBackend, +//} + +/// Get an output from the chain locally and present it back as an API output +fn get_output_local(chain: &chain::Chain, commit: &pedersen::Commitment) -> Option { + let outputs = [ + OutputIdentifier::new(OutputFeatures::Plain, commit), + OutputIdentifier::new(OutputFeatures::Coinbase, commit), + ]; + + for x in outputs.iter() { + if let Ok(_) = chain.is_unspent(&x) { + let block_height = chain.get_header_for_output(&x).unwrap().height; + let output_pos = chain.get_output_pos(&x.commit).unwrap_or(0); + return Some(api::Output::new(&commit, block_height, output_pos)); + } + } + None +} + +/// get output listing traversing pmmr from local +fn get_outputs_by_pmmr_index_local( + chain: Arc, + start_index: u64, + max: u64, +) -> api::OutputListing { + let outputs = chain + .unspent_outputs_by_insertion_index(start_index, max) + .unwrap(); + api::OutputListing { + last_retrieved_index: outputs.0, + highest_index: outputs.1, + outputs: outputs + .2 + .iter() + .map(|x| api::OutputPrintable::from_output(x, chain.clone(), None, true)) + .collect(), + } +} + +/// Adds a block with a given reward to the chain and mines it +pub fn add_block_with_reward(chain: &Chain, txs: Vec<&Transaction>, reward: CbData) { + let prev = chain.head_header().unwrap(); + let next_header_info = consensus::next_difficulty(1, chain.difficulty_iter()); + let out_bin = util::from_hex(reward.output).unwrap(); + let kern_bin = util::from_hex(reward.kernel).unwrap(); + let output = ser::deserialize(&mut &out_bin[..]).unwrap(); + let kernel = ser::deserialize(&mut &kern_bin[..]).unwrap(); + let mut b = core::core::Block::new( + &prev, + txs.into_iter().cloned().collect(), + next_header_info.clone().difficulty, + (output, kernel), + ) + .unwrap(); + b.header.timestamp = prev.timestamp + Duration::seconds(60); + b.header.pow.secondary_scaling = next_header_info.secondary_scaling; + chain.set_txhashset_roots(&mut b).unwrap(); + pow::pow_size( + &mut b.header, + next_header_info.difficulty, + global::proofsize(), + global::min_edge_bits(), + ) + .unwrap(); + chain.process_block(b, chain::Options::MINE).unwrap(); + chain.validate(false).unwrap(); +} + +/// adds a reward output to a wallet, includes that reward in a block, mines +/// the block and adds it to the chain, with option transactions included. +/// Helpful for building up precise wallet balances for testing. +pub fn award_block_to_wallet( + chain: &Chain, + txs: Vec<&Transaction>, + wallet: Arc>>, +) -> Result<(), libwallet::Error> +where + C: NodeClient, + K: keychain::Keychain, +{ + // build block fees + let prev = chain.head_header().unwrap(); + let fee_amt = txs.iter().map(|tx| tx.fee()).sum(); + let block_fees = BlockFees { + fees: fee_amt, + key_id: None, + height: prev.height + 1, + }; + // build coinbase (via api) and add block + controller::foreign_single_use(wallet.clone(), |api| { + let coinbase_tx = api.build_coinbase(&block_fees)?; + add_block_with_reward(chain, txs, coinbase_tx.clone()); + Ok(()) + })?; + Ok(()) +} + +/// Award a blocks to a wallet directly +pub fn award_blocks_to_wallet( + chain: &Chain, + wallet: Arc>>, + number: usize, +) -> Result<(), libwallet::Error> +where + C: NodeClient, + K: keychain::Keychain, +{ + for _ in 0..number { + award_block_to_wallet(chain, vec![], wallet.clone())?; + } + Ok(()) +} + +/// dispatch a db wallet +pub fn create_wallet( + dir: &str, + n_client: C, + rec_phrase: Option<&str>, +) -> Arc>> +where + C: NodeClient + 'static, + K: keychain::Keychain + 'static, +{ + let z_string = match rec_phrase { + Some(s) => Some(util::ZeroingString::from(s)), + None => None, + }; + let mut wallet_config = WalletConfig::default(); + wallet_config.data_file_dir = String::from(dir); + let _ = WalletSeed::init_file(&wallet_config, 32, z_string, ""); + let mut wallet = LMDBBackend::new(wallet_config.clone(), "", n_client) + .unwrap_or_else(|e| panic!("Error creating wallet: {:?} Config: {:?}", e, wallet_config)); + wallet.open_with_credentials().unwrap_or_else(|e| { + panic!( + "Error initializing wallet: {:?} Config: {:?}", + e, wallet_config + ) + }); + Arc::new(Mutex::new(wallet)) +} + +/// send an amount to a destination +pub fn send_to_dest( + client: LocalWalletClient, + api: &mut APIOwner, + dest: &str, + amount: u64, +) -> Result<(), libwallet::Error> +where + T: WalletBackend, + C: NodeClient, + K: keychain::Keychain, +{ + let (slate_i, lock_fn) = api.initiate_tx( + None, // account + amount, // amount + 2, // minimum confirmations + 500, // max outputs + 1, // num change outputs + true, // select all outputs + None, + )?; + let mut slate = client.send_tx_slate_direct(dest, &slate_i)?; + api.tx_lock_outputs(&slate, lock_fn)?; + api.finalize_tx(&mut slate)?; + api.post_tx(&slate.tx, false)?; // mines a block + Ok(()) +} + +/// get wallet info totals +pub fn wallet_info( + api: &mut APIOwner, +) -> Result +where + T: WalletBackend, + C: NodeClient, + K: keychain::Keychain, +{ + let (wallet_refreshed, wallet_info) = api.retrieve_summary_info(true, 1)?; + assert!(wallet_refreshed); + Ok(wallet_info) +} diff --git a/src/bin/wallet/test_framework/testclient.rs b/src/bin/wallet/test_framework/testclient.rs new file mode 100644 index 0000000..e99b605 --- /dev/null +++ b/src/bin/wallet/test_framework/testclient.rs @@ -0,0 +1,532 @@ +// Copyright 2018 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Test client that acts against a local instance of a node +//! so that wallet API can be fully exercised +//! Operates directly on a chain instance + +use self::chain::types::NoopAdapter; +use self::chain::Chain; +use self::core::core::verifier_cache::LruVerifierCache; +use self::core::core::Transaction; +use self::core::global::{set_mining_mode, ChainTypes}; +use self::core::{pow, ser}; +use self::keychain::Keychain; +use self::util::secp::pedersen; +use self::util::secp::pedersen::Commitment; +use self::util::{Mutex, RwLock, StopState}; +use crate::libwallet::slate::Slate; +use crate::libwallet::types::*; +use crate::{controller, libwallet, WalletCommAdapter, WalletConfig}; +use failure::ResultExt; +use grin_api as api; +use grin_chain as chain; +use grin_core as core; +use grin_keychain as keychain; +use grin_store as store; +use grin_util as util; +use serde_json; +use std::collections::HashMap; +use std::marker::PhantomData; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::mpsc::{channel, Receiver, Sender}; +use std::sync::Arc; +use std::thread; +use std::time::Duration; + +/// Messages to simulate wallet requests/responses +#[derive(Clone, Debug)] +pub struct WalletProxyMessage { + /// sender ID + pub sender_id: String, + /// destination wallet (or server) + pub dest: String, + /// method (like a GET url) + pub method: String, + /// payload (json body) + pub body: String, +} + +/// communicates with a chain instance or other wallet +/// listener APIs via message queues +pub struct WalletProxy +where + C: NodeClient, + K: Keychain, +{ + /// directory to create the chain in + pub chain_dir: String, + /// handle to chain itself + pub chain: Arc, + /// list of interested wallets + pub wallets: HashMap< + String, + ( + Sender, + Arc>>, + ), + >, + /// simulate json send to another client + /// address, method, payload (simulate HTTP request) + pub tx: Sender, + /// simulate json receiving + pub rx: Receiver, + /// queue control + pub running: Arc, + /// Phantom + phantom_c: PhantomData, + /// Phantom + phantom_k: PhantomData, +} + +impl WalletProxy +where + C: NodeClient, + K: Keychain, +{ + /// Create a new client that will communicate with the given grin node + pub fn new(chain_dir: &str) -> Self { + set_mining_mode(ChainTypes::AutomatedTesting); + let genesis_block = pow::mine_genesis_block().unwrap(); + let verifier_cache = Arc::new(RwLock::new(LruVerifierCache::new())); + let dir_name = format!("{}/.grin", chain_dir); + let db_env = Arc::new(store::new_env(dir_name.to_string())); + let c = Chain::init( + dir_name.to_string(), + db_env, + Arc::new(NoopAdapter {}), + genesis_block, + pow::verify_size, + verifier_cache, + false, + Arc::new(Mutex::new(StopState::new())), + ) + .unwrap(); + let (tx, rx) = channel(); + let retval = WalletProxy { + chain_dir: chain_dir.to_owned(), + chain: Arc::new(c), + tx: tx, + rx: rx, + wallets: HashMap::new(), + running: Arc::new(AtomicBool::new(false)), + phantom_c: PhantomData, + phantom_k: PhantomData, + }; + retval + } + + /// Add wallet with a given "address" + pub fn add_wallet( + &mut self, + addr: &str, + tx: Sender, + wallet: Arc>>, + ) { + self.wallets.insert(addr.to_owned(), (tx, wallet)); + } + + pub fn stop(&mut self) { + self.running.store(false, Ordering::Relaxed); + } + + /// Run the incoming message queue and respond more or less + /// synchronously + pub fn run(&mut self) -> Result<(), libwallet::Error> { + self.running.store(true, Ordering::Relaxed); + loop { + thread::sleep(Duration::from_millis(10)); + // read queue + let m = self.rx.recv().unwrap(); + trace!("Wallet Client Proxy Received: {:?}", m); + let resp = match m.method.as_ref() { + "get_chain_height" => self.get_chain_height(m)?, + "get_outputs_from_node" => self.get_outputs_from_node(m)?, + "get_outputs_by_pmmr_index" => self.get_outputs_by_pmmr_index(m)?, + "send_tx_slate" => self.send_tx_slate(m)?, + "post_tx" => self.post_tx(m)?, + _ => panic!("Unknown Wallet Proxy Message"), + }; + + self.respond(resp); + if !self.running.load(Ordering::Relaxed) { + return Ok(()); + } + } + } + + /// Return a message to a given wallet client + fn respond(&mut self, m: WalletProxyMessage) { + if let Some(s) = self.wallets.get_mut(&m.dest) { + if let Err(e) = s.0.send(m.clone()) { + panic!("Error sending response from proxy: {:?}, {}", m, e); + } + } else { + panic!("Unknown wallet recipient for response message: {:?}", m); + } + } + + /// post transaction to the chain (and mine it, taking the reward) + fn post_tx(&mut self, m: WalletProxyMessage) -> Result { + let dest_wallet = self.wallets.get_mut(&m.sender_id).unwrap().1.clone(); + let wrapper: TxWrapper = serde_json::from_str(&m.body).context( + libwallet::ErrorKind::ClientCallback("Error parsing TxWrapper".to_owned()), + )?; + + let tx_bin = util::from_hex(wrapper.tx_hex).context( + libwallet::ErrorKind::ClientCallback("Error parsing TxWrapper: tx_bin".to_owned()), + )?; + + let tx: Transaction = ser::deserialize(&mut &tx_bin[..]).context( + libwallet::ErrorKind::ClientCallback("Error parsing TxWrapper: tx".to_owned()), + )?; + + super::award_block_to_wallet(&self.chain, vec![&tx], dest_wallet)?; + + Ok(WalletProxyMessage { + sender_id: "node".to_owned(), + dest: m.sender_id, + method: m.method, + body: "".to_owned(), + }) + } + + /// send tx slate + fn send_tx_slate( + &mut self, + m: WalletProxyMessage, + ) -> Result { + let dest_wallet = self.wallets.get_mut(&m.dest); + if let None = dest_wallet { + panic!("Unknown wallet destination for send_tx_slate: {:?}", m); + } + let w = dest_wallet.unwrap().1.clone(); + let mut slate = serde_json::from_str(&m.body).unwrap(); + controller::foreign_single_use(w.clone(), |listener_api| { + listener_api.receive_tx(&mut slate, None, None)?; + Ok(()) + })?; + Ok(WalletProxyMessage { + sender_id: m.dest, + dest: m.sender_id, + method: m.method, + body: serde_json::to_string(&slate).unwrap(), + }) + } + + /// get chain height + fn get_chain_height( + &mut self, + m: WalletProxyMessage, + ) -> Result { + Ok(WalletProxyMessage { + sender_id: "node".to_owned(), + dest: m.sender_id, + method: m.method, + body: format!("{}", self.chain.head().unwrap().height).to_owned(), + }) + } + + /// get api outputs + fn get_outputs_from_node( + &mut self, + m: WalletProxyMessage, + ) -> Result { + let split = m.body.split(","); + //let mut api_outputs: HashMap = HashMap::new(); + let mut outputs: Vec = vec![]; + for o in split { + let o_str = String::from(o); + if o_str.len() == 0 { + continue; + } + let c = util::from_hex(o_str).unwrap(); + let commit = Commitment::from_vec(c); + let out = super::get_output_local(&self.chain.clone(), &commit); + if let Some(o) = out { + outputs.push(o); + } + } + Ok(WalletProxyMessage { + sender_id: "node".to_owned(), + dest: m.sender_id, + method: m.method, + body: serde_json::to_string(&outputs).unwrap(), + }) + } + + /// get api outputs + fn get_outputs_by_pmmr_index( + &mut self, + m: WalletProxyMessage, + ) -> Result { + let split = m.body.split(",").collect::>(); + let start_index = split[0].parse::().unwrap(); + let max = split[1].parse::().unwrap(); + let ol = super::get_outputs_by_pmmr_index_local(self.chain.clone(), start_index, max); + Ok(WalletProxyMessage { + sender_id: "node".to_owned(), + dest: m.sender_id, + method: m.method, + body: serde_json::to_string(&ol).unwrap(), + }) + } +} + +#[derive(Clone)] +pub struct LocalWalletClient { + /// wallet identifier for the proxy queue + pub id: String, + /// proxy's tx queue (receive messages from other wallets or node + pub proxy_tx: Arc>>, + /// my rx queue + pub rx: Arc>>, + /// my tx queue + pub tx: Arc>>, +} + +impl LocalWalletClient { + /// new + pub fn new(id: &str, proxy_rx: Sender) -> Self { + let (tx, rx) = channel(); + LocalWalletClient { + id: id.to_owned(), + proxy_tx: Arc::new(Mutex::new(proxy_rx)), + rx: Arc::new(Mutex::new(rx)), + tx: Arc::new(Mutex::new(tx)), + } + } + + /// get an instance of the send queue for other senders + pub fn get_send_instance(&self) -> Sender { + self.tx.lock().clone() + } + + /// Send the slate to a listening wallet instance + pub fn send_tx_slate_direct( + &self, + dest: &str, + slate: &Slate, + ) -> Result { + let m = WalletProxyMessage { + sender_id: self.id.clone(), + dest: dest.to_owned(), + method: "send_tx_slate".to_owned(), + body: serde_json::to_string(slate).unwrap(), + }; + { + let p = self.proxy_tx.lock(); + p.send(m).context(libwallet::ErrorKind::ClientCallback( + "Send TX Slate".to_owned(), + ))?; + } + let r = self.rx.lock(); + let m = r.recv().unwrap(); + trace!("Received send_tx_slate response: {:?}", m.clone()); + Ok( + serde_json::from_str(&m.body).context(libwallet::ErrorKind::ClientCallback( + "Parsing send_tx_slate response".to_owned(), + ))?, + ) + } +} + +impl WalletCommAdapter for LocalWalletClient { + fn supports_sync(&self) -> bool { + true + } + + /// Send the slate to a listening wallet instance + fn send_tx_sync(&self, dest: &str, slate: &Slate) -> Result { + let m = WalletProxyMessage { + sender_id: self.id.clone(), + dest: dest.to_owned(), + method: "send_tx_slate".to_owned(), + body: serde_json::to_string(slate).unwrap(), + }; + { + let p = self.proxy_tx.lock(); + p.send(m).context(libwallet::ErrorKind::ClientCallback( + "Send TX Slate".to_owned(), + ))?; + } + let r = self.rx.lock(); + let m = r.recv().unwrap(); + trace!("Received send_tx_slate response: {:?}", m.clone()); + Ok( + serde_json::from_str(&m.body).context(libwallet::ErrorKind::ClientCallback( + "Parsing send_tx_slate response".to_owned(), + ))?, + ) + } + + fn send_tx_async(&self, _dest: &str, _slate: &Slate) -> Result<(), libwallet::Error> { + unimplemented!(); + } + + fn receive_tx_async(&self, _params: &str) -> Result { + unimplemented!(); + } + + fn listen( + &self, + _params: HashMap, + _config: WalletConfig, + _passphrase: &str, + _account: &str, + _node_api_secret: Option, + ) -> Result<(), libwallet::Error> { + unimplemented!(); + } +} + +impl NodeClient for LocalWalletClient { + fn node_url(&self) -> &str { + "node" + } + fn node_api_secret(&self) -> Option { + None + } + fn set_node_url(&mut self, _node_url: &str) {} + fn set_node_api_secret(&mut self, _node_api_secret: Option) {} + /// Posts a transaction to a grin node + /// In this case it will create a new block with award rewarded to + fn post_tx(&self, tx: &TxWrapper, _fluff: bool) -> Result<(), libwallet::Error> { + let m = WalletProxyMessage { + sender_id: self.id.clone(), + dest: self.node_url().to_owned(), + method: "post_tx".to_owned(), + body: serde_json::to_string(tx).unwrap(), + }; + { + let p = self.proxy_tx.lock(); + p.send(m).context(libwallet::ErrorKind::ClientCallback( + "post_tx send".to_owned(), + ))?; + } + let r = self.rx.lock(); + let m = r.recv().unwrap(); + trace!("Received post_tx response: {:?}", m.clone()); + Ok(()) + } + + /// Return the chain tip from a given node + fn get_chain_height(&self) -> Result { + let m = WalletProxyMessage { + sender_id: self.id.clone(), + dest: self.node_url().to_owned(), + method: "get_chain_height".to_owned(), + body: "".to_owned(), + }; + { + let p = self.proxy_tx.lock(); + p.send(m).context(libwallet::ErrorKind::ClientCallback( + "Get chain height send".to_owned(), + ))?; + } + let r = self.rx.lock(); + let m = r.recv().unwrap(); + trace!("Received get_chain_height response: {:?}", m.clone()); + Ok(m.body + .parse::() + .context(libwallet::ErrorKind::ClientCallback( + "Parsing get_height response".to_owned(), + ))?) + } + + /// Retrieve outputs from node + fn get_outputs_from_node( + &self, + wallet_outputs: Vec, + ) -> Result, libwallet::Error> { + let query_params: Vec = wallet_outputs + .iter() + .map(|commit| format!("{}", util::to_hex(commit.as_ref().to_vec()))) + .collect(); + let query_str = query_params.join(","); + let m = WalletProxyMessage { + sender_id: self.id.clone(), + dest: self.node_url().to_owned(), + method: "get_outputs_from_node".to_owned(), + body: query_str, + }; + { + let p = self.proxy_tx.lock(); + p.send(m).context(libwallet::ErrorKind::ClientCallback( + "Get outputs from node send".to_owned(), + ))?; + } + let r = self.rx.lock(); + let m = r.recv().unwrap(); + let outputs: Vec = serde_json::from_str(&m.body).unwrap(); + let mut api_outputs: HashMap = HashMap::new(); + for out in outputs { + api_outputs.insert( + out.commit.commit(), + (util::to_hex(out.commit.to_vec()), out.height, out.mmr_index), + ); + } + Ok(api_outputs) + } + + fn get_outputs_by_pmmr_index( + &self, + start_height: u64, + max_outputs: u64, + ) -> Result< + ( + u64, + u64, + Vec<(pedersen::Commitment, pedersen::RangeProof, bool, u64, u64)>, + ), + libwallet::Error, + > { + // start index, max + let query_str = format!("{},{}", start_height, max_outputs); + let m = WalletProxyMessage { + sender_id: self.id.clone(), + dest: self.node_url().to_owned(), + method: "get_outputs_by_pmmr_index".to_owned(), + body: query_str, + }; + { + let p = self.proxy_tx.lock(); + p.send(m).context(libwallet::ErrorKind::ClientCallback( + "Get outputs from node by PMMR index send".to_owned(), + ))?; + } + + let r = self.rx.lock(); + let m = r.recv().unwrap(); + let o: api::OutputListing = serde_json::from_str(&m.body).unwrap(); + + let mut api_outputs: Vec<(pedersen::Commitment, pedersen::RangeProof, bool, u64, u64)> = + Vec::new(); + + for out in o.outputs { + let is_coinbase = match out.output_type { + api::OutputType::Coinbase => true, + api::OutputType::Transaction => false, + }; + api_outputs.push(( + out.commit, + out.range_proof().unwrap(), + is_coinbase, + out.block_height.unwrap(), + out.mmr_index, + )); + } + Ok((o.highest_index, o.last_retrieved_index, api_outputs)) + } +} diff --git a/src/bin/wallet/types.rs b/src/bin/wallet/types.rs new file mode 100644 index 0000000..5031e0f --- /dev/null +++ b/src/bin/wallet/types.rs @@ -0,0 +1,361 @@ +// Copyright 2018 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::fs::{self, File}; +use std::io::{Read, Write}; +use std::path::Path; +use std::path::MAIN_SEPARATOR; + +use crate::blake2; +use rand::{thread_rng, Rng}; +use serde_json; + +use ring::aead; +use ring::{digest, pbkdf2}; + +use crate::core::global::ChainTypes; +use crate::error::{Error, ErrorKind}; +use crate::keychain::{mnemonic, Keychain}; +use crate::util; +use failure::ResultExt; + +pub const SEED_FILE: &'static str = "wallet.seed"; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct WalletConfig { + // Chain parameters (default to Testnet3 if none at the moment) + pub chain_type: Option, + // The api interface/ip_address that this api server (i.e. this wallet) will run + // by default this is 127.0.0.1 (and will not accept connections from external clients) + pub api_listen_interface: String, + // The port this wallet will run on + pub api_listen_port: u16, + // The port this wallet's owner API will run on + pub owner_api_listen_port: Option, + /// Location of the secret for basic auth on the Owner API + pub api_secret_path: Option, + /// Location of the node api secret for basic auth on the Grin API + pub node_api_secret_path: Option, + // The api address of a running server node against which transaction inputs + // will be checked during send + pub check_node_api_http_addr: String, + // Whether to include foreign API endpoints on the Owner API + pub owner_api_include_foreign: Option, + // The directory in which wallet files are stored + pub data_file_dir: String, + /// If Some(true), don't cache commits alongside output data + /// speed improvement, but your commits are in the database + pub no_commit_cache: Option, + /// TLS certificate file + pub tls_certificate_file: Option, + /// TLS certificate private key file + pub tls_certificate_key: Option, + /// Whether to use the black background color scheme for command line + /// if enabled, wallet command output color will be suitable for black background terminal + pub dark_background_color_scheme: Option, + // The exploding lifetime (minutes) for keybase notification on coins received + pub keybase_notify_ttl: Option, +} + +impl Default for WalletConfig { + fn default() -> WalletConfig { + WalletConfig { + chain_type: Some(ChainTypes::Mainnet), + api_listen_interface: "127.0.0.1".to_string(), + api_listen_port: 3415, + owner_api_listen_port: Some(WalletConfig::default_owner_api_listen_port()), + api_secret_path: Some(".api_secret".to_string()), + node_api_secret_path: Some(".api_secret".to_string()), + check_node_api_http_addr: "http://127.0.0.1:3413".to_string(), + owner_api_include_foreign: Some(false), + data_file_dir: ".".to_string(), + no_commit_cache: Some(false), + tls_certificate_file: None, + tls_certificate_key: None, + dark_background_color_scheme: Some(true), + keybase_notify_ttl: Some(1440), + } + } +} + +impl WalletConfig { + pub fn api_listen_addr(&self) -> String { + format!("{}:{}", self.api_listen_interface, self.api_listen_port) + } + + pub fn default_owner_api_listen_port() -> u16 { + 3420 + } + + /// Use value from config file, defaulting to sensible value if missing. + pub fn owner_api_listen_port(&self) -> u16 { + self.owner_api_listen_port + .unwrap_or(WalletConfig::default_owner_api_listen_port()) + } + + pub fn owner_api_listen_addr(&self) -> String { + format!("127.0.0.1:{}", self.owner_api_listen_port()) + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct WalletSeed(Vec); + +impl WalletSeed { + pub fn from_bytes(bytes: &[u8]) -> WalletSeed { + WalletSeed(bytes.to_vec()) + } + + pub fn from_mnemonic(word_list: &str) -> Result { + let res = mnemonic::to_entropy(word_list); + match res { + Ok(s) => Ok(WalletSeed::from_bytes(&s)), + Err(_) => Err(ErrorKind::Mnemonic.into()), + } + } + + pub fn from_hex(hex: &str) -> Result { + let bytes = util::from_hex(hex.to_string()) + .context(ErrorKind::GenericError("Invalid hex".to_owned()))?; + Ok(WalletSeed::from_bytes(&bytes)) + } + + pub fn to_hex(&self) -> String { + util::to_hex(self.0.to_vec()) + } + + pub fn to_mnemonic(&self) -> Result { + let result = mnemonic::from_entropy(&self.0); + match result { + Ok(r) => Ok(r), + Err(_) => Err(ErrorKind::Mnemonic.into()), + } + } + + pub fn derive_keychain_old(old_wallet_seed: [u8; 32], password: &str) -> Vec { + let seed = blake2::blake2b::blake2b(64, password.as_bytes(), &old_wallet_seed); + seed.as_bytes().to_vec() + } + + pub fn derive_keychain(&self, is_floonet: bool) -> Result { + let result = K::from_seed(&self.0, is_floonet)?; + Ok(result) + } + + pub fn init_new(seed_length: usize) -> WalletSeed { + let mut seed: Vec = vec![]; + let mut rng = thread_rng(); + for _ in 0..seed_length { + seed.push(rng.gen()); + } + WalletSeed(seed) + } + + pub fn seed_file_exists(wallet_config: &WalletConfig) -> Result<(), Error> { + let seed_file_path = &format!( + "{}{}{}", + wallet_config.data_file_dir, MAIN_SEPARATOR, SEED_FILE, + ); + if Path::new(seed_file_path).exists() { + return Err(ErrorKind::WalletSeedExists(seed_file_path.to_owned()))?; + } + Ok(()) + } + + pub fn backup_seed(wallet_config: &WalletConfig) -> Result<(), Error> { + let seed_file_name = &format!( + "{}{}{}", + wallet_config.data_file_dir, MAIN_SEPARATOR, SEED_FILE, + ); + + let mut path = Path::new(seed_file_name).to_path_buf(); + path.pop(); + let mut backup_seed_file_name = format!( + "{}{}{}.bak", + wallet_config.data_file_dir, MAIN_SEPARATOR, SEED_FILE + ); + let mut i = 1; + while Path::new(&backup_seed_file_name).exists() { + backup_seed_file_name = format!( + "{}{}{}.bak.{}", + wallet_config.data_file_dir, MAIN_SEPARATOR, SEED_FILE, i + ); + i += 1; + } + path.push(backup_seed_file_name.clone()); + if let Err(_) = fs::rename(seed_file_name, backup_seed_file_name.as_str()) { + return Err(ErrorKind::GenericError( + "Can't rename wallet seed file".to_owned(), + ))?; + } + warn!("{} backed up as {}", seed_file_name, backup_seed_file_name); + Ok(()) + } + + pub fn recover_from_phrase( + wallet_config: &WalletConfig, + word_list: &str, + password: &str, + ) -> Result<(), Error> { + let seed_file_path = &format!( + "{}{}{}", + wallet_config.data_file_dir, MAIN_SEPARATOR, SEED_FILE, + ); + if WalletSeed::seed_file_exists(wallet_config).is_err() { + WalletSeed::backup_seed(wallet_config)?; + } + let seed = WalletSeed::from_mnemonic(word_list)?; + let enc_seed = EncryptedWalletSeed::from_seed(&seed, password)?; + let enc_seed_json = serde_json::to_string_pretty(&enc_seed).context(ErrorKind::Format)?; + let mut file = File::create(seed_file_path).context(ErrorKind::IO)?; + file.write_all(&enc_seed_json.as_bytes()) + .context(ErrorKind::IO)?; + warn!("Seed created from word list"); + Ok(()) + } + + pub fn show_recovery_phrase(&self) -> Result<(), Error> { + println!("Your recovery phrase is:"); + println!(); + println!("{}", self.to_mnemonic()?); + println!(); + println!("Please back-up these words in a non-digital format."); + Ok(()) + } + + pub fn init_file( + wallet_config: &WalletConfig, + seed_length: usize, + recovery_phrase: Option, + password: &str, + ) -> Result { + // create directory if it doesn't exist + fs::create_dir_all(&wallet_config.data_file_dir).context(ErrorKind::IO)?; + + let seed_file_path = &format!( + "{}{}{}", + wallet_config.data_file_dir, MAIN_SEPARATOR, SEED_FILE, + ); + + warn!("Generating wallet seed file at: {}", seed_file_path); + let _ = WalletSeed::seed_file_exists(wallet_config)?; + + let seed = match recovery_phrase { + Some(p) => WalletSeed::from_mnemonic(&p)?, + None => WalletSeed::init_new(seed_length), + }; + + let enc_seed = EncryptedWalletSeed::from_seed(&seed, password)?; + let enc_seed_json = serde_json::to_string_pretty(&enc_seed).context(ErrorKind::Format)?; + let mut file = File::create(seed_file_path).context(ErrorKind::IO)?; + file.write_all(&enc_seed_json.as_bytes()) + .context(ErrorKind::IO)?; + seed.show_recovery_phrase()?; + Ok(seed) + } + + pub fn from_file(wallet_config: &WalletConfig, password: &str) -> Result { + // create directory if it doesn't exist + fs::create_dir_all(&wallet_config.data_file_dir).context(ErrorKind::IO)?; + + let seed_file_path = &format!( + "{}{}{}", + wallet_config.data_file_dir, MAIN_SEPARATOR, SEED_FILE, + ); + + debug!("Using wallet seed file at: {}", seed_file_path); + + if Path::new(seed_file_path).exists() { + let mut file = File::open(seed_file_path).context(ErrorKind::IO)?; + let mut buffer = String::new(); + file.read_to_string(&mut buffer).context(ErrorKind::IO)?; + let enc_seed: EncryptedWalletSeed = + serde_json::from_str(&buffer).context(ErrorKind::Format)?; + let wallet_seed = enc_seed.decrypt(password)?; + Ok(wallet_seed) + } else { + error!( + "wallet seed file {} could not be opened (grin wallet init). \ + Run \"grin wallet init\" to initialize a new wallet.", + seed_file_path + ); + Err(ErrorKind::WalletSeedDoesntExist)? + } + } +} + +/// Encrypted wallet seed, for storing on disk and decrypting +/// with provided password + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub struct EncryptedWalletSeed { + encrypted_seed: String, + /// Salt, not so useful in single case but include anyhow for situations + /// where someone wants to store many of these + pub salt: String, + /// Nonce + pub nonce: String, +} + +impl EncryptedWalletSeed { + /// Create a new encrypted seed from the given seed + password + pub fn from_seed(seed: &WalletSeed, password: &str) -> Result { + let salt: [u8; 8] = thread_rng().gen(); + let nonce: [u8; 12] = thread_rng().gen(); + let password = password.as_bytes(); + let mut key = [0; 32]; + pbkdf2::derive(&digest::SHA512, 100, &salt, password, &mut key); + let content = seed.0.to_vec(); + let mut enc_bytes = content.clone(); + let suffix_len = aead::CHACHA20_POLY1305.tag_len(); + for _ in 0..suffix_len { + enc_bytes.push(0); + } + let sealing_key = + aead::SealingKey::new(&aead::CHACHA20_POLY1305, &key).context(ErrorKind::Encryption)?; + aead::seal_in_place(&sealing_key, &nonce, &[], &mut enc_bytes, suffix_len) + .context(ErrorKind::Encryption)?; + Ok(EncryptedWalletSeed { + encrypted_seed: util::to_hex(enc_bytes.to_vec()), + salt: util::to_hex(salt.to_vec()), + nonce: util::to_hex(nonce.to_vec()), + }) + } + + /// Decrypt seed + pub fn decrypt(&self, password: &str) -> Result { + let mut encrypted_seed = match util::from_hex(self.encrypted_seed.clone()) { + Ok(s) => s, + Err(_) => return Err(ErrorKind::Encryption)?, + }; + let salt = match util::from_hex(self.salt.clone()) { + Ok(s) => s, + Err(_) => return Err(ErrorKind::Encryption)?, + }; + let nonce = match util::from_hex(self.nonce.clone()) { + Ok(s) => s, + Err(_) => return Err(ErrorKind::Encryption)?, + }; + let password = password.as_bytes(); + let mut key = [0; 32]; + pbkdf2::derive(&digest::SHA512, 100, &salt, password, &mut key); + + let opening_key = + aead::OpeningKey::new(&aead::CHACHA20_POLY1305, &key).context(ErrorKind::Encryption)?; + let decrypted_data = aead::open_in_place(&opening_key, &nonce, &[], 0, &mut encrypted_seed) + .context(ErrorKind::Encryption)?; + + Ok(WalletSeed::from_bytes(&decrypted_data)) + } +} diff --git a/src/build/build.rs b/src/build/build.rs new file mode 100644 index 0000000..722505b --- /dev/null +++ b/src/build/build.rs @@ -0,0 +1,50 @@ +// Copyright 2018 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Build hooks to spit out version+build time info + +use built; +use std::env; +use std::path::PathBuf; +use std::process::Command; + +fn main() { + // Setting up git hooks in the project: rustfmt and so on. + let git_hooks = format!( + "git config core.hooksPath {}", + PathBuf::from("./.hooks").to_str().unwrap() + ); + + if cfg!(target_os = "windows") { + Command::new("cmd") + .args(&["/C", &git_hooks]) + .output() + .expect("failed to execute git config for hooks"); + } else { + Command::new("sh") + .args(&["-c", &git_hooks]) + .output() + .expect("failed to execute git config for hooks"); + } + + // build and versioning information + let mut opts = built::Options::default(); + opts.set_dependencies(true); + // don't fail the build if something's missing, may just be cargo release + let _ = built::write_built_file_with_opts( + &opts, + env!("CARGO_MANIFEST_DIR"), + format!("{}{}", env::var("OUT_DIR").unwrap(), "/built.rs"), + ); +} diff --git a/tests/accounts.rs b/tests/accounts.rs new file mode 100644 index 0000000..98340c0 --- /dev/null +++ b/tests/accounts.rs @@ -0,0 +1,259 @@ +// Copyright 2018 The Grin Developers +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! tests differing accounts in the same wallet +#[macro_use] +extern crate log; + +use self::core::global; +use self::core::global::ChainTypes; +use self::keychain::{ExtKeychain, Keychain}; +use self::wallet::libwallet; +use self::wallet::test_framework::{self, LocalWalletClient, WalletProxy}; +use grin_core as core; +use grin_keychain as keychain; +use grin_util as util; +use grin_wallet as wallet; +use std::fs; +use std::thread; +use std::time::Duration; + +fn clean_output_dir(test_dir: &str) { + let _ = fs::remove_dir_all(test_dir); +} + +fn setup(test_dir: &str) { + util::init_test_logger(); + clean_output_dir(test_dir); + global::set_mining_mode(ChainTypes::AutomatedTesting); +} + +/// Various tests on accounts within the same wallet +fn accounts_test_impl(test_dir: &str) -> Result<(), libwallet::Error> { + setup(test_dir); + // Create a new proxy to simulate server and wallet responses + let mut wallet_proxy: WalletProxy = WalletProxy::new(test_dir); + let chain = wallet_proxy.chain.clone(); + + // Create a new wallet test client, and set its queues to communicate with the + // proxy + let client1 = LocalWalletClient::new("wallet1", wallet_proxy.tx.clone()); + let wallet1 = + test_framework::create_wallet(&format!("{}/wallet1", test_dir), client1.clone(), None); + wallet_proxy.add_wallet("wallet1", client1.get_send_instance(), wallet1.clone()); + + let client2 = LocalWalletClient::new("wallet2", wallet_proxy.tx.clone()); + // define recipient wallet, add to proxy + let wallet2 = + test_framework::create_wallet(&format!("{}/wallet2", test_dir), client2.clone(), None); + wallet_proxy.add_wallet("wallet2", client2.get_send_instance(), wallet2.clone()); + + // Set the wallet proxy listener running + thread::spawn(move || { + if let Err(e) = wallet_proxy.run() { + error!("Wallet Proxy error: {}", e); + } + }); + + // few values to keep things shorter + let reward = core::consensus::REWARD; + let cm = global::coinbase_maturity(); // assume all testing precedes soft fork height + + // test default accounts exist + wallet::controller::owner_single_use(wallet1.clone(), |api| { + let accounts = api.accounts()?; + assert_eq!(accounts[0].label, "default"); + assert_eq!(accounts[0].path, ExtKeychain::derive_key_id(2, 0, 0, 0, 0)); + Ok(()) + })?; + + // add some accounts + wallet::controller::owner_single_use(wallet1.clone(), |api| { + let new_path = api.create_account_path("account1").unwrap(); + assert_eq!(new_path, ExtKeychain::derive_key_id(2, 1, 0, 0, 0)); + let new_path = api.create_account_path("account2").unwrap(); + assert_eq!(new_path, ExtKeychain::derive_key_id(2, 2, 0, 0, 0)); + let new_path = api.create_account_path("account3").unwrap(); + assert_eq!(new_path, ExtKeychain::derive_key_id(2, 3, 0, 0, 0)); + // trying to add same label again should fail + let res = api.create_account_path("account1"); + assert!(res.is_err()); + Ok(()) + })?; + + // add account to wallet 2 + wallet::controller::owner_single_use(wallet2.clone(), |api| { + let new_path = api.create_account_path("listener_account").unwrap(); + assert_eq!(new_path, ExtKeychain::derive_key_id(2, 1, 0, 0, 0)); + Ok(()) + })?; + + // Default wallet 2 to listen on that account + { + let mut w = wallet2.lock(); + w.set_parent_key_id_by_name("listener_account")?; + } + + // Mine into two different accounts in the same wallet + { + let mut w = wallet1.lock(); + w.set_parent_key_id_by_name("account1")?; + assert_eq!(w.parent_key_id(), ExtKeychain::derive_key_id(2, 1, 0, 0, 0)); + } + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), 7); + + { + let mut w = wallet1.lock(); + w.set_parent_key_id_by_name("account2")?; + assert_eq!(w.parent_key_id(), ExtKeychain::derive_key_id(2, 2, 0, 0, 0)); + } + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), 5); + + // Should have 5 in account1 (5 spendable), 5 in account (2 spendable) + wallet::controller::owner_single_use(wallet1.clone(), |api| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(true, 1)?; + assert!(wallet1_refreshed); + assert_eq!(wallet1_info.last_confirmed_height, 12); + assert_eq!(wallet1_info.total, 5 * reward); + assert_eq!(wallet1_info.amount_currently_spendable, (5 - cm) * reward); + // check tx log as well + let (_, txs) = api.retrieve_txs(true, None, None)?; + assert_eq!(txs.len(), 5); + Ok(()) + })?; + // now check second account + { + let mut w = wallet1.lock(); + w.set_parent_key_id_by_name("account1")?; + } + wallet::controller::owner_single_use(wallet1.clone(), |api| { + // check last confirmed height on this account is different from above (should be 0) + let (_, wallet1_info) = api.retrieve_summary_info(false, 1)?; + assert_eq!(wallet1_info.last_confirmed_height, 0); + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(true, 1)?; + assert!(wallet1_refreshed); + assert_eq!(wallet1_info.last_confirmed_height, 12); + assert_eq!(wallet1_info.total, 7 * reward); + assert_eq!(wallet1_info.amount_currently_spendable, 7 * reward); + // check tx log as well + let (_, txs) = api.retrieve_txs(true, None, None)?; + assert_eq!(txs.len(), 7); + Ok(()) + })?; + + // should be nothing in default account + { + let mut w = wallet1.lock(); + w.set_parent_key_id_by_name("default")?; + } + wallet::controller::owner_single_use(wallet1.clone(), |api| { + let (_, wallet1_info) = api.retrieve_summary_info(false, 1)?; + assert_eq!(wallet1_info.last_confirmed_height, 0); + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(true, 1)?; + assert!(wallet1_refreshed); + assert_eq!(wallet1_info.last_confirmed_height, 12); + assert_eq!(wallet1_info.total, 0,); + assert_eq!(wallet1_info.amount_currently_spendable, 0,); + // check tx log as well + let (_, txs) = api.retrieve_txs(true, None, None)?; + assert_eq!(txs.len(), 0); + Ok(()) + })?; + + // Send a tx to another wallet + { + let mut w = wallet1.lock(); + w.set_parent_key_id_by_name("account1")?; + } + + wallet::controller::owner_single_use(wallet1.clone(), |api| { + let (mut slate, lock_fn) = api.initiate_tx( + None, reward, // amount + 2, // minimum confirmations + 500, // max outputs + 1, // num change outputs + true, // select all outputs + None, + )?; + slate = client1.send_tx_slate_direct("wallet2", &slate)?; + api.tx_lock_outputs(&slate, lock_fn)?; + api.finalize_tx(&mut slate)?; + api.post_tx(&slate.tx, false)?; + Ok(()) + })?; + + wallet::controller::owner_single_use(wallet1.clone(), |api| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(true, 1)?; + assert!(wallet1_refreshed); + assert_eq!(wallet1_info.last_confirmed_height, 13); + let (_, txs) = api.retrieve_txs(true, None, None)?; + assert_eq!(txs.len(), 9); + Ok(()) + })?; + + // other account should be untouched + { + let mut w = wallet1.lock(); + w.set_parent_key_id_by_name("account2")?; + } + wallet::controller::owner_single_use(wallet1.clone(), |api| { + let (_, wallet1_info) = api.retrieve_summary_info(false, 1)?; + assert_eq!(wallet1_info.last_confirmed_height, 12); + let (_, wallet1_info) = api.retrieve_summary_info(true, 1)?; + assert_eq!(wallet1_info.last_confirmed_height, 13); + let (_, txs) = api.retrieve_txs(true, None, None)?; + println!("{:?}", txs); + assert_eq!(txs.len(), 5); + Ok(()) + })?; + + // wallet 2 should only have this tx on the listener account + wallet::controller::owner_single_use(wallet2.clone(), |api| { + let (wallet2_refreshed, wallet2_info) = api.retrieve_summary_info(true, 1)?; + assert!(wallet2_refreshed); + assert_eq!(wallet2_info.last_confirmed_height, 13); + let (_, txs) = api.retrieve_txs(true, None, None)?; + assert_eq!(txs.len(), 1); + Ok(()) + })?; + // Default account on wallet 2 should be untouched + { + let mut w = wallet2.lock(); + w.set_parent_key_id_by_name("default")?; + } + wallet::controller::owner_single_use(wallet2.clone(), |api| { + let (_, wallet2_info) = api.retrieve_summary_info(false, 1)?; + assert_eq!(wallet2_info.last_confirmed_height, 0); + let (wallet2_refreshed, wallet2_info) = api.retrieve_summary_info(true, 1)?; + assert!(wallet2_refreshed); + assert_eq!(wallet2_info.last_confirmed_height, 13); + assert_eq!(wallet2_info.total, 0,); + assert_eq!(wallet2_info.amount_currently_spendable, 0,); + // check tx log as well + let (_, txs) = api.retrieve_txs(true, None, None)?; + assert_eq!(txs.len(), 0); + Ok(()) + })?; + + // let logging finish + thread::sleep(Duration::from_millis(200)); + Ok(()) +} + +#[test] +fn accounts() { + let test_dir = "test_output/accounts"; + if let Err(e) = accounts_test_impl(test_dir) { + panic!("Libwallet Error: {} - {}", e, e.backtrace().unwrap()); + } +} diff --git a/tests/check.rs b/tests/check.rs new file mode 100644 index 0000000..8e46d70 --- /dev/null +++ b/tests/check.rs @@ -0,0 +1,591 @@ +// Copyright 2018 The Grin Developers +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! tests differing accounts in the same wallet +#[macro_use] +extern crate log; + +use self::core::consensus; +use self::core::global; +use self::core::global::ChainTypes; +use self::keychain::ExtKeychain; +use self::wallet::test_framework::{self, LocalWalletClient, WalletProxy}; +use self::wallet::{libwallet, FileWalletCommAdapter}; +use grin_core as core; +use grin_keychain as keychain; +use grin_util as util; +use grin_wallet as wallet; +use std::fs; +use std::thread; +use std::time::Duration; + +fn clean_output_dir(test_dir: &str) { + let _ = fs::remove_dir_all(test_dir); +} + +fn setup(test_dir: &str) { + util::init_test_logger(); + clean_output_dir(test_dir); + global::set_mining_mode(ChainTypes::AutomatedTesting); +} + +/// Various tests on checking functionality +fn check_repair_impl(test_dir: &str) -> Result<(), libwallet::Error> { + setup(test_dir); + // Create a new proxy to simulate server and wallet responses + let mut wallet_proxy: WalletProxy = WalletProxy::new(test_dir); + let chain = wallet_proxy.chain.clone(); + + // Create a new wallet test client, and set its queues to communicate with the + // proxy + let client1 = LocalWalletClient::new("wallet1", wallet_proxy.tx.clone()); + let wallet1 = + test_framework::create_wallet(&format!("{}/wallet1", test_dir), client1.clone(), None); + wallet_proxy.add_wallet("wallet1", client1.get_send_instance(), wallet1.clone()); + + let client2 = LocalWalletClient::new("wallet2", wallet_proxy.tx.clone()); + // define recipient wallet, add to proxy + let wallet2 = + test_framework::create_wallet(&format!("{}/wallet2", test_dir), client2.clone(), None); + wallet_proxy.add_wallet("wallet2", client2.get_send_instance(), wallet2.clone()); + + // Set the wallet proxy listener running + thread::spawn(move || { + if let Err(e) = wallet_proxy.run() { + error!("Wallet Proxy error: {}", e); + } + }); + + // few values to keep things shorter + let reward = core::consensus::REWARD; + let cm = global::coinbase_maturity() as u64; // assume all testing precedes soft fork height + + // add some accounts + wallet::controller::owner_single_use(wallet1.clone(), |api| { + api.create_account_path("account_1")?; + api.create_account_path("account_2")?; + api.create_account_path("account_3")?; + api.set_active_account("account_1")?; + Ok(()) + })?; + + // add account to wallet 2 + wallet::controller::owner_single_use(wallet2.clone(), |api| { + api.create_account_path("account_1")?; + api.set_active_account("account_1")?; + Ok(()) + })?; + + // Do some mining + let bh = 20u64; + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), bh as usize); + + // Sanity check contents + wallet::controller::owner_single_use(wallet1.clone(), |api| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(true, 1)?; + assert!(wallet1_refreshed); + assert_eq!(wallet1_info.last_confirmed_height, bh); + assert_eq!(wallet1_info.total, bh * reward); + assert_eq!(wallet1_info.amount_currently_spendable, (bh - cm) * reward); + // check tx log as well + let (_, txs) = api.retrieve_txs(true, None, None)?; + let (c, _) = libwallet::types::TxLogEntry::sum_confirmed(&txs); + assert_eq!(wallet1_info.total, c); + assert_eq!(txs.len(), bh as usize); + Ok(()) + })?; + + // Accidentally delete some outputs + let mut w1_outputs_commits = vec![]; + wallet::controller::owner_single_use(wallet1.clone(), |api| { + w1_outputs_commits = api.retrieve_outputs(false, true, None)?.1; + Ok(()) + })?; + let w1_outputs: Vec = + w1_outputs_commits.into_iter().map(|o| o.0).collect(); + { + let mut w = wallet1.lock(); + w.open_with_credentials()?; + { + let mut batch = w.batch()?; + batch.delete(&w1_outputs[4].key_id, &None)?; + batch.delete(&w1_outputs[10].key_id, &None)?; + let mut accidental_spent = w1_outputs[13].clone(); + accidental_spent.status = libwallet::types::OutputStatus::Spent; + batch.save(accidental_spent)?; + batch.commit()?; + } + w.close()?; + } + + // check we have a problem now + wallet::controller::owner_single_use(wallet1.clone(), |api| { + let (_, wallet1_info) = api.retrieve_summary_info(true, 1)?; + let (_, txs) = api.retrieve_txs(true, None, None)?; + let (c, _) = libwallet::types::TxLogEntry::sum_confirmed(&txs); + assert!(wallet1_info.total != c); + Ok(()) + })?; + + // this should restore our missing outputs + wallet::controller::owner_single_use(wallet1.clone(), |api| { + api.check_repair()?; + Ok(()) + })?; + + // check our outputs match again + wallet::controller::owner_single_use(wallet1.clone(), |api| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(true, 1)?; + assert!(wallet1_refreshed); + assert_eq!(wallet1_info.total, bh * reward); + Ok(()) + })?; + + // perform a transaction, but don't let it finish + wallet::controller::owner_single_use(wallet1.clone(), |api| { + // send to send + let (mut slate, lock_fn) = api.initiate_tx( + None, + reward * 2, // amount + cm, // minimum confirmations + 500, // max outputs + 1, // num change outputs + true, // select all outputs + None, // optional message + )?; + // output tx file + let file_adapter = FileWalletCommAdapter::new(); + let send_file = format!("{}/part_tx_1.tx", test_dir); + file_adapter.send_tx_async(&send_file, &mut slate)?; + api.tx_lock_outputs(&slate, lock_fn)?; + Ok(()) + })?; + + // check we're all locked + wallet::controller::owner_single_use(wallet1.clone(), |api| { + let (_, wallet1_info) = api.retrieve_summary_info(true, 1)?; + assert!(wallet1_info.amount_currently_spendable == 0); + Ok(()) + })?; + + // unlock/restore + wallet::controller::owner_single_use(wallet1.clone(), |api| { + api.check_repair()?; + Ok(()) + })?; + + // check spendable amount again + wallet::controller::owner_single_use(wallet1.clone(), |api| { + let (_, wallet1_info) = api.retrieve_summary_info(true, 1)?; + assert_eq!(wallet1_info.amount_currently_spendable, (bh - cm) * reward); + Ok(()) + })?; + + // let logging finish + thread::sleep(Duration::from_millis(200)); + Ok(()) +} + +fn two_wallets_one_seed_impl(test_dir: &str) -> Result<(), libwallet::Error> { + setup(test_dir); + let seed_phrase = + "affair pistol cancel crush garment candy ancient flag work \ + market crush dry stand focus mutual weapon offer ceiling rival turn team spring \ + where swift"; + + // Create a new proxy to simulate server and wallet responses + let mut wallet_proxy: WalletProxy = WalletProxy::new(test_dir); + let chain = wallet_proxy.chain.clone(); + + // Create a new wallet test client, and set its queues to communicate with the + // proxy + let m_client = LocalWalletClient::new("miner", wallet_proxy.tx.clone()); + let miner = + test_framework::create_wallet(&format!("{}/miner", test_dir), m_client.clone(), None); + wallet_proxy.add_wallet("miner", m_client.get_send_instance(), miner.clone()); + + // non-mining recipient wallets + let client1 = LocalWalletClient::new("wallet1", wallet_proxy.tx.clone()); + let wallet1 = test_framework::create_wallet( + &format!("{}/wallet1", test_dir), + client1.clone(), + Some(seed_phrase), + ); + wallet_proxy.add_wallet("wallet1", client1.get_send_instance(), wallet1.clone()); + + let client2 = LocalWalletClient::new("wallet2", wallet_proxy.tx.clone()); + let wallet2 = test_framework::create_wallet( + &format!("{}/wallet2", test_dir), + client2.clone(), + Some(seed_phrase), + ); + wallet_proxy.add_wallet("wallet2", client2.get_send_instance(), wallet2.clone()); + + // we'll restore into here + let client3 = LocalWalletClient::new("wallet3", wallet_proxy.tx.clone()); + let wallet3 = test_framework::create_wallet( + &format!("{}/wallet3", test_dir), + client3.clone(), + Some(seed_phrase), + ); + wallet_proxy.add_wallet("wallet3", client3.get_send_instance(), wallet3.clone()); + + // also restore into here + let client4 = LocalWalletClient::new("wallet4", wallet_proxy.tx.clone()); + let wallet4 = test_framework::create_wallet( + &format!("{}/wallet4", test_dir), + client4.clone(), + Some(seed_phrase), + ); + wallet_proxy.add_wallet("wallet4", client4.get_send_instance(), wallet4.clone()); + + // Simulate a recover from seed without restore into here + let client5 = LocalWalletClient::new("wallet5", wallet_proxy.tx.clone()); + let wallet5 = test_framework::create_wallet( + &format!("{}/wallet5", test_dir), + client5.clone(), + Some(seed_phrase), + ); + wallet_proxy.add_wallet("wallet5", client5.get_send_instance(), wallet5.clone()); + + //simulate a recover from seed without restore into here + let client6 = LocalWalletClient::new("wallet6", wallet_proxy.tx.clone()); + let wallet6 = test_framework::create_wallet( + &format!("{}/wallet6", test_dir), + client6.clone(), + Some(seed_phrase), + ); + wallet_proxy.add_wallet("wallet6", client6.get_send_instance(), wallet6.clone()); + + let client7 = LocalWalletClient::new("wallet7", wallet_proxy.tx.clone()); + let wallet7 = test_framework::create_wallet( + &format!("{}/wallet7", test_dir), + client7.clone(), + Some(seed_phrase), + ); + wallet_proxy.add_wallet("wallet7", client7.get_send_instance(), wallet7.clone()); + + let client8 = LocalWalletClient::new("wallet8", wallet_proxy.tx.clone()); + let wallet8 = test_framework::create_wallet( + &format!("{}/wallet8", test_dir), + client8.clone(), + Some(seed_phrase), + ); + wallet_proxy.add_wallet("wallet8", client8.get_send_instance(), wallet8.clone()); + + let client9 = LocalWalletClient::new("wallet9", wallet_proxy.tx.clone()); + let wallet9 = test_framework::create_wallet( + &format!("{}/wallet9", test_dir), + client9.clone(), + Some(seed_phrase), + ); + wallet_proxy.add_wallet("wallet9", client9.get_send_instance(), wallet9.clone()); + + let client10 = LocalWalletClient::new("wallet10", wallet_proxy.tx.clone()); + let wallet10 = test_framework::create_wallet( + &format!("{}/wallet10", test_dir), + client10.clone(), + Some(seed_phrase), + ); + wallet_proxy.add_wallet("wallet10", client10.get_send_instance(), wallet10.clone()); + + // Set the wallet proxy listener running + thread::spawn(move || { + if let Err(e) = wallet_proxy.run() { + error!("Wallet Proxy error: {}", e); + } + }); + + // few values to keep things shorter + let _reward = core::consensus::REWARD; + let cm = global::coinbase_maturity() as usize; // assume all testing precedes soft fork height + + // Do some mining + let mut bh = 20u64; + let base_amount = consensus::GRIN_BASE; + let _ = test_framework::award_blocks_to_wallet(&chain, miner.clone(), bh as usize); + + // send some funds to wallets 1 + wallet::controller::owner_single_use(miner.clone(), |api| { + test_framework::send_to_dest(m_client.clone(), api, "wallet1", base_amount * 1)?; + test_framework::send_to_dest(m_client.clone(), api, "wallet1", base_amount * 2)?; + test_framework::send_to_dest(m_client.clone(), api, "wallet1", base_amount * 3)?; + bh += 3; + Ok(()) + })?; + + // 0) Check repair when all is okay should leave wallet contents alone + wallet::controller::owner_single_use(wallet1.clone(), |api| { + api.check_repair()?; + let info = test_framework::wallet_info(api)?; + assert_eq!(info.amount_currently_spendable, base_amount * 6); + assert_eq!(info.total, base_amount * 6); + Ok(()) + })?; + + // send some funds to wallet 2 + wallet::controller::owner_single_use(miner.clone(), |api| { + test_framework::send_to_dest(m_client.clone(), api, "wallet2", base_amount * 4)?; + test_framework::send_to_dest(m_client.clone(), api, "wallet2", base_amount * 5)?; + test_framework::send_to_dest(m_client.clone(), api, "wallet2", base_amount * 6)?; + bh += 3; + Ok(()) + })?; + + let _ = test_framework::award_blocks_to_wallet(&chain, miner.clone(), cm); + bh += cm as u64; + + // confirm balances + wallet::controller::owner_single_use(wallet1.clone(), |api| { + let info = test_framework::wallet_info(api)?; + assert_eq!(info.amount_currently_spendable, base_amount * 6); + assert_eq!(info.total, base_amount * 6); + Ok(()) + })?; + + wallet::controller::owner_single_use(wallet2.clone(), |api| { + let info = test_framework::wallet_info(api)?; + assert_eq!(info.amount_currently_spendable, base_amount * 15); + assert_eq!(info.total, base_amount * 15); + Ok(()) + })?; + + // Now there should be outputs on the chain using the same + // seed + BIP32 path. + + // 1) a full restore should recover all of them: + wallet::controller::owner_single_use(wallet3.clone(), |api| { + api.restore()?; + Ok(()) + })?; + + wallet::controller::owner_single_use(wallet3.clone(), |api| { + let info = test_framework::wallet_info(api)?; + let outputs = api.retrieve_outputs(true, false, None)?.1; + assert_eq!(outputs.len(), 6); + assert_eq!(info.amount_currently_spendable, base_amount * 21); + assert_eq!(info.total, base_amount * 21); + Ok(()) + })?; + + // 2) check_repair should recover them into a single wallet + wallet::controller::owner_single_use(wallet1.clone(), |api| { + api.check_repair()?; + Ok(()) + })?; + + wallet::controller::owner_single_use(wallet1.clone(), |api| { + let info = test_framework::wallet_info(api)?; + let outputs = api.retrieve_outputs(true, false, None)?.1; + assert_eq!(outputs.len(), 6); + assert_eq!(info.amount_currently_spendable, base_amount * 21); + Ok(()) + })?; + + // 3) If I recover from seed and start using the wallet without restoring, + // check_repair should restore the older outputs + wallet::controller::owner_single_use(miner.clone(), |api| { + test_framework::send_to_dest(m_client.clone(), api, "wallet4", base_amount * 7)?; + test_framework::send_to_dest(m_client.clone(), api, "wallet4", base_amount * 8)?; + test_framework::send_to_dest(m_client.clone(), api, "wallet4", base_amount * 9)?; + bh += 3; + Ok(()) + })?; + let _ = test_framework::award_blocks_to_wallet(&chain, miner.clone(), cm); + bh += cm as u64; + + wallet::controller::owner_single_use(wallet4.clone(), |api| { + let info = test_framework::wallet_info(api)?; + let outputs = api.retrieve_outputs(true, false, None)?.1; + assert_eq!(outputs.len(), 3); + assert_eq!(info.amount_currently_spendable, base_amount * 24); + Ok(()) + })?; + + wallet::controller::owner_single_use(wallet5.clone(), |api| { + api.restore()?; + Ok(()) + })?; + + wallet::controller::owner_single_use(wallet5.clone(), |api| { + let info = test_framework::wallet_info(api)?; + let outputs = api.retrieve_outputs(true, false, None)?.1; + assert_eq!(outputs.len(), 9); + assert_eq!(info.amount_currently_spendable, base_amount * (45)); + Ok(()) + })?; + + // 4) If I recover from seed and start using the wallet without restoring, + // check_repair should restore the older outputs + wallet::controller::owner_single_use(miner.clone(), |api| { + test_framework::send_to_dest(m_client.clone(), api, "wallet6", base_amount * 10)?; + test_framework::send_to_dest(m_client.clone(), api, "wallet6", base_amount * 11)?; + test_framework::send_to_dest(m_client.clone(), api, "wallet6", base_amount * 12)?; + bh += 3; + Ok(()) + })?; + let _ = test_framework::award_blocks_to_wallet(&chain, miner.clone(), cm as usize); + bh += cm as u64; + + wallet::controller::owner_single_use(wallet6.clone(), |api| { + let info = test_framework::wallet_info(api)?; + let outputs = api.retrieve_outputs(true, false, None)?.1; + assert_eq!(outputs.len(), 3); + assert_eq!(info.amount_currently_spendable, base_amount * 33); + Ok(()) + })?; + + wallet::controller::owner_single_use(wallet6.clone(), |api| { + api.check_repair()?; + Ok(()) + })?; + + wallet::controller::owner_single_use(wallet6.clone(), |api| { + let info = test_framework::wallet_info(api)?; + let outputs = api.retrieve_outputs(true, false, None)?.1; + assert_eq!(outputs.len(), 12); + assert_eq!(info.amount_currently_spendable, base_amount * (78)); + Ok(()) + })?; + + // 5) Start using same seed with a different account, amounts should + // be distinct and restore should return funds from other account + + wallet::controller::owner_single_use(miner.clone(), |api| { + test_framework::send_to_dest(m_client.clone(), api, "wallet7", base_amount * 13)?; + test_framework::send_to_dest(m_client.clone(), api, "wallet7", base_amount * 14)?; + test_framework::send_to_dest(m_client.clone(), api, "wallet7", base_amount * 15)?; + bh += 3; + Ok(()) + })?; + + // mix it up a bit + wallet::controller::owner_single_use(wallet7.clone(), |api| { + api.create_account_path("account_1")?; + api.set_active_account("account_1")?; + Ok(()) + })?; + + wallet::controller::owner_single_use(miner.clone(), |api| { + test_framework::send_to_dest(m_client.clone(), api, "wallet7", base_amount * 1)?; + test_framework::send_to_dest(m_client.clone(), api, "wallet7", base_amount * 2)?; + test_framework::send_to_dest(m_client.clone(), api, "wallet7", base_amount * 3)?; + bh += 3; + Ok(()) + })?; + + // check balances + let _ = test_framework::award_blocks_to_wallet(&chain, miner.clone(), cm); + bh += cm as u64; + + wallet::controller::owner_single_use(wallet7.clone(), |api| { + let info = test_framework::wallet_info(api)?; + let outputs = api.retrieve_outputs(true, false, None)?.1; + assert_eq!(outputs.len(), 3); + assert_eq!(info.amount_currently_spendable, base_amount * 6); + api.set_active_account("default")?; + let info = test_framework::wallet_info(api)?; + let outputs = api.retrieve_outputs(true, false, None)?.1; + assert_eq!(outputs.len(), 3); + assert_eq!(info.amount_currently_spendable, base_amount * 42); + Ok(()) + })?; + + wallet::controller::owner_single_use(wallet8.clone(), |api| { + api.restore()?; + let info = test_framework::wallet_info(api)?; + let outputs = api.retrieve_outputs(true, false, None)?.1; + assert_eq!(outputs.len(), 15); + assert_eq!(info.amount_currently_spendable, base_amount * 120); + api.set_active_account("account_1")?; + let info = test_framework::wallet_info(api)?; + let outputs = api.retrieve_outputs(true, false, None)?.1; + assert_eq!(outputs.len(), 3); + assert_eq!(info.amount_currently_spendable, base_amount * 6); + Ok(()) + })?; + + // 6) Start using same seed with a different account, now overwriting + // ids on account 2 as well, check_repair should get all outputs created + // to now into 2 accounts + + wallet::controller::owner_single_use(wallet9.clone(), |api| { + api.create_account_path("account_1")?; + api.set_active_account("account_1")?; + Ok(()) + })?; + wallet::controller::owner_single_use(miner.clone(), |api| { + test_framework::send_to_dest(m_client.clone(), api, "wallet9", base_amount * 4)?; + test_framework::send_to_dest(m_client.clone(), api, "wallet9", base_amount * 5)?; + test_framework::send_to_dest(m_client.clone(), api, "wallet9", base_amount * 6)?; + bh += 3; + Ok(()) + })?; + + wallet::controller::owner_single_use(wallet9.clone(), |api| { + let info = test_framework::wallet_info(api)?; + let outputs = api.retrieve_outputs(true, false, None)?.1; + assert_eq!(outputs.len(), 3); + assert_eq!(info.amount_currently_spendable, base_amount * 15); + api.check_repair()?; + let info = test_framework::wallet_info(api)?; + let outputs = api.retrieve_outputs(true, false, None)?.1; + assert_eq!(outputs.len(), 6); + assert_eq!(info.amount_currently_spendable, base_amount * 21); + + api.set_active_account("default")?; + let info = test_framework::wallet_info(api)?; + let outputs = api.retrieve_outputs(true, false, None)?.1; + assert_eq!(outputs.len(), 15); + assert_eq!(info.amount_currently_spendable, base_amount * 120); + Ok(()) + })?; + + let _ = test_framework::award_blocks_to_wallet(&chain, miner.clone(), cm); + + // 7) Ensure check_repair creates missing accounts + wallet::controller::owner_single_use(wallet10.clone(), |api| { + api.check_repair()?; + api.set_active_account("account_1")?; + let info = test_framework::wallet_info(api)?; + let outputs = api.retrieve_outputs(true, false, None)?.1; + assert_eq!(outputs.len(), 6); + assert_eq!(info.amount_currently_spendable, base_amount * 21); + + api.set_active_account("default")?; + let info = test_framework::wallet_info(api)?; + let outputs = api.retrieve_outputs(true, false, None)?.1; + assert_eq!(outputs.len(), 15); + assert_eq!(info.amount_currently_spendable, base_amount * 120); + Ok(()) + })?; + + // let logging finish + thread::sleep(Duration::from_millis(200)); + Ok(()) +} +#[test] +fn check_repair() { + let test_dir = "test_output/check_repair"; + if let Err(e) = check_repair_impl(test_dir) { + panic!("Libwallet Error: {} - {}", e, e.backtrace().unwrap()); + } +} + +#[test] +fn two_wallets_one_seed() { + let test_dir = "test_output/two_wallets_one_seed"; + if let Err(e) = two_wallets_one_seed_impl(test_dir) { + panic!("Libwallet Error: {} - {}", e, e.backtrace().unwrap()); + } +} diff --git a/tests/file.rs b/tests/file.rs new file mode 100644 index 0000000..925f7cc --- /dev/null +++ b/tests/file.rs @@ -0,0 +1,220 @@ +// Copyright 2018 The Grin Developers +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Test a wallet file send/recieve +#[macro_use] +extern crate log; +use self::core::global; +use self::core::global::ChainTypes; +use self::keychain::ExtKeychain; +use self::wallet::test_framework::{self, LocalWalletClient, WalletProxy}; +use self::wallet::{libwallet, FileWalletCommAdapter}; +use grin_core as core; +use grin_keychain as keychain; +use grin_util as util; +use grin_wallet as wallet; +use std::fs; +use std::thread; +use std::time::Duration; + +use serde_json; + +fn clean_output_dir(test_dir: &str) { + let _ = fs::remove_dir_all(test_dir); +} + +fn setup(test_dir: &str) { + util::init_test_logger(); + clean_output_dir(test_dir); + global::set_mining_mode(ChainTypes::AutomatedTesting); +} + +/// self send impl +fn file_exchange_test_impl(test_dir: &str) -> Result<(), libwallet::Error> { + setup(test_dir); + // Create a new proxy to simulate server and wallet responses + let mut wallet_proxy: WalletProxy = WalletProxy::new(test_dir); + let chain = wallet_proxy.chain.clone(); + + let client1 = LocalWalletClient::new("wallet1", wallet_proxy.tx.clone()); + let wallet1 = + test_framework::create_wallet(&format!("{}/wallet1", test_dir), client1.clone(), None); + wallet_proxy.add_wallet("wallet1", client1.get_send_instance(), wallet1.clone()); + + let client2 = LocalWalletClient::new("wallet2", wallet_proxy.tx.clone()); + let wallet2 = + test_framework::create_wallet(&format!("{}/wallet2", test_dir), client2.clone(), None); + wallet_proxy.add_wallet("wallet2", client2.get_send_instance(), wallet2.clone()); + + // Set the wallet proxy listener running + thread::spawn(move || { + if let Err(e) = wallet_proxy.run() { + error!("Wallet Proxy error: {}", e); + } + }); + + // few values to keep things shorter + let reward = core::consensus::REWARD; + + // add some accounts + wallet::controller::owner_single_use(wallet1.clone(), |api| { + api.create_account_path("mining")?; + api.create_account_path("listener")?; + Ok(()) + })?; + + // add some accounts + wallet::controller::owner_single_use(wallet2.clone(), |api| { + api.create_account_path("account1")?; + api.create_account_path("account2")?; + Ok(()) + })?; + + // Get some mining done + { + let mut w = wallet1.lock(); + w.set_parent_key_id_by_name("mining")?; + } + let mut bh = 10u64; + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), bh as usize); + + let send_file = format!("{}/part_tx_1.tx", test_dir); + let receive_file = format!("{}/part_tx_2.tx", test_dir); + + // test optional message + let message = "sender test message, sender test message"; + + // Should have 5 in account1 (5 spendable), 5 in account (2 spendable) + wallet::controller::owner_single_use(wallet1.clone(), |api| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(true, 1)?; + assert!(wallet1_refreshed); + assert_eq!(wallet1_info.last_confirmed_height, bh); + assert_eq!(wallet1_info.total, bh * reward); + // send to send + let (mut slate, lock_fn) = api.initiate_tx( + Some("mining"), + reward * 2, // amount + 2, // minimum confirmations + 500, // max outputs + 1, // num change outputs + true, // select all outputs + Some(message.to_owned()), // optional message + )?; + // output tx file + let file_adapter = FileWalletCommAdapter::new(); + file_adapter.send_tx_async(&send_file, &mut slate)?; + api.tx_lock_outputs(&slate, lock_fn)?; + Ok(()) + })?; + + // Get some mining done + { + let mut w = wallet2.lock(); + w.set_parent_key_id_by_name("account1")?; + } + + let adapter = FileWalletCommAdapter::new(); + let mut slate = adapter.receive_tx_async(&send_file)?; + let mut naughty_slate = slate.clone(); + naughty_slate.participant_data[0].message = Some("I changed the message".to_owned()); + + // verify messages on slate match + wallet::controller::owner_single_use(wallet1.clone(), |api| { + api.verify_slate_messages(&slate)?; + assert!(api.verify_slate_messages(&naughty_slate).is_err()); + Ok(()) + })?; + + let sender2_message = "And this is sender 2's message".to_owned(); + + // wallet 2 receives file, completes, sends file back + wallet::controller::foreign_single_use(wallet2.clone(), |api| { + api.receive_tx(&mut slate, None, Some(sender2_message.clone()))?; + adapter.send_tx_async(&receive_file, &mut slate)?; + Ok(()) + })?; + + // wallet 1 finalises and posts + wallet::controller::owner_single_use(wallet1.clone(), |api| { + let adapter = FileWalletCommAdapter::new(); + let mut slate = adapter.receive_tx_async(&receive_file)?; + api.verify_slate_messages(&slate)?; + api.finalize_tx(&mut slate)?; + api.post_tx(&slate.tx, false)?; + bh += 1; + Ok(()) + })?; + + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), 3); + bh += 3; + + // Check total in mining account + wallet::controller::owner_single_use(wallet1.clone(), |api| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(true, 1)?; + assert!(wallet1_refreshed); + assert_eq!(wallet1_info.last_confirmed_height, bh); + assert_eq!(wallet1_info.total, bh * reward - reward * 2); + Ok(()) + })?; + + // Check total in 'wallet 2' account + wallet::controller::owner_single_use(wallet2.clone(), |api| { + let (wallet2_refreshed, wallet2_info) = api.retrieve_summary_info(true, 1)?; + assert!(wallet2_refreshed); + assert_eq!(wallet2_info.last_confirmed_height, bh); + assert_eq!(wallet2_info.total, 2 * reward); + Ok(()) + })?; + + // Check messages, all participants should have both + wallet::controller::owner_single_use(wallet1.clone(), |api| { + let (_, tx) = api.retrieve_txs(true, None, Some(slate.id))?; + assert_eq!( + tx[0].clone().messages.unwrap().messages[0].message, + Some(message.to_owned()) + ); + assert_eq!( + tx[0].clone().messages.unwrap().messages[1].message, + Some(sender2_message.to_owned()) + ); + + let msg_json = serde_json::to_string_pretty(&tx[0].clone().messages.unwrap()).unwrap(); + println!("{}", msg_json); + Ok(()) + })?; + + wallet::controller::owner_single_use(wallet2.clone(), |api| { + let (_, tx) = api.retrieve_txs(true, None, Some(slate.id))?; + assert_eq!( + tx[0].clone().messages.unwrap().messages[0].message, + Some(message.to_owned()) + ); + assert_eq!( + tx[0].clone().messages.unwrap().messages[1].message, + Some(sender2_message) + ); + Ok(()) + })?; + + // let logging finish + thread::sleep(Duration::from_millis(200)); + Ok(()) +} + +#[test] +fn wallet_file_exchange() { + let test_dir = "test_output/file_exchange"; + if let Err(e) = file_exchange_test_impl(test_dir) { + panic!("Libwallet Error: {} - {}", e, e.backtrace().unwrap()); + } +} diff --git a/tests/libwallet.rs b/tests/libwallet.rs new file mode 100644 index 0000000..44447f5 --- /dev/null +++ b/tests/libwallet.rs @@ -0,0 +1,521 @@ +// Copyright 2018 The Grin Developers +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! core::libtx specific tests +use self::core::core::transaction; +use self::core::libtx::{aggsig, proof}; +use self::keychain::{BlindSum, BlindingFactor, ExtKeychain, Keychain}; +use self::util::secp; +use self::util::secp::key::{PublicKey, SecretKey}; +use self::wallet::libwallet::types::Context; +use self::wallet::{EncryptedWalletSeed, WalletSeed}; +use grin_core as core; +use grin_keychain as keychain; +use grin_util as util; +use grin_wallet as wallet; +use rand::thread_rng; + +fn kernel_sig_msg() -> secp::Message { + transaction::kernel_sig_msg(0, 0, transaction::KernelFeatures::Plain).unwrap() +} + +#[test] +fn aggsig_sender_receiver_interaction() { + let sender_keychain = ExtKeychain::from_random_seed(true).unwrap(); + let receiver_keychain = ExtKeychain::from_random_seed(true).unwrap(); + + // Calculate the kernel excess here for convenience. + // Normally this would happen during transaction building. + let kernel_excess = { + let id1 = ExtKeychain::derive_key_id(1, 1, 0, 0, 0); + let skey1 = sender_keychain.derive_key(0, &id1).unwrap(); + let skey2 = receiver_keychain.derive_key(0, &id1).unwrap(); + + let keychain = ExtKeychain::from_random_seed(true).unwrap(); + let blinding_factor = keychain + .blind_sum( + &BlindSum::new() + .sub_blinding_factor(BlindingFactor::from_secret_key(skey1)) + .add_blinding_factor(BlindingFactor::from_secret_key(skey2)), + ) + .unwrap(); + + keychain + .secp() + .commit(0, blinding_factor.secret_key(&keychain.secp()).unwrap()) + .unwrap() + }; + + let s_cx; + let mut rx_cx; + // sender starts the tx interaction + let (sender_pub_excess, _sender_pub_nonce) = { + let keychain = sender_keychain.clone(); + let id1 = ExtKeychain::derive_key_id(1, 1, 0, 0, 0); + let skey = keychain.derive_key(0, &id1).unwrap(); + + // dealing with an input here so we need to negate the blinding_factor + // rather than use it as is + let bs = BlindSum::new(); + let blinding_factor = keychain + .blind_sum(&bs.sub_blinding_factor(BlindingFactor::from_secret_key(skey))) + .unwrap(); + + let blind = blinding_factor.secret_key(&keychain.secp()).unwrap(); + + s_cx = Context::new(&keychain.secp(), blind); + s_cx.get_public_keys(&keychain.secp()) + }; + + let pub_nonce_sum; + let pub_key_sum; + // receiver receives partial tx + let (receiver_pub_excess, _receiver_pub_nonce, rx_sig_part) = { + let keychain = receiver_keychain.clone(); + let key_id = ExtKeychain::derive_key_id(1, 1, 0, 0, 0); + + // let blind = blind_sum.secret_key(&keychain.secp())?; + let blind = keychain.derive_key(0, &key_id).unwrap(); + + rx_cx = Context::new(&keychain.secp(), blind); + let (pub_excess, pub_nonce) = rx_cx.get_public_keys(&keychain.secp()); + rx_cx.add_output(&key_id, &None); + + pub_nonce_sum = PublicKey::from_combination( + keychain.secp(), + vec![ + &s_cx.get_public_keys(keychain.secp()).1, + &rx_cx.get_public_keys(keychain.secp()).1, + ], + ) + .unwrap(); + + pub_key_sum = PublicKey::from_combination( + keychain.secp(), + vec![ + &s_cx.get_public_keys(keychain.secp()).0, + &rx_cx.get_public_keys(keychain.secp()).0, + ], + ) + .unwrap(); + + let msg = kernel_sig_msg(); + let sig_part = aggsig::calculate_partial_sig( + &keychain.secp(), + &rx_cx.sec_key, + &rx_cx.sec_nonce, + &pub_nonce_sum, + Some(&pub_key_sum), + &msg, + ) + .unwrap(); + (pub_excess, pub_nonce, sig_part) + }; + + // check the sender can verify the partial signature + // received in the response back from the receiver + { + let keychain = sender_keychain.clone(); + let msg = kernel_sig_msg(); + let sig_verifies = aggsig::verify_partial_sig( + &keychain.secp(), + &rx_sig_part, + &pub_nonce_sum, + &receiver_pub_excess, + Some(&pub_key_sum), + &msg, + ); + assert!(!sig_verifies.is_err()); + } + + // now sender signs with their key + let sender_sig_part = { + let keychain = sender_keychain.clone(); + let msg = kernel_sig_msg(); + let sig_part = aggsig::calculate_partial_sig( + &keychain.secp(), + &s_cx.sec_key, + &s_cx.sec_nonce, + &pub_nonce_sum, + Some(&pub_key_sum), + &msg, + ) + .unwrap(); + sig_part + }; + + // check the receiver can verify the partial signature + // received by the sender + { + let keychain = receiver_keychain.clone(); + let msg = kernel_sig_msg(); + let sig_verifies = aggsig::verify_partial_sig( + &keychain.secp(), + &sender_sig_part, + &pub_nonce_sum, + &sender_pub_excess, + Some(&pub_key_sum), + &msg, + ); + assert!(!sig_verifies.is_err()); + } + + // Receiver now builds final signature from sender and receiver parts + let (final_sig, final_pubkey) = { + let keychain = receiver_keychain.clone(); + + let msg = kernel_sig_msg(); + let our_sig_part = aggsig::calculate_partial_sig( + &keychain.secp(), + &rx_cx.sec_key, + &rx_cx.sec_nonce, + &pub_nonce_sum, + Some(&pub_key_sum), + &msg, + ) + .unwrap(); + + // Receiver now generates final signature from the two parts + let final_sig = aggsig::add_signatures( + &keychain.secp(), + vec![&sender_sig_part, &our_sig_part], + &pub_nonce_sum, + ) + .unwrap(); + + // Receiver calculates the final public key (to verify sig later) + let final_pubkey = PublicKey::from_combination( + keychain.secp(), + vec![ + &s_cx.get_public_keys(keychain.secp()).0, + &rx_cx.get_public_keys(keychain.secp()).0, + ], + ) + .unwrap(); + + (final_sig, final_pubkey) + }; + + // Receiver checks the final signature verifies + { + let keychain = receiver_keychain.clone(); + let msg = kernel_sig_msg(); + + // Receiver check the final signature verifies + let sig_verifies = aggsig::verify_completed_sig( + &keychain.secp(), + &final_sig, + &final_pubkey, + Some(&final_pubkey), + &msg, + ); + assert!(!sig_verifies.is_err()); + } + + // Check we can verify the sig using the kernel excess + { + let keychain = ExtKeychain::from_random_seed(true).unwrap(); + let msg = kernel_sig_msg(); + let sig_verifies = + aggsig::verify_single_from_commit(&keychain.secp(), &final_sig, &msg, &kernel_excess); + + assert!(!sig_verifies.is_err()); + } +} + +#[test] +fn aggsig_sender_receiver_interaction_offset() { + let sender_keychain = ExtKeychain::from_random_seed(true).unwrap(); + let receiver_keychain = ExtKeychain::from_random_seed(true).unwrap(); + + // This is the kernel offset that we use to split the key + // Summing these at the block level prevents the + // kernels from being used to reconstruct (or identify) individual transactions + let kernel_offset = SecretKey::new(&sender_keychain.secp(), &mut thread_rng()); + + // Calculate the kernel excess here for convenience. + // Normally this would happen during transaction building. + let kernel_excess = { + let id1 = ExtKeychain::derive_key_id(1, 1, 0, 0, 0); + let skey1 = sender_keychain.derive_key(0, &id1).unwrap(); + let skey2 = receiver_keychain.derive_key(0, &id1).unwrap(); + + let keychain = ExtKeychain::from_random_seed(true).unwrap(); + let blinding_factor = keychain + .blind_sum( + &BlindSum::new() + .sub_blinding_factor(BlindingFactor::from_secret_key(skey1)) + .add_blinding_factor(BlindingFactor::from_secret_key(skey2)) + // subtract the kernel offset here like as would when + // verifying a kernel signature + .sub_blinding_factor(BlindingFactor::from_secret_key(kernel_offset)), + ) + .unwrap(); + + keychain + .secp() + .commit(0, blinding_factor.secret_key(&keychain.secp()).unwrap()) + .unwrap() + }; + + let s_cx; + let mut rx_cx; + // sender starts the tx interaction + let (sender_pub_excess, _sender_pub_nonce) = { + let keychain = sender_keychain.clone(); + let id1 = ExtKeychain::derive_key_id(1, 1, 0, 0, 0); + let skey = keychain.derive_key(0, &id1).unwrap(); + + // dealing with an input here so we need to negate the blinding_factor + // rather than use it as is + let blinding_factor = keychain + .blind_sum( + &BlindSum::new() + .sub_blinding_factor(BlindingFactor::from_secret_key(skey)) + // subtract the kernel offset to create an aggsig context + // with our "split" key + .sub_blinding_factor(BlindingFactor::from_secret_key(kernel_offset)), + ) + .unwrap(); + + let blind = blinding_factor.secret_key(&keychain.secp()).unwrap(); + + s_cx = Context::new(&keychain.secp(), blind); + s_cx.get_public_keys(&keychain.secp()) + }; + + // receiver receives partial tx + let pub_nonce_sum; + let pub_key_sum; + let (receiver_pub_excess, _receiver_pub_nonce, sig_part) = { + let keychain = receiver_keychain.clone(); + let key_id = ExtKeychain::derive_key_id(1, 1, 0, 0, 0); + + let blind = keychain.derive_key(0, &key_id).unwrap(); + + rx_cx = Context::new(&keychain.secp(), blind); + let (pub_excess, pub_nonce) = rx_cx.get_public_keys(&keychain.secp()); + rx_cx.add_output(&key_id, &None); + + pub_nonce_sum = PublicKey::from_combination( + keychain.secp(), + vec![ + &s_cx.get_public_keys(keychain.secp()).1, + &rx_cx.get_public_keys(keychain.secp()).1, + ], + ) + .unwrap(); + + pub_key_sum = PublicKey::from_combination( + keychain.secp(), + vec![ + &s_cx.get_public_keys(keychain.secp()).0, + &rx_cx.get_public_keys(keychain.secp()).0, + ], + ) + .unwrap(); + + let msg = kernel_sig_msg(); + let sig_part = aggsig::calculate_partial_sig( + &keychain.secp(), + &rx_cx.sec_key, + &rx_cx.sec_nonce, + &pub_nonce_sum, + Some(&pub_key_sum), + &msg, + ) + .unwrap(); + (pub_excess, pub_nonce, sig_part) + }; + + // check the sender can verify the partial signature + // received in the response back from the receiver + { + let keychain = sender_keychain.clone(); + let msg = kernel_sig_msg(); + let sig_verifies = aggsig::verify_partial_sig( + &keychain.secp(), + &sig_part, + &pub_nonce_sum, + &receiver_pub_excess, + Some(&pub_key_sum), + &msg, + ); + assert!(!sig_verifies.is_err()); + } + + // now sender signs with their key + let sender_sig_part = { + let keychain = sender_keychain.clone(); + let msg = kernel_sig_msg(); + let sig_part = aggsig::calculate_partial_sig( + &keychain.secp(), + &s_cx.sec_key, + &s_cx.sec_nonce, + &pub_nonce_sum, + Some(&pub_key_sum), + &msg, + ) + .unwrap(); + sig_part + }; + + // check the receiver can verify the partial signature + // received by the sender + { + let keychain = receiver_keychain.clone(); + let msg = kernel_sig_msg(); + let sig_verifies = aggsig::verify_partial_sig( + &keychain.secp(), + &sender_sig_part, + &pub_nonce_sum, + &sender_pub_excess, + Some(&pub_key_sum), + &msg, + ); + assert!(!sig_verifies.is_err()); + } + + // Receiver now builds final signature from sender and receiver parts + let (final_sig, final_pubkey) = { + let keychain = receiver_keychain.clone(); + let msg = kernel_sig_msg(); + let our_sig_part = aggsig::calculate_partial_sig( + &keychain.secp(), + &rx_cx.sec_key, + &rx_cx.sec_nonce, + &pub_nonce_sum, + Some(&pub_key_sum), + &msg, + ) + .unwrap(); + + // Receiver now generates final signature from the two parts + let final_sig = aggsig::add_signatures( + &keychain.secp(), + vec![&sender_sig_part, &our_sig_part], + &pub_nonce_sum, + ) + .unwrap(); + + // Receiver calculates the final public key (to verify sig later) + let final_pubkey = PublicKey::from_combination( + keychain.secp(), + vec![ + &s_cx.get_public_keys(keychain.secp()).0, + &rx_cx.get_public_keys(keychain.secp()).0, + ], + ) + .unwrap(); + + (final_sig, final_pubkey) + }; + + // Receiver checks the final signature verifies + { + let keychain = receiver_keychain.clone(); + let msg = kernel_sig_msg(); + + // Receiver check the final signature verifies + let sig_verifies = aggsig::verify_completed_sig( + &keychain.secp(), + &final_sig, + &final_pubkey, + Some(&final_pubkey), + &msg, + ); + assert!(!sig_verifies.is_err()); + } + + // Check we can verify the sig using the kernel excess + { + let keychain = ExtKeychain::from_random_seed(true).unwrap(); + let msg = kernel_sig_msg(); + let sig_verifies = + aggsig::verify_single_from_commit(&keychain.secp(), &final_sig, &msg, &kernel_excess); + + assert!(!sig_verifies.is_err()); + } +} + +#[test] +fn test_rewind_range_proof() { + let keychain = ExtKeychain::from_random_seed(true).unwrap(); + let key_id = ExtKeychain::derive_key_id(1, 1, 0, 0, 0); + let key_id2 = ExtKeychain::derive_key_id(1, 2, 0, 0, 0); + let commit = keychain.commit(5, &key_id).unwrap(); + let extra_data = [99u8; 64]; + + let proof = proof::create( + &keychain, + 5, + &key_id, + commit, + Some(extra_data.to_vec().clone()), + ) + .unwrap(); + let proof_info = + proof::rewind(&keychain, commit, Some(extra_data.to_vec().clone()), proof).unwrap(); + + assert_eq!(proof_info.success, true); + assert_eq!(proof_info.value, 5); + assert_eq!(proof_info.message.as_bytes(), key_id.serialize_path()); + + // cannot rewind with a different commit + let commit2 = keychain.commit(5, &key_id2).unwrap(); + let proof_info = + proof::rewind(&keychain, commit2, Some(extra_data.to_vec().clone()), proof).unwrap(); + assert_eq!(proof_info.success, false); + assert_eq!(proof_info.value, 0); + assert_eq!(proof_info.message, secp::pedersen::ProofMessage::empty()); + + // cannot rewind with a commitment to a different value + let commit3 = keychain.commit(4, &key_id).unwrap(); + let proof_info = + proof::rewind(&keychain, commit3, Some(extra_data.to_vec().clone()), proof).unwrap(); + assert_eq!(proof_info.success, false); + assert_eq!(proof_info.value, 0); + + // cannot rewind with wrong extra committed data + let commit3 = keychain.commit(4, &key_id).unwrap(); + let wrong_extra_data = [98u8; 64]; + let _should_err = proof::rewind( + &keychain, + commit3, + Some(wrong_extra_data.to_vec().clone()), + proof, + ) + .unwrap(); + + assert_eq!(proof_info.success, false); + assert_eq!(proof_info.value, 0); +} + +#[test] +fn wallet_seed_encrypt() { + let password = "passwoid"; + let wallet_seed = WalletSeed::init_new(32); + let mut enc_wallet_seed = EncryptedWalletSeed::from_seed(&wallet_seed, password).unwrap(); + println!("EWS: {:?}", enc_wallet_seed); + let decrypted_wallet_seed = enc_wallet_seed.decrypt(password).unwrap(); + assert_eq!(wallet_seed, decrypted_wallet_seed); + + // Wrong password + let decrypted_wallet_seed = enc_wallet_seed.decrypt(""); + assert!(decrypted_wallet_seed.is_err()); + + // Wrong nonce + enc_wallet_seed.nonce = "wrongnonce".to_owned(); + let decrypted_wallet_seed = enc_wallet_seed.decrypt(password); + assert!(decrypted_wallet_seed.is_err()); +} diff --git a/tests/repost.rs b/tests/repost.rs new file mode 100644 index 0000000..e24cd6f --- /dev/null +++ b/tests/repost.rs @@ -0,0 +1,257 @@ +// Copyright 2018 The Grin Developers +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Test a wallet repost command +#[macro_use] +extern crate log; + +use self::core::global; +use self::core::global::ChainTypes; +use self::keychain::ExtKeychain; +use self::libwallet::slate::Slate; +use self::wallet::test_framework::{self, LocalWalletClient, WalletProxy}; +use self::wallet::{libwallet, FileWalletCommAdapter}; +use grin_core as core; +use grin_keychain as keychain; +use grin_util as util; +use grin_wallet as wallet; +use std::fs; +use std::thread; +use std::time::Duration; + +fn clean_output_dir(test_dir: &str) { + let _ = fs::remove_dir_all(test_dir); +} + +fn setup(test_dir: &str) { + util::init_test_logger(); + clean_output_dir(test_dir); + global::set_mining_mode(ChainTypes::AutomatedTesting); +} + +/// self send impl +fn file_repost_test_impl(test_dir: &str) -> Result<(), libwallet::Error> { + setup(test_dir); + // Create a new proxy to simulate server and wallet responses + let mut wallet_proxy: WalletProxy = WalletProxy::new(test_dir); + let chain = wallet_proxy.chain.clone(); + + let client1 = LocalWalletClient::new("wallet1", wallet_proxy.tx.clone()); + let wallet1 = + test_framework::create_wallet(&format!("{}/wallet1", test_dir), client1.clone(), None); + wallet_proxy.add_wallet("wallet1", client1.get_send_instance(), wallet1.clone()); + + let client2 = LocalWalletClient::new("wallet2", wallet_proxy.tx.clone()); + let wallet2 = + test_framework::create_wallet(&format!("{}/wallet2", test_dir), client2.clone(), None); + wallet_proxy.add_wallet("wallet2", client2.get_send_instance(), wallet2.clone()); + + // Set the wallet proxy listener running + thread::spawn(move || { + if let Err(e) = wallet_proxy.run() { + error!("Wallet Proxy error: {}", e); + } + }); + + // few values to keep things shorter + let reward = core::consensus::REWARD; + + // add some accounts + wallet::controller::owner_single_use(wallet1.clone(), |api| { + api.create_account_path("mining")?; + api.create_account_path("listener")?; + Ok(()) + })?; + + // add some accounts + wallet::controller::owner_single_use(wallet2.clone(), |api| { + api.create_account_path("account1")?; + api.create_account_path("account2")?; + Ok(()) + })?; + + // Get some mining done + { + let mut w = wallet1.lock(); + w.set_parent_key_id_by_name("mining")?; + } + let mut bh = 10u64; + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), bh as usize); + + let send_file = format!("{}/part_tx_1.tx", test_dir); + let receive_file = format!("{}/part_tx_2.tx", test_dir); + + let mut slate = Slate::blank(2); + + // Should have 5 in account1 (5 spendable), 5 in account (2 spendable) + wallet::controller::owner_single_use(wallet1.clone(), |api| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(true, 1)?; + assert!(wallet1_refreshed); + assert_eq!(wallet1_info.last_confirmed_height, bh); + assert_eq!(wallet1_info.total, bh * reward); + // send to send + let (mut slate, lock_fn) = api.initiate_tx( + Some("mining"), + reward * 2, // amount + 2, // minimum confirmations + 500, // max outputs + 1, // num change outputs + true, // select all outputs + None, + )?; + // output tx file + let file_adapter = FileWalletCommAdapter::new(); + file_adapter.send_tx_async(&send_file, &mut slate)?; + api.tx_lock_outputs(&slate, lock_fn)?; + Ok(()) + })?; + + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), 3); + bh += 3; + + // wallet 1 receives file to different account, completes + { + let mut w = wallet1.lock(); + w.set_parent_key_id_by_name("listener")?; + } + + wallet::controller::foreign_single_use(wallet1.clone(), |api| { + let adapter = FileWalletCommAdapter::new(); + slate = adapter.receive_tx_async(&send_file)?; + api.receive_tx(&mut slate, None, None)?; + adapter.send_tx_async(&receive_file, &mut slate)?; + Ok(()) + })?; + + // wallet 1 receives file to different account, completes + { + let mut w = wallet1.lock(); + w.set_parent_key_id_by_name("mining")?; + } + + // wallet 1 finalize + wallet::controller::owner_single_use(wallet1.clone(), |api| { + let adapter = FileWalletCommAdapter::new(); + slate = adapter.receive_tx_async(&receive_file)?; + api.finalize_tx(&mut slate)?; + Ok(()) + })?; + + // Now repost from cached + wallet::controller::owner_single_use(wallet1.clone(), |api| { + let (_, txs) = api.retrieve_txs(true, None, Some(slate.id))?; + let stored_tx = api.get_stored_tx(&txs[0])?; + api.post_tx(&stored_tx.unwrap(), false)?; + bh += 1; + Ok(()) + })?; + + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), 3); + bh += 3; + + // update/test contents of both accounts + wallet::controller::owner_single_use(wallet1.clone(), |api| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(true, 1)?; + assert!(wallet1_refreshed); + assert_eq!(wallet1_info.last_confirmed_height, bh); + assert_eq!(wallet1_info.total, bh * reward - reward * 2); + Ok(()) + })?; + + { + let mut w = wallet1.lock(); + w.set_parent_key_id_by_name("listener")?; + } + + wallet::controller::owner_single_use(wallet1.clone(), |api| { + let (wallet2_refreshed, wallet2_info) = api.retrieve_summary_info(true, 1)?; + assert!(wallet2_refreshed); + assert_eq!(wallet2_info.last_confirmed_height, bh); + assert_eq!(wallet2_info.total, 2 * reward); + Ok(()) + })?; + + // as above, but syncronously + { + let mut w = wallet1.lock(); + w.set_parent_key_id_by_name("mining")?; + } + { + let mut w = wallet2.lock(); + w.set_parent_key_id_by_name("account1")?; + } + + let mut slate = Slate::blank(2); + let amount = 60_000_000_000; + + wallet::controller::owner_single_use(wallet1.clone(), |sender_api| { + // note this will increment the block count as part of the transaction "Posting" + let (slate_i, lock_fn) = sender_api.initiate_tx( + None, + amount * 2, // amount + 2, // minimum confirmations + 500, // max outputs + 1, // num change outputs + true, // select all outputs + None, + )?; + slate = client1.send_tx_slate_direct("wallet2", &slate_i)?; + sender_api.tx_lock_outputs(&slate, lock_fn)?; + sender_api.finalize_tx(&mut slate)?; + Ok(()) + })?; + + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), 3); + bh += 3; + + // Now repost from cached + wallet::controller::owner_single_use(wallet1.clone(), |api| { + let (_, txs) = api.retrieve_txs(true, None, Some(slate.id))?; + let stored_tx = api.get_stored_tx(&txs[0])?; + api.post_tx(&stored_tx.unwrap(), false)?; + bh += 1; + Ok(()) + })?; + + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), 3); + bh += 3; + // + // update/test contents of both accounts + wallet::controller::owner_single_use(wallet1.clone(), |api| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(true, 1)?; + assert!(wallet1_refreshed); + assert_eq!(wallet1_info.last_confirmed_height, bh); + assert_eq!(wallet1_info.total, bh * reward - reward * 4); + Ok(()) + })?; + + wallet::controller::owner_single_use(wallet2.clone(), |api| { + let (wallet2_refreshed, wallet2_info) = api.retrieve_summary_info(true, 1)?; + assert!(wallet2_refreshed); + assert_eq!(wallet2_info.last_confirmed_height, bh); + assert_eq!(wallet2_info.total, 2 * amount); + Ok(()) + })?; + + // let logging finish + thread::sleep(Duration::from_millis(200)); + Ok(()) +} + +#[test] +fn wallet_file_repost() { + let test_dir = "test_output/file_repost"; + if let Err(e) = file_repost_test_impl(test_dir) { + panic!("Libwallet Error: {} - {}", e, e.backtrace().unwrap()); + } +} diff --git a/tests/restore.rs b/tests/restore.rs new file mode 100644 index 0000000..9f15a72 --- /dev/null +++ b/tests/restore.rs @@ -0,0 +1,389 @@ +// Copyright 2018 The Grin Developers +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! tests for wallet restore +#[macro_use] +extern crate log; +use self::core::global; +use self::core::global::ChainTypes; +use self::keychain::{ExtKeychain, Identifier, Keychain}; +use self::libwallet::slate::Slate; +use self::wallet::libwallet; +use self::wallet::libwallet::types::AcctPathMapping; +use self::wallet::test_framework::{self, LocalWalletClient, WalletProxy}; +use grin_core as core; +use grin_keychain as keychain; +use grin_util as util; +use grin_wallet as wallet; +use std::fs; +use std::sync::atomic::Ordering; +use std::thread; +use std::time::Duration; + +fn clean_output_dir(test_dir: &str) { + let _ = fs::remove_dir_all(test_dir); +} + +fn setup(test_dir: &str) { + util::init_test_logger(); + clean_output_dir(test_dir); + global::set_mining_mode(ChainTypes::AutomatedTesting); +} + +fn restore_wallet(base_dir: &str, wallet_dir: &str) -> Result<(), libwallet::Error> { + let source_seed = format!("{}/{}/wallet.seed", base_dir, wallet_dir); + let dest_dir = format!("{}/{}_restore", base_dir, wallet_dir); + fs::create_dir_all(dest_dir.clone())?; + let dest_seed = format!("{}/wallet.seed", dest_dir); + println!("Source: {}, Dest: {}", source_seed, dest_seed); + fs::copy(source_seed, dest_seed)?; + + let mut wallet_proxy: WalletProxy = WalletProxy::new(base_dir); + let client = LocalWalletClient::new(wallet_dir, wallet_proxy.tx.clone()); + + let wallet = test_framework::create_wallet(&dest_dir, client.clone(), None); + + wallet_proxy.add_wallet(wallet_dir, client.get_send_instance(), wallet.clone()); + + // Set the wallet proxy listener running + let wp_running = wallet_proxy.running.clone(); + thread::spawn(move || { + if let Err(e) = wallet_proxy.run() { + error!("Wallet Proxy error: {}", e); + } + }); + + // perform the restore and update wallet info + wallet::controller::owner_single_use(wallet.clone(), |api| { + let _ = api.restore()?; + let _ = api.retrieve_summary_info(true, 1)?; + Ok(()) + })?; + + wp_running.store(false, Ordering::Relaxed); + //thread::sleep(Duration::from_millis(1000)); + + Ok(()) +} + +fn compare_wallet_restore( + base_dir: &str, + wallet_dir: &str, + account_path: &Identifier, +) -> Result<(), libwallet::Error> { + let restore_name = format!("{}_restore", wallet_dir); + let source_dir = format!("{}/{}", base_dir, wallet_dir); + let dest_dir = format!("{}/{}", base_dir, restore_name); + + let mut wallet_proxy: WalletProxy = WalletProxy::new(base_dir); + + let client = LocalWalletClient::new(wallet_dir, wallet_proxy.tx.clone()); + let wallet_source = test_framework::create_wallet(&source_dir, client.clone(), None); + wallet_proxy.add_wallet( + &wallet_dir, + client.get_send_instance(), + wallet_source.clone(), + ); + + let client = LocalWalletClient::new(&restore_name, wallet_proxy.tx.clone()); + let wallet_dest = test_framework::create_wallet(&dest_dir, client.clone(), None); + wallet_proxy.add_wallet( + &restore_name, + client.get_send_instance(), + wallet_dest.clone(), + ); + + { + let mut w = wallet_source.lock(); + w.set_parent_key_id(account_path.clone()); + } + + { + let mut w = wallet_dest.lock(); + w.set_parent_key_id(account_path.clone()); + } + + // Set the wallet proxy listener running + let wp_running = wallet_proxy.running.clone(); + thread::spawn(move || { + if let Err(e) = wallet_proxy.run() { + error!("Wallet Proxy error: {}", e); + } + }); + + let mut src_info: Option = None; + let mut dest_info: Option = None; + + let mut src_txs: Option> = None; + let mut dest_txs: Option> = None; + + let mut src_accts: Option> = None; + let mut dest_accts: Option> = None; + + // Overall wallet info should be the same + wallet::controller::owner_single_use(wallet_source.clone(), |api| { + src_info = Some(api.retrieve_summary_info(true, 1)?.1); + src_txs = Some(api.retrieve_txs(true, None, None)?.1); + src_accts = Some(api.accounts()?); + Ok(()) + })?; + + wallet::controller::owner_single_use(wallet_dest.clone(), |api| { + dest_info = Some(api.retrieve_summary_info(true, 1)?.1); + dest_txs = Some(api.retrieve_txs(true, None, None)?.1); + dest_accts = Some(api.accounts()?); + Ok(()) + })?; + + // Info should all be the same + assert_eq!(src_info, dest_info); + + // Net differences in TX logs should be the same + let src_sum: i64 = src_txs + .clone() + .unwrap() + .iter() + .map(|t| t.amount_credited as i64 - t.amount_debited as i64) + .sum(); + + let dest_sum: i64 = dest_txs + .clone() + .unwrap() + .iter() + .map(|t| t.amount_credited as i64 - t.amount_debited as i64) + .sum(); + + assert_eq!(src_sum, dest_sum); + + // Number of created accounts should be the same + assert_eq!( + src_accts.as_ref().unwrap().len(), + dest_accts.as_ref().unwrap().len() + ); + + wp_running.store(false, Ordering::Relaxed); + //thread::sleep(Duration::from_millis(1000)); + + Ok(()) +} + +/// Build up 2 wallets, perform a few transactions on them +/// Then attempt to restore them in separate directories and check contents are the same +fn setup_restore(test_dir: &str) -> Result<(), libwallet::Error> { + setup(test_dir); + // Create a new proxy to simulate server and wallet responses + let mut wallet_proxy: WalletProxy = WalletProxy::new(test_dir); + let chain = wallet_proxy.chain.clone(); + + // Create a new wallet test client, and set its queues to communicate with the + // proxy + let client1 = LocalWalletClient::new("wallet1", wallet_proxy.tx.clone()); + let wallet1 = + test_framework::create_wallet(&format!("{}/wallet1", test_dir), client1.clone(), None); + wallet_proxy.add_wallet("wallet1", client1.get_send_instance(), wallet1.clone()); + + // define recipient wallet, add to proxy + let client2 = LocalWalletClient::new("wallet2", wallet_proxy.tx.clone()); + let wallet2 = + test_framework::create_wallet(&format!("{}/wallet2", test_dir), client2.clone(), None); + wallet_proxy.add_wallet("wallet2", client2.get_send_instance(), wallet2.clone()); + + // wallet 2 will use another account + wallet::controller::owner_single_use(wallet2.clone(), |api| { + api.create_account_path("account1")?; + api.create_account_path("account2")?; + Ok(()) + })?; + + // Default wallet 2 to listen on that account + { + let mut w = wallet2.lock(); + w.set_parent_key_id_by_name("account1")?; + } + + // Another wallet + let client3 = LocalWalletClient::new("wallet3", wallet_proxy.tx.clone()); + let wallet3 = + test_framework::create_wallet(&format!("{}/wallet3", test_dir), client3.clone(), None); + wallet_proxy.add_wallet("wallet3", client3.get_send_instance(), wallet3.clone()); + + // Set the wallet proxy listener running + let wp_running = wallet_proxy.running.clone(); + thread::spawn(move || { + if let Err(e) = wallet_proxy.run() { + error!("Wallet Proxy error: {}", e); + } + }); + + // mine a few blocks + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), 10); + + // assert wallet contents + // and a single use api for a send command + let amount = 60_000_000_000; + let mut slate = Slate::blank(1); + wallet::controller::owner_single_use(wallet1.clone(), |sender_api| { + // note this will increment the block count as part of the transaction "Posting" + let (slate_i, lock_fn) = sender_api.initiate_tx( + None, amount, // amount + 2, // minimum confirmations + 500, // max outputs + 1, // num change outputs + true, // select all outputs + None, + )?; + slate = client1.send_tx_slate_direct("wallet2", &slate_i)?; + sender_api.tx_lock_outputs(&slate, lock_fn)?; + sender_api.finalize_tx(&mut slate)?; + sender_api.post_tx(&slate.tx, false)?; + Ok(()) + })?; + + // mine a few more blocks + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), 3); + + // Send some to wallet 3 + wallet::controller::owner_single_use(wallet1.clone(), |sender_api| { + // note this will increment the block count as part of the transaction "Posting" + let (slate_i, lock_fn) = sender_api.initiate_tx( + None, + amount * 2, // amount + 2, // minimum confirmations + 500, // max outputs + 1, // num change outputs + true, // select all outputs + None, + )?; + slate = client1.send_tx_slate_direct("wallet3", &slate_i)?; + sender_api.tx_lock_outputs(&slate, lock_fn)?; + sender_api.finalize_tx(&mut slate)?; + sender_api.post_tx(&slate.tx, false)?; + Ok(()) + })?; + + // mine a few more blocks + let _ = test_framework::award_blocks_to_wallet(&chain, wallet3.clone(), 10); + + // Wallet3 to wallet 2 + wallet::controller::owner_single_use(wallet3.clone(), |sender_api| { + // note this will increment the block count as part of the transaction "Posting" + let (slate_i, lock_fn) = sender_api.initiate_tx( + None, + amount * 3, // amount + 2, // minimum confirmations + 500, // max outputs + 1, // num change outputs + true, // select all outputs + None, + )?; + slate = client3.send_tx_slate_direct("wallet2", &slate_i)?; + sender_api.tx_lock_outputs(&slate, lock_fn)?; + sender_api.finalize_tx(&mut slate)?; + sender_api.post_tx(&slate.tx, false)?; + Ok(()) + })?; + + // Another listener account on wallet 2 + { + let mut w = wallet2.lock(); + w.set_parent_key_id_by_name("account2")?; + } + + // mine a few more blocks + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), 2); + + // Wallet3 to wallet 2 again (to another account) + wallet::controller::owner_single_use(wallet3.clone(), |sender_api| { + // note this will increment the block count as part of the transaction "Posting" + let (slate_i, lock_fn) = sender_api.initiate_tx( + None, + amount * 3, // amount + 2, // minimum confirmations + 500, // max outputs + 1, // num change outputs + true, // select all outputs + None, + )?; + slate = client3.send_tx_slate_direct("wallet2", &slate_i)?; + sender_api.tx_lock_outputs(&slate, lock_fn)?; + sender_api.finalize_tx(&mut slate)?; + sender_api.post_tx(&slate.tx, false)?; + Ok(()) + })?; + + // mine a few more blocks + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), 5); + + // update everyone + wallet::controller::owner_single_use(wallet1.clone(), |api| { + let _ = api.retrieve_summary_info(true, 1)?; + Ok(()) + })?; + wallet::controller::owner_single_use(wallet2.clone(), |api| { + let _ = api.retrieve_summary_info(true, 1)?; + Ok(()) + })?; + wallet::controller::owner_single_use(wallet3.clone(), |api| { + let _ = api.retrieve_summary_info(true, 1)?; + Ok(()) + })?; + + wp_running.store(false, Ordering::Relaxed); + + Ok(()) +} + +fn perform_restore(test_dir: &str) -> Result<(), libwallet::Error> { + restore_wallet(test_dir, "wallet1")?; + compare_wallet_restore( + test_dir, + "wallet1", + &ExtKeychain::derive_key_id(2, 0, 0, 0, 0), + )?; + restore_wallet(test_dir, "wallet2")?; + compare_wallet_restore( + test_dir, + "wallet2", + &ExtKeychain::derive_key_id(2, 0, 0, 0, 0), + )?; + compare_wallet_restore( + test_dir, + "wallet2", + &ExtKeychain::derive_key_id(2, 1, 0, 0, 0), + )?; + compare_wallet_restore( + test_dir, + "wallet2", + &ExtKeychain::derive_key_id(2, 2, 0, 0, 0), + )?; + restore_wallet(test_dir, "wallet3")?; + compare_wallet_restore( + test_dir, + "wallet3", + &ExtKeychain::derive_key_id(2, 0, 0, 0, 0), + )?; + Ok(()) +} + +#[test] +fn wallet_restore() { + let test_dir = "test_output/wallet_restore"; + if let Err(e) = setup_restore(test_dir) { + panic!("Libwallet Error: {} - {}", e, e.backtrace().unwrap()); + } + if let Err(e) = perform_restore(test_dir) { + panic!("Libwallet Error: {} - {}", e, e.backtrace().unwrap()); + } + // let logging finish + thread::sleep(Duration::from_millis(200)); +} diff --git a/tests/self_send.rs b/tests/self_send.rs new file mode 100644 index 0000000..54cab30 --- /dev/null +++ b/tests/self_send.rs @@ -0,0 +1,144 @@ +// Copyright 2018 The Grin Developers +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Test a wallet sending to self +#[macro_use] +extern crate log; + +use self::core::global; +use self::core::global::ChainTypes; +use self::keychain::ExtKeychain; +use self::wallet::libwallet; +use self::wallet::test_framework::{self, LocalWalletClient, WalletProxy}; +use grin_core as core; +use grin_keychain as keychain; +use grin_util as util; +use grin_wallet as wallet; +use std::fs; +use std::thread; +use std::time::Duration; + +fn clean_output_dir(test_dir: &str) { + let _ = fs::remove_dir_all(test_dir); +} + +fn setup(test_dir: &str) { + util::init_test_logger(); + clean_output_dir(test_dir); + global::set_mining_mode(ChainTypes::AutomatedTesting); +} + +/// self send impl +fn self_send_test_impl(test_dir: &str) -> Result<(), libwallet::Error> { + setup(test_dir); + // Create a new proxy to simulate server and wallet responses + let mut wallet_proxy: WalletProxy = WalletProxy::new(test_dir); + let chain = wallet_proxy.chain.clone(); + + // Create a new wallet test client, and set its queues to communicate with the + // proxy + let client1 = LocalWalletClient::new("wallet1", wallet_proxy.tx.clone()); + let wallet1 = + test_framework::create_wallet(&format!("{}/wallet1", test_dir), client1.clone(), None); + wallet_proxy.add_wallet("wallet1", client1.get_send_instance(), wallet1.clone()); + + // Set the wallet proxy listener running + thread::spawn(move || { + if let Err(e) = wallet_proxy.run() { + error!("Wallet Proxy error: {}", e); + } + }); + + // few values to keep things shorter + let reward = core::consensus::REWARD; + + // add some accounts + wallet::controller::owner_single_use(wallet1.clone(), |api| { + api.create_account_path("mining")?; + api.create_account_path("listener")?; + Ok(()) + })?; + + // Get some mining done + { + let mut w = wallet1.lock(); + w.set_parent_key_id_by_name("mining")?; + } + let mut bh = 10u64; + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), bh as usize); + + // Should have 5 in account1 (5 spendable), 5 in account (2 spendable) + wallet::controller::owner_single_use(wallet1.clone(), |api| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(true, 1)?; + assert!(wallet1_refreshed); + assert_eq!(wallet1_info.last_confirmed_height, bh); + assert_eq!(wallet1_info.total, bh * reward); + // send to send + let (mut slate, lock_fn) = api.initiate_tx( + Some("mining"), + reward * 2, // amount + 2, // minimum confirmations + 500, // max outputs + 1, // num change outputs + true, // select all outputs + None, + )?; + api.tx_lock_outputs(&slate, lock_fn)?; + // Send directly to self + wallet::controller::foreign_single_use(wallet1.clone(), |api| { + api.receive_tx(&mut slate, Some("listener"), None)?; + Ok(()) + })?; + api.finalize_tx(&mut slate)?; + api.post_tx(&slate.tx, false)?; // mines a block + bh += 1; + Ok(()) + })?; + + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), 3); + bh += 3; + + // Check total in mining account + wallet::controller::owner_single_use(wallet1.clone(), |api| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(true, 1)?; + assert!(wallet1_refreshed); + assert_eq!(wallet1_info.last_confirmed_height, bh); + assert_eq!(wallet1_info.total, bh * reward - reward * 2); + Ok(()) + })?; + + // Check total in 'listener' account + { + let mut w = wallet1.lock(); + w.set_parent_key_id_by_name("listener")?; + } + wallet::controller::owner_single_use(wallet1.clone(), |api| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(true, 1)?; + assert!(wallet1_refreshed); + assert_eq!(wallet1_info.last_confirmed_height, bh); + assert_eq!(wallet1_info.total, 2 * reward); + Ok(()) + })?; + + // let logging finish + thread::sleep(Duration::from_millis(200)); + Ok(()) +} + +#[test] +fn wallet_self_send() { + let test_dir = "test_output/self_send"; + if let Err(e) = self_send_test_impl(test_dir) { + panic!("Libwallet Error: {} - {}", e, e.backtrace().unwrap()); + } +} diff --git a/tests/transaction.rs b/tests/transaction.rs new file mode 100644 index 0000000..ac22461 --- /dev/null +++ b/tests/transaction.rs @@ -0,0 +1,498 @@ +// Copyright 2018 The Grin Developers +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! tests for transactions building within core::libtx +#[macro_use] +extern crate log; +use self::core::global; +use self::core::global::ChainTypes; +use self::keychain::ExtKeychain; +use self::libwallet::slate::Slate; +use self::wallet::libwallet; +use self::wallet::libwallet::types::OutputStatus; +use self::wallet::test_framework::{self, LocalWalletClient, WalletProxy}; +use grin_core as core; +use grin_keychain as keychain; +use grin_util as util; +use grin_wallet as wallet; +use std::fs; +use std::thread; +use std::time::Duration; + +fn clean_output_dir(test_dir: &str) { + let _ = fs::remove_dir_all(test_dir); +} + +fn setup(test_dir: &str) { + util::init_test_logger(); + clean_output_dir(test_dir); + global::set_mining_mode(ChainTypes::AutomatedTesting); +} + +/// Exercises the Transaction API fully with a test NodeClient operating +/// directly on a chain instance +/// Callable with any type of wallet +fn basic_transaction_api(test_dir: &str) -> Result<(), libwallet::Error> { + setup(test_dir); + // Create a new proxy to simulate server and wallet responses + let mut wallet_proxy: WalletProxy = WalletProxy::new(test_dir); + let chain = wallet_proxy.chain.clone(); + + // Create a new wallet test client, and set its queues to communicate with the + // proxy + let client1 = LocalWalletClient::new("wallet1", wallet_proxy.tx.clone()); + let wallet1 = + test_framework::create_wallet(&format!("{}/wallet1", test_dir), client1.clone(), None); + wallet_proxy.add_wallet("wallet1", client1.get_send_instance(), wallet1.clone()); + + let client2 = LocalWalletClient::new("wallet2", wallet_proxy.tx.clone()); + // define recipient wallet, add to proxy + let wallet2 = + test_framework::create_wallet(&format!("{}/wallet2", test_dir), client2.clone(), None); + wallet_proxy.add_wallet("wallet2", client2.get_send_instance(), wallet2.clone()); + + // Set the wallet proxy listener running + thread::spawn(move || { + if let Err(e) = wallet_proxy.run() { + error!("Wallet Proxy error: {}", e); + } + }); + + // few values to keep things shorter + let reward = core::consensus::REWARD; + let cm = global::coinbase_maturity(); // assume all testing precedes soft fork height + // mine a few blocks + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), 10); + + // Check wallet 1 contents are as expected + wallet::controller::owner_single_use(wallet1.clone(), |api| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(true, 1)?; + debug!( + "Wallet 1 Info Pre-Transaction, after {} blocks: {:?}", + wallet1_info.last_confirmed_height, wallet1_info + ); + assert!(wallet1_refreshed); + assert_eq!( + wallet1_info.amount_currently_spendable, + (wallet1_info.last_confirmed_height - cm) * reward + ); + assert_eq!(wallet1_info.amount_immature, cm * reward); + Ok(()) + })?; + + // assert wallet contents + // and a single use api for a send command + let amount = 60_000_000_000; + let mut slate = Slate::blank(1); + wallet::controller::owner_single_use(wallet1.clone(), |sender_api| { + // note this will increment the block count as part of the transaction "Posting" + let (slate_i, lock_fn) = sender_api.initiate_tx( + None, amount, // amount + 2, // minimum confirmations + 500, // max outputs + 1, // num change outputs + true, // select all outputs + None, + )?; + slate = client1.send_tx_slate_direct("wallet2", &slate_i)?; + sender_api.tx_lock_outputs(&slate, lock_fn)?; + sender_api.finalize_tx(&mut slate)?; + Ok(()) + })?; + + // Check transaction log for wallet 1 + wallet::controller::owner_single_use(wallet1.clone(), |api| { + let (_, wallet1_info) = api.retrieve_summary_info(true, 1)?; + let (refreshed, txs) = api.retrieve_txs(true, None, None)?; + assert!(refreshed); + let fee = core::libtx::tx_fee( + wallet1_info.last_confirmed_height as usize - cm as usize, + 2, + 1, + None, + ); + // we should have a transaction entry for this slate + let tx = txs.iter().find(|t| t.tx_slate_id == Some(slate.id)); + assert!(tx.is_some()); + let tx = tx.unwrap(); + assert!(!tx.confirmed); + assert!(tx.confirmation_ts.is_none()); + assert_eq!(tx.amount_debited - tx.amount_credited, fee + amount); + assert_eq!(Some(fee), tx.fee); + Ok(()) + })?; + + // Check transaction log for wallet 2 + wallet::controller::owner_single_use(wallet2.clone(), |api| { + let (refreshed, txs) = api.retrieve_txs(true, None, None)?; + assert!(refreshed); + // we should have a transaction entry for this slate + let tx = txs.iter().find(|t| t.tx_slate_id == Some(slate.id)); + assert!(tx.is_some()); + let tx = tx.unwrap(); + assert!(!tx.confirmed); + assert!(tx.confirmation_ts.is_none()); + assert_eq!(amount, tx.amount_credited); + assert_eq!(0, tx.amount_debited); + assert_eq!(None, tx.fee); + Ok(()) + })?; + + // post transaction + wallet::controller::owner_single_use(wallet1.clone(), |api| { + api.post_tx(&slate.tx, false)?; + Ok(()) + })?; + + // Check wallet 1 contents are as expected + wallet::controller::owner_single_use(wallet1.clone(), |api| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(true, 1)?; + debug!( + "Wallet 1 Info Post Transaction, after {} blocks: {:?}", + wallet1_info.last_confirmed_height, wallet1_info + ); + let fee = core::libtx::tx_fee( + wallet1_info.last_confirmed_height as usize - 1 - cm as usize, + 2, + 1, + None, + ); + assert!(wallet1_refreshed); + // wallet 1 received fees, so amount should be the same + assert_eq!( + wallet1_info.total, + amount * wallet1_info.last_confirmed_height - amount + ); + assert_eq!( + wallet1_info.amount_currently_spendable, + (wallet1_info.last_confirmed_height - cm) * reward - amount - fee + ); + assert_eq!(wallet1_info.amount_immature, cm * reward + fee); + + // check tx log entry is confirmed + let (refreshed, txs) = api.retrieve_txs(true, None, None)?; + assert!(refreshed); + let tx = txs.iter().find(|t| t.tx_slate_id == Some(slate.id)); + assert!(tx.is_some()); + let tx = tx.unwrap(); + assert!(tx.confirmed); + assert!(tx.confirmation_ts.is_some()); + + Ok(()) + })?; + + // mine a few more blocks + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), 3); + + // refresh wallets and retrieve info/tests for each wallet after maturity + wallet::controller::owner_single_use(wallet1.clone(), |api| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(true, 1)?; + debug!("Wallet 1 Info: {:?}", wallet1_info); + assert!(wallet1_refreshed); + assert_eq!( + wallet1_info.total, + amount * wallet1_info.last_confirmed_height - amount + ); + assert_eq!( + wallet1_info.amount_currently_spendable, + (wallet1_info.last_confirmed_height - cm - 1) * reward + ); + Ok(()) + })?; + + wallet::controller::owner_single_use(wallet2.clone(), |api| { + let (wallet2_refreshed, wallet2_info) = api.retrieve_summary_info(true, 1)?; + assert!(wallet2_refreshed); + assert_eq!(wallet2_info.amount_currently_spendable, amount); + + // check tx log entry is confirmed + let (refreshed, txs) = api.retrieve_txs(true, None, None)?; + assert!(refreshed); + let tx = txs.iter().find(|t| t.tx_slate_id == Some(slate.id)); + assert!(tx.is_some()); + let tx = tx.unwrap(); + assert!(tx.confirmed); + assert!(tx.confirmation_ts.is_some()); + Ok(()) + })?; + + // Estimate fee and locked amount for a transaction + wallet::controller::owner_single_use(wallet1.clone(), |sender_api| { + let (total, fee) = sender_api.estimate_initiate_tx( + None, + amount * 2, // amount + 2, // minimum confirmations + 500, // max outputs + 1, // num change outputs + true, // select all outputs + )?; + assert_eq!(total, 600_000_000_000); + assert_eq!(fee, 4_000_000); + + let (total, fee) = sender_api.estimate_initiate_tx( + None, + amount * 2, // amount + 2, // minimum confirmations + 500, // max outputs + 1, // num change outputs + false, // select the smallest amount of outputs + )?; + assert_eq!(total, 180_000_000_000); + assert_eq!(fee, 6_000_000); + + Ok(()) + })?; + + // Send another transaction, but don't post to chain immediately and use + // the stored transaction instead + wallet::controller::owner_single_use(wallet1.clone(), |sender_api| { + // note this will increment the block count as part of the transaction "Posting" + let (slate_i, lock_fn) = sender_api.initiate_tx( + None, + amount * 2, // amount + 2, // minimum confirmations + 500, // max outputs + 1, // num change outputs + true, // select all outputs + None, + )?; + slate = client1.send_tx_slate_direct("wallet2", &slate_i)?; + sender_api.tx_lock_outputs(&slate, lock_fn)?; + sender_api.finalize_tx(&mut slate)?; + Ok(()) + })?; + + wallet::controller::owner_single_use(wallet1.clone(), |sender_api| { + let (refreshed, _wallet1_info) = sender_api.retrieve_summary_info(true, 1)?; + assert!(refreshed); + let (_, txs) = sender_api.retrieve_txs(true, None, None)?; + // find the transaction + let tx = txs + .iter() + .find(|t| t.tx_slate_id == Some(slate.id)) + .unwrap(); + let stored_tx = sender_api.get_stored_tx(&tx)?; + sender_api.post_tx(&stored_tx.unwrap(), false)?; + let (_, wallet1_info) = sender_api.retrieve_summary_info(true, 1)?; + // should be mined now + assert_eq!( + wallet1_info.total, + amount * wallet1_info.last_confirmed_height - amount * 3 + ); + Ok(()) + })?; + + // mine a few more blocks + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), 3); + + // check wallet2 has stored transaction + wallet::controller::owner_single_use(wallet2.clone(), |api| { + let (wallet2_refreshed, wallet2_info) = api.retrieve_summary_info(true, 1)?; + assert!(wallet2_refreshed); + assert_eq!(wallet2_info.amount_currently_spendable, amount * 3); + + // check tx log entry is confirmed + let (refreshed, txs) = api.retrieve_txs(true, None, None)?; + assert!(refreshed); + let tx = txs.iter().find(|t| t.tx_slate_id == Some(slate.id)); + assert!(tx.is_some()); + let tx = tx.unwrap(); + assert!(tx.confirmed); + assert!(tx.confirmation_ts.is_some()); + Ok(()) + })?; + + // let logging finish + thread::sleep(Duration::from_millis(200)); + Ok(()) +} + +/// Test rolling back transactions and outputs when a transaction is never +/// posted to a chain +fn tx_rollback(test_dir: &str) -> Result<(), libwallet::Error> { + setup(test_dir); + // Create a new proxy to simulate server and wallet responses + let mut wallet_proxy: WalletProxy = WalletProxy::new(test_dir); + let chain = wallet_proxy.chain.clone(); + + // Create a new wallet test client, and set its queues to communicate with the + // proxy + let client1 = LocalWalletClient::new("wallet1", wallet_proxy.tx.clone()); + let wallet1 = + test_framework::create_wallet(&format!("{}/wallet1", test_dir), client1.clone(), None); + wallet_proxy.add_wallet("wallet1", client1.get_send_instance(), wallet1.clone()); + + // define recipient wallet, add to proxy + let client2 = LocalWalletClient::new("wallet2", wallet_proxy.tx.clone()); + let wallet2 = + test_framework::create_wallet(&format!("{}/wallet2", test_dir), client2.clone(), None); + wallet_proxy.add_wallet("wallet2", client2.get_send_instance(), wallet2.clone()); + + // Set the wallet proxy listener running + thread::spawn(move || { + if let Err(e) = wallet_proxy.run() { + error!("Wallet Proxy error: {}", e); + } + }); + + // few values to keep things shorter + let reward = core::consensus::REWARD; + let cm = global::coinbase_maturity(); // assume all testing precedes soft fork height + // mine a few blocks + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), 5); + + let amount = 30_000_000_000; + let mut slate = Slate::blank(1); + wallet::controller::owner_single_use(wallet1.clone(), |sender_api| { + // note this will increment the block count as part of the transaction "Posting" + let (slate_i, lock_fn) = sender_api.initiate_tx( + None, amount, // amount + 2, // minimum confirmations + 500, // max outputs + 1, // num change outputs + true, // select all outputs + None, + )?; + slate = client1.send_tx_slate_direct("wallet2", &slate_i)?; + sender_api.tx_lock_outputs(&slate, lock_fn)?; + sender_api.finalize_tx(&mut slate)?; + Ok(()) + })?; + + // Check transaction log for wallet 1 + wallet::controller::owner_single_use(wallet1.clone(), |api| { + let (refreshed, wallet1_info) = api.retrieve_summary_info(true, 1)?; + println!( + "last confirmed height: {}", + wallet1_info.last_confirmed_height + ); + assert!(refreshed); + let (_, txs) = api.retrieve_txs(true, None, None)?; + // we should have a transaction entry for this slate + let tx = txs.iter().find(|t| t.tx_slate_id == Some(slate.id)); + assert!(tx.is_some()); + let mut locked_count = 0; + let mut unconfirmed_count = 0; + // get the tx entry, check outputs are as expected + let (_, outputs) = api.retrieve_outputs(true, false, Some(tx.unwrap().id))?; + for (o, _) in outputs.clone() { + if o.status == OutputStatus::Locked { + locked_count = locked_count + 1; + } + if o.status == OutputStatus::Unconfirmed { + unconfirmed_count = unconfirmed_count + 1; + } + } + assert_eq!(outputs.len(), 3); + assert_eq!(locked_count, 2); + assert_eq!(unconfirmed_count, 1); + + Ok(()) + })?; + + // Check transaction log for wallet 2 + wallet::controller::owner_single_use(wallet2.clone(), |api| { + let (refreshed, txs) = api.retrieve_txs(true, None, None)?; + assert!(refreshed); + let mut unconfirmed_count = 0; + let tx = txs.iter().find(|t| t.tx_slate_id == Some(slate.id)); + assert!(tx.is_some()); + // get the tx entry, check outputs are as expected + let (_, outputs) = api.retrieve_outputs(true, false, Some(tx.unwrap().id))?; + for (o, _) in outputs.clone() { + if o.status == OutputStatus::Unconfirmed { + unconfirmed_count = unconfirmed_count + 1; + } + } + assert_eq!(outputs.len(), 1); + assert_eq!(unconfirmed_count, 1); + let (refreshed, wallet2_info) = api.retrieve_summary_info(true, 1)?; + assert!(refreshed); + assert_eq!(wallet2_info.amount_currently_spendable, 0,); + assert_eq!(wallet2_info.total, amount); + Ok(()) + })?; + + // wallet 1 is bold and doesn't ever post the transaction mine a few more blocks + let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), 5); + + // Wallet 1 decides to roll back instead + wallet::controller::owner_single_use(wallet1.clone(), |api| { + // can't roll back coinbase + let res = api.cancel_tx(Some(1), None); + assert!(res.is_err()); + let (_, txs) = api.retrieve_txs(true, None, None)?; + let tx = txs + .iter() + .find(|t| t.tx_slate_id == Some(slate.id)) + .unwrap(); + api.cancel_tx(Some(tx.id), None)?; + let (refreshed, wallet1_info) = api.retrieve_summary_info(true, 1)?; + assert!(refreshed); + println!( + "last confirmed height: {}", + wallet1_info.last_confirmed_height + ); + // check all eligible inputs should be now be spendable + println!("cm: {}", cm); + assert_eq!( + wallet1_info.amount_currently_spendable, + (wallet1_info.last_confirmed_height - cm) * reward + ); + // can't roll back again + let res = api.cancel_tx(Some(tx.id), None); + assert!(res.is_err()); + + Ok(()) + })?; + + // Wallet 2 rolls back + wallet::controller::owner_single_use(wallet2.clone(), |api| { + let (_, txs) = api.retrieve_txs(true, None, None)?; + let tx = txs + .iter() + .find(|t| t.tx_slate_id == Some(slate.id)) + .unwrap(); + api.cancel_tx(Some(tx.id), None)?; + let (refreshed, wallet2_info) = api.retrieve_summary_info(true, 1)?; + assert!(refreshed); + // check all eligible inputs should be now be spendable + assert_eq!(wallet2_info.amount_currently_spendable, 0,); + assert_eq!(wallet2_info.total, 0,); + // can't roll back again + let res = api.cancel_tx(Some(tx.id), None); + assert!(res.is_err()); + + Ok(()) + })?; + + // let logging finish + thread::sleep(Duration::from_millis(200)); + Ok(()) +} + +#[test] +fn db_wallet_basic_transaction_api() { + let test_dir = "test_output/basic_transaction_api"; + if let Err(e) = basic_transaction_api(test_dir) { + panic!("Libwallet Error: {} - {}", e, e.backtrace().unwrap()); + } +} + +#[test] +fn db_wallet_tx_rollback() { + let test_dir = "test_output/tx_rollback"; + if let Err(e) = tx_rollback(test_dir) { + panic!("Libwallet Error: {} - {}", e, e.backtrace().unwrap()); + } +}