From 41c0058e841a6128fb426784950b952badc9a215 Mon Sep 17 00:00:00 2001 From: Yeastplume Date: Mon, 2 Sep 2019 16:03:35 +0100 Subject: [PATCH] API Lifecycle Implementation, Pt. 1 (#211) * allow owner api to start up without a wallet, begin to add lifecycle functions * rustfmt * test and typos fix * updated with lifecycle functions * rustfmt * updates to allow owner api tests to be executed against local wallet proxy * rustfmt * fix for windows test * add ability to pass configuration to , begin to add documentation and doctests * add ability to pass configuration to , begin to add documentation and doctests * doctests for lifecycle functions * rustfmt * ensure foreign API also has mask updated when being run along owner api, add more tests to lifecycle * rustfmt * documentation for lifecycle methods + init_secure_api * rustfmt * failing doctest --- Cargo.lock | 74 +++-- api/src/foreign.rs | 4 +- api/src/foreign_rpc.rs | 8 +- api/src/lib.rs | 1 + api/src/owner.rs | 341 +++++++++++++++++++- api/src/owner_rpc.rs | 8 +- api/src/owner_rpc_s.rs | 333 ++++++++++++++++++- config/src/lib.rs | 2 +- controller/src/command.rs | 10 +- controller/src/controller.rs | 195 +++++++++-- controller/tests/common/mod.rs | 6 +- impls/src/adapters/keybase.rs | 2 +- impls/src/lifecycle/default.rs | 59 +++- impls/src/lifecycle/seed.rs | 12 +- libwallet/Cargo.toml | 2 - libwallet/src/api_impl/owner.rs | 1 + libwallet/src/error.rs | 4 +- libwallet/src/lib.rs | 1 + libwallet/src/slate_versions/mod.rs | 2 +- libwallet/src/types.rs | 18 +- src/bin/grin-wallet.rs | 6 +- src/bin/grin-wallet.yml | 8 +- src/cmd/wallet.rs | 14 +- src/cmd/wallet_args.rs | 61 +++- tests/common/mod.rs | 53 ++- tests/data/v2_reqs/init_send_tx.req.json | 19 ++ tests/data/v3_reqs/close_wallet.req.json | 8 + tests/data/v3_reqs/create_config.req.json | 10 + tests/data/v3_reqs/create_wallet.req.json | 11 + tests/data/v3_reqs/get_top_level.req.json | 7 + tests/data/v3_reqs/init_send_tx.req.json | 20 ++ tests/data/v3_reqs/open_wallet.req.json | 5 +- tests/owner_v3_init_secure.rs | 10 +- tests/owner_v3_lifecycle.rs | 374 ++++++++++++++++++++++ 34 files changed, 1557 insertions(+), 132 deletions(-) create mode 100644 tests/data/v2_reqs/init_send_tx.req.json create mode 100644 tests/data/v3_reqs/close_wallet.req.json create mode 100644 tests/data/v3_reqs/create_config.req.json create mode 100644 tests/data/v3_reqs/create_wallet.req.json create mode 100644 tests/data/v3_reqs/get_top_level.req.json create mode 100644 tests/data/v3_reqs/init_send_tx.req.json create mode 100644 tests/owner_v3_lifecycle.rs diff --git a/Cargo.lock b/Cargo.lock index e4e1a240..d6ca0679 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -581,16 +581,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "grin_api" version = "2.0.1-beta.1" +source = "git+https://github.com/mimblewimble/grin#ea023387bfa4f1493cc57f120205a09251468e60" 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.28 (registry+https://github.com/rust-lang/crates.io-index)", - "grin_chain 2.0.1-beta.1", - "grin_core 2.0.1-beta.1", - "grin_p2p 2.0.1-beta.1", - "grin_pool 2.0.1-beta.1", - "grin_store 2.0.1-beta.1", - "grin_util 2.0.1-beta.1", + "grin_chain 2.0.1-beta.1 (git+https://github.com/mimblewimble/grin)", + "grin_core 2.0.1-beta.1 (git+https://github.com/mimblewimble/grin)", + "grin_p2p 2.0.1-beta.1 (git+https://github.com/mimblewimble/grin)", + "grin_pool 2.0.1-beta.1 (git+https://github.com/mimblewimble/grin)", + "grin_store 2.0.1-beta.1 (git+https://github.com/mimblewimble/grin)", + "grin_util 2.0.1-beta.1 (git+https://github.com/mimblewimble/grin)", "http 0.1.18 (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)", @@ -612,6 +613,7 @@ dependencies = [ [[package]] name = "grin_chain" version = "2.0.1-beta.1" +source = "git+https://github.com/mimblewimble/grin#ea023387bfa4f1493cc57f120205a09251468e60" dependencies = [ "bitflags 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", @@ -619,10 +621,10 @@ dependencies = [ "croaring 0.3.9 (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 2.0.1-beta.1", - "grin_keychain 2.0.1-beta.1", - "grin_store 2.0.1-beta.1", - "grin_util 2.0.1-beta.1", + "grin_core 2.0.1-beta.1 (git+https://github.com/mimblewimble/grin)", + "grin_keychain 2.0.1-beta.1 (git+https://github.com/mimblewimble/grin)", + "grin_store 2.0.1-beta.1 (git+https://github.com/mimblewimble/grin)", + "grin_util 2.0.1-beta.1 (git+https://github.com/mimblewimble/grin)", "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "lru-cache 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", @@ -634,6 +636,7 @@ dependencies = [ [[package]] name = "grin_core" version = "2.0.1-beta.1" +source = "git+https://github.com/mimblewimble/grin#ea023387bfa4f1493cc57f120205a09251468e60" dependencies = [ "blake2-rfc 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)", "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", @@ -642,8 +645,8 @@ dependencies = [ "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 2.0.1-beta.1", - "grin_util 2.0.1-beta.1", + "grin_keychain 2.0.1-beta.1 (git+https://github.com/mimblewimble/grin)", + "grin_util 2.0.1-beta.1 (git+https://github.com/mimblewimble/grin)", "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "lru-cache 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", @@ -660,11 +663,12 @@ dependencies = [ [[package]] name = "grin_keychain" version = "2.0.1-beta.1" +source = "git+https://github.com/mimblewimble/grin#ea023387bfa4f1493cc57f120205a09251468e60" dependencies = [ "blake2-rfc 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)", "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", "digest 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)", - "grin_util 2.0.1-beta.1", + "grin_util 2.0.1-beta.1 (git+https://github.com/mimblewimble/grin)", "hmac 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", @@ -682,15 +686,16 @@ dependencies = [ [[package]] name = "grin_p2p" version = "2.0.1-beta.1" +source = "git+https://github.com/mimblewimble/grin#ea023387bfa4f1493cc57f120205a09251468e60" dependencies = [ "bitflags 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", "chrono 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", "enum_primitive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "grin_chain 2.0.1-beta.1", - "grin_core 2.0.1-beta.1", - "grin_store 2.0.1-beta.1", - "grin_util 2.0.1-beta.1", + "grin_chain 2.0.1-beta.1 (git+https://github.com/mimblewimble/grin)", + "grin_core 2.0.1-beta.1 (git+https://github.com/mimblewimble/grin)", + "grin_store 2.0.1-beta.1 (git+https://github.com/mimblewimble/grin)", + "grin_util 2.0.1-beta.1 (git+https://github.com/mimblewimble/grin)", "log 0.4.8 (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)", @@ -703,15 +708,16 @@ dependencies = [ [[package]] name = "grin_pool" version = "2.0.1-beta.1" +source = "git+https://github.com/mimblewimble/grin#ea023387bfa4f1493cc57f120205a09251468e60" dependencies = [ "blake2-rfc 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)", "chrono 0.4.7 (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 2.0.1-beta.1", - "grin_keychain 2.0.1-beta.1", - "grin_store 2.0.1-beta.1", - "grin_util 2.0.1-beta.1", + "grin_core 2.0.1-beta.1 (git+https://github.com/mimblewimble/grin)", + "grin_keychain 2.0.1-beta.1 (git+https://github.com/mimblewimble/grin)", + "grin_store 2.0.1-beta.1 (git+https://github.com/mimblewimble/grin)", + "grin_util 2.0.1-beta.1 (git+https://github.com/mimblewimble/grin)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.98 (registry+https://github.com/rust-lang/crates.io-index)", @@ -736,14 +742,15 @@ dependencies = [ [[package]] name = "grin_store" version = "2.0.1-beta.1" +source = "git+https://github.com/mimblewimble/grin#ea023387bfa4f1493cc57f120205a09251468e60" dependencies = [ "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", "croaring 0.3.9 (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 2.0.1-beta.1", - "grin_util 2.0.1-beta.1", + "grin_core 2.0.1-beta.1 (git+https://github.com/mimblewimble/grin)", + "grin_util 2.0.1-beta.1 (git+https://github.com/mimblewimble/grin)", "libc 0.2.60 (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.8 (registry+https://github.com/rust-lang/crates.io-index)", @@ -756,6 +763,7 @@ dependencies = [ [[package]] name = "grin_util" version = "2.0.1-beta.1" +source = "git+https://github.com/mimblewimble/grin#ea023387bfa4f1493cc57f120205a09251468e60" dependencies = [ "backtrace 0.3.34 (registry+https://github.com/rust-lang/crates.io-index)", "base64 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)", @@ -918,12 +926,12 @@ name = "grin_wallet_util" version = "2.1.0-beta.1" dependencies = [ "dirs 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", - "grin_api 2.0.1-beta.1", - "grin_chain 2.0.1-beta.1", - "grin_core 2.0.1-beta.1", - "grin_keychain 2.0.1-beta.1", - "grin_store 2.0.1-beta.1", - "grin_util 2.0.1-beta.1", + "grin_api 2.0.1-beta.1 (git+https://github.com/mimblewimble/grin)", + "grin_chain 2.0.1-beta.1 (git+https://github.com/mimblewimble/grin)", + "grin_core 2.0.1-beta.1 (git+https://github.com/mimblewimble/grin)", + "grin_keychain 2.0.1-beta.1 (git+https://github.com/mimblewimble/grin)", + "grin_store 2.0.1-beta.1 (git+https://github.com/mimblewimble/grin)", + "grin_util 2.0.1-beta.1 (git+https://github.com/mimblewimble/grin)", "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.98 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2878,7 +2886,15 @@ dependencies = [ "checksum getrandom 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "cd8e190892c840661957ba9f32dacfb3eb405e657f9f9f60485605f0bb37d6f8" "checksum git2 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)" = "8cb400360e8a4d61b10e648285bbfa919bbf9519d0d5d5720354456f44349226" "checksum glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "8be18de09a56b60ed0edf84bc9df007e30040691af7acd1c41874faac5895bfb" +"checksum grin_api 2.0.1-beta.1 (git+https://github.com/mimblewimble/grin)" = "" +"checksum grin_chain 2.0.1-beta.1 (git+https://github.com/mimblewimble/grin)" = "" +"checksum grin_core 2.0.1-beta.1 (git+https://github.com/mimblewimble/grin)" = "" +"checksum grin_keychain 2.0.1-beta.1 (git+https://github.com/mimblewimble/grin)" = "" +"checksum grin_p2p 2.0.1-beta.1 (git+https://github.com/mimblewimble/grin)" = "" +"checksum grin_pool 2.0.1-beta.1 (git+https://github.com/mimblewimble/grin)" = "" "checksum grin_secp256k1zkp 0.7.7 (registry+https://github.com/rust-lang/crates.io-index)" = "23027a7673df2c2b20fb9589d742ff400a10a9c3e4c769a77e9fa3bd19586822" +"checksum grin_store 2.0.1-beta.1 (git+https://github.com/mimblewimble/grin)" = "" +"checksum grin_util 2.0.1-beta.1 (git+https://github.com/mimblewimble/grin)" = "" "checksum h2 0.1.26 (registry+https://github.com/rust-lang/crates.io-index)" = "a5b34c246847f938a410a03c5458c7fee2274436675e76d8b903c08efc29c462" "checksum heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" "checksum hmac 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)" = "733e1b3ac906631ca01ebb577e9bb0f5e37a454032b9036b5eaea4013ed6f99a" diff --git a/api/src/foreign.rs b/api/src/foreign.rs index 1a0a35d6..6c5178e6 100644 --- a/api/src/foreign.rs +++ b/api/src/foreign.rs @@ -144,7 +144,7 @@ where /// /// // The top level wallet directory should be set manually (in the reference implementation, /// // this is provided in the WalletConfig) - /// lc.set_wallet_directory(&wallet_config.data_file_dir); + /// let _ = lc.set_top_level_directory(&wallet_config.data_file_dir); /// /// // Wallet must be opened with the password (TBD) /// let pw = ZeroingString::from("wallet_password"); @@ -506,7 +506,7 @@ macro_rules! doctest_helper_setup_doc_env_foreign { >, >; let lc = wallet.lc_provider().unwrap(); - lc.set_wallet_directory(&wallet_config.data_file_dir); + let _ = lc.set_top_level_directory(&wallet_config.data_file_dir); lc.open_wallet(None, pw, false, false); let mut $wallet = Arc::new(Mutex::new(wallet)); }; diff --git a/api/src/foreign_rpc.rs b/api/src/foreign_rpc.rs index a360b29c..3d873edb 100644 --- a/api/src/foreign_rpc.rs +++ b/api/src/foreign_rpc.rs @@ -624,8 +624,8 @@ pub fn run_doctest_foreign( >, >; let lc = wallet1.lc_provider().unwrap(); - lc.set_wallet_directory(&format!("{}/wallet1", test_dir)); - lc.create_wallet(None, Some(rec_phrase_1), 32, empty_string.clone()) + let _ = lc.set_top_level_directory(&format!("{}/wallet1", test_dir)); + lc.create_wallet(None, Some(rec_phrase_1), 32, empty_string.clone(), false) .unwrap(); let mask1 = lc .open_wallet(None, empty_string.clone(), use_token, true) @@ -659,8 +659,8 @@ pub fn run_doctest_foreign( >, >; let lc = wallet2.lc_provider().unwrap(); - lc.set_wallet_directory(&format!("{}/wallet2", test_dir)); - lc.create_wallet(None, Some(rec_phrase_2), 32, empty_string.clone()) + let _ = lc.set_top_level_directory(&format!("{}/wallet2", test_dir)); + lc.create_wallet(None, Some(rec_phrase_2), 32, empty_string.clone(), false) .unwrap(); let mask2 = lc .open_wallet(None, empty_string.clone(), use_token, true) diff --git a/api/src/lib.rs b/api/src/lib.rs index 62eb33d2..10c3d8d4 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -22,6 +22,7 @@ #![deny(unused_mut)] #![warn(missing_docs)] +use grin_wallet_config as config; use grin_wallet_util::grin_core as core; use grin_wallet_util::grin_keychain as keychain; use grin_wallet_util::grin_util as util; diff --git a/api/src/owner.rs b/api/src/owner.rs index 2a9413fb..2556c02c 100644 --- a/api/src/owner.rs +++ b/api/src/owner.rs @@ -17,7 +17,9 @@ use chrono::prelude::*; use uuid::Uuid; +use crate::config::WalletConfig; use crate::core::core::Transaction; +use crate::core::global; use crate::impls::create_sender; use crate::keychain::{Identifier, Keychain}; use crate::libwallet::api_impl::owner; @@ -27,7 +29,7 @@ use crate::libwallet::{ WalletLCProvider, }; use crate::util::secp::key::SecretKey; -use crate::util::Mutex; +use crate::util::{from_hex, static_secp_instance, LoggingConfig, Mutex, ZeroingString}; use std::sync::Arc; /// Main interface into all wallet API functions. @@ -125,7 +127,7 @@ where /// /// // The top level wallet directory should be set manually (in the reference implementation, /// // this is provided in the WalletConfig) - /// lc.set_wallet_directory(&wallet_config.data_file_dir); + /// let _ = lc.set_top_level_directory(&wallet_config.data_file_dir); /// /// // Wallet must be opened with the password (TBD) /// let pw = ZeroingString::from("wallet_password"); @@ -1231,6 +1233,338 @@ where let _ = w.keychain(keychain_mask)?; owner::node_height(&mut **w, keychain_mask) } + + // LIFECYCLE FUNCTIONS + + /// Retrieve the top-level directory for the wallet. This directory should contain the + /// `grin-wallet.toml` file and the `wallet_data` directory that contains the wallet + /// seed + data files. Future versions of the wallet API will support multiple wallets + /// within the top level directory. + /// + /// The top level directory defaults to (in order of precedence): + /// + /// 1) The current directory, from which `grin-wallet` or the main process was run, if it + /// contains a `grin-wallet.toml` file. + /// 2) ~/.grin// otherwise + /// + /// # Arguments + /// + /// * None + /// + /// # Returns + /// * Ok with a String value representing the full path to the top level wallet dierctory + /// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered. + /// + /// # Example + /// Set up as in [`new`](struct.Owner.html#method.new) method above. + /// ``` + /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); + /// + /// let api_owner = Owner::new(wallet.clone()); + /// let result = api_owner.get_top_level_directory(); + /// + /// if let Ok(dir) = result { + /// println!("Top level directory is: {}", dir); + /// //... + /// } + /// ``` + + pub fn get_top_level_directory(&self) -> Result { + let mut w_lock = self.wallet_inst.lock(); + let lc = w_lock.lc_provider()?; + if self.doctest_mode { + Ok("/doctest/dir".to_owned()) + } else { + lc.get_top_level_directory() + } + } + + /// Set the top-level directory for the wallet. This directory can be empty, and will be created + /// during a subsequent calls to [`create_config`](struct.Owner.html#method.create_config) + /// + /// Set [`get_top_level_directory`](struct.Owner.html#method.get_top_level_directory) for a + /// description of the top level directory and default paths. + /// + /// # Arguments + /// + /// * `dir`: The new top-level directory path (either relative to current directory or + /// absolute. + /// + /// # Returns + /// * Ok if successful + /// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered. + /// + /// # Example + /// Set up as in [`new`](struct.Owner.html#method.new) method above. + /// ``` + /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); + /// + /// let dir = "path/to/wallet/dir"; + /// + /// # let dir = tempdir().map_err(|e| format!("{:#?}", e)).unwrap(); + /// # let dir = dir + /// # .path() + /// # .to_str() + /// # .ok_or("Failed to convert tmpdir path to string.".to_owned()) + /// # .unwrap(); + /// + /// let api_owner = Owner::new(wallet.clone()); + /// let result = api_owner.set_top_level_directory(dir); + /// + /// if let Ok(dir) = result { + /// //... + /// } + /// ``` + + pub fn set_top_level_directory(&self, dir: &str) -> Result<(), Error> { + let mut w_lock = self.wallet_inst.lock(); + let lc = w_lock.lc_provider()?; + lc.set_top_level_directory(dir) + } + + /// Create a `grin-wallet.toml` configuration file in the top-level directory for the + /// specified chain type. + /// A custom [`WalletConfig`](../grin_wallet_config/types/struct.WalletConfig.html) + /// and/or grin `LoggingConfig` may optionally be provided, otherwise defaults will be used. + /// + /// Paths in the configuration file will be updated to reflect the top level directory, so + /// path-related values in the optional configuration structs will be ignored. + /// + /// # Arguments + /// + /// * `chain_type`: The chain type to use in creation of the configuration file. This can be + /// * `AutomatedTesting` + /// * `UserTesting` + /// * `Floonet` + /// * `Mainnet` + /// + /// # Returns + /// * Ok if successful + /// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered. + /// + /// # Example + /// Set up as in [`new`](struct.Owner.html#method.new) method above. + /// ``` + /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); + /// + /// use grin_core::global::ChainTypes; + /// + /// let dir = "path/to/wallet/dir"; + /// + /// # let dir = tempdir().map_err(|e| format!("{:#?}", e)).unwrap(); + /// # let dir = dir + /// # .path() + /// # .to_str() + /// # .ok_or("Failed to convert tmpdir path to string.".to_owned()) + /// # .unwrap(); + /// + /// let api_owner = Owner::new(wallet.clone()); + /// let _ = api_owner.set_top_level_directory(dir); + /// + /// let result = api_owner.create_config(&ChainTypes::Mainnet, None, None); + /// + /// if let Ok(_) = result { + /// //... + /// } + /// ``` + + pub fn create_config( + &self, + chain_type: &global::ChainTypes, + wallet_config: Option, + logging_config: Option, + ) -> Result<(), Error> { + let mut w_lock = self.wallet_inst.lock(); + let lc = w_lock.lc_provider()?; + lc.create_config( + chain_type, + "grin-wallet.toml", + wallet_config, + logging_config, + ) + } + + /// Creates a new wallet seed and empty wallet database in the `wallet_data` directory of + /// the top level directory. + /// + /// Paths in the configuration file will be updated to reflect the top level directory, so + /// path-related values in the optional configuration structs will be ignored. + /// + /// The wallet files must not already exist, and ~The `grin-wallet.toml` file must exist + /// in the top level directory (can be created via a call to + /// [`create_config`](struct.Owner.html#method.create_config)) + /// + /// # Arguments + /// + /// * `name`: Reserved for future use, use `None` for the time being. + /// * `mnemonic`: If present, restore the wallet seed from the given mnemonic instead of creating + /// a new random seed. + /// * `mnemonic_length`: Desired length of mnemonic in bytes (16 or 32, either 12 or 24 words). + /// Use 0 if mnemonic isn't being used. + /// * `password`: The password used to encrypt/decrypt the `wallet.seed` file + /// + /// # Returns + /// * Ok if successful + /// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered. + /// + /// # Example + /// Set up as in [`new`](struct.Owner.html#method.new) method above. + /// ``` + /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); + /// + /// use grin_core::global::ChainTypes; + /// + /// // note that the WalletInst struct does not necessarily need to contain an + /// // instantiated wallet + /// + /// let dir = "path/to/wallet/dir"; + /// + /// # let dir = tempdir().map_err(|e| format!("{:#?}", e)).unwrap(); + /// # let dir = dir + /// # .path() + /// # .to_str() + /// # .ok_or("Failed to convert tmpdir path to string.".to_owned()) + /// # .unwrap(); + /// let api_owner = Owner::new(wallet.clone()); + /// let _ = api_owner.set_top_level_directory(dir); + /// + /// // Create configuration + /// let result = api_owner.create_config(&ChainTypes::Mainnet, None, None); + /// + /// // create new wallet wirh random seed + /// let pw = ZeroingString::from("my_password"); + /// let result = api_owner.create_wallet(None, None, 0, pw); + /// + /// if let Ok(r) = result { + /// //... + /// } + /// ``` + + pub fn create_wallet( + &self, + name: Option<&str>, + mnemonic: Option, + mnemonic_length: u32, + password: ZeroingString, + ) -> Result<(), Error> { + let mut w_lock = self.wallet_inst.lock(); + let lc = w_lock.lc_provider()?; + lc.create_wallet( + name, + mnemonic, + mnemonic_length as usize, + password, + self.doctest_mode, + ) + } + + /// `Opens` a wallet, populating the internal keychain with the encrypted seed, and optionally + /// returning a `keychain_mask` token to the caller to provide in all future calls. + /// If using a mask, the seed will be stored in-memory XORed against the `keychain_mask`, and + /// will not be useable if the mask is not provided. + /// + /// # Arguments + /// + /// * `name`: Reserved for future use, use `None` for the time being. + /// * `password`: The password to use to open the wallet + /// a new random seed. + /// * `use_mask`: Whether to create and return a mask which much be provided in all future + /// API calls. + /// + /// # Returns + /// * Ok if successful + /// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered. + /// + /// # Example + /// Set up as in [`new`](struct.Owner.html#method.new) method above. + /// ``` + /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); + /// + /// use grin_core::global::ChainTypes; + /// + /// // note that the WalletInst struct does not necessarily need to contain an + /// // instantiated wallet + /// let dir = "path/to/wallet/dir"; + /// + /// # let dir = tempdir().map_err(|e| format!("{:#?}", e)).unwrap(); + /// # let dir = dir + /// # .path() + /// # .to_str() + /// # .ok_or("Failed to convert tmpdir path to string.".to_owned()) + /// # .unwrap(); + /// let api_owner = Owner::new(wallet.clone()); + /// let _ = api_owner.set_top_level_directory(dir); + /// + /// // Create configuration + /// let result = api_owner.create_config(&ChainTypes::Mainnet, None, None); + /// + /// // create new wallet wirh random seed + /// let pw = ZeroingString::from("my_password"); + /// let _ = api_owner.create_wallet(None, None, 0, pw.clone()); + /// + /// let result = api_owner.open_wallet(None, pw, true); + /// + /// if let Ok(m) = result { + /// // use this mask in all subsequent calls + /// let mask = m; + /// } + /// ``` + + pub fn open_wallet( + &self, + name: Option<&str>, + password: ZeroingString, + use_mask: bool, + ) -> Result, Error> { + // just return a representative string for doctest mode + if self.doctest_mode { + let secp_inst = static_secp_instance(); + let secp = secp_inst.lock(); + return Ok(Some(SecretKey::from_slice( + &secp, + &from_hex( + "d096b3cb75986b3b13f80b8f5243a9edf0af4c74ac37578c5a12cfb5b59b1868".to_owned(), + ) + .unwrap(), + )?)); + } + let mut w_lock = self.wallet_inst.lock(); + let lc = w_lock.lc_provider()?; + lc.open_wallet(name, password, use_mask, self.doctest_mode) + } + + /// `Close` a wallet, removing the master seed from memory. + /// + /// # Arguments + /// + /// * `name`: Reserved for future use, use `None` for the time being. + /// + /// # Returns + /// * Ok if successful + /// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered. + /// + /// # Example + /// Set up as in [`new`](struct.Owner.html#method.new) method above. + /// ``` + /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); + /// + /// use grin_core::global::ChainTypes; + /// + /// // Set up as above + /// # let api_owner = Owner::new(wallet.clone()); + /// + /// let res = api_owner.close_wallet(None); + /// + /// if let Ok(_) = res { + /// // ... + /// } + /// ``` + + pub fn close_wallet(&self, name: Option<&str>) -> Result<(), Error> { + let mut w_lock = self.wallet_inst.lock(); + let lc = w_lock.lc_provider()?; + lc.close_wallet(name) + } } #[doc(hidden)] @@ -1241,6 +1575,7 @@ macro_rules! doctest_helper_setup_doc_env { use grin_wallet_config as config; use grin_wallet_impls as impls; use grin_wallet_libwallet as libwallet; + use grin_wallet_util::grin_core; use grin_wallet_util::grin_keychain as keychain; use grin_wallet_util::grin_util as util; @@ -1278,7 +1613,7 @@ macro_rules! doctest_helper_setup_doc_env { >, >; let lc = wallet.lc_provider().unwrap(); - lc.set_wallet_directory(&wallet_config.data_file_dir); + let _ = lc.set_top_level_directory(&wallet_config.data_file_dir); lc.open_wallet(None, pw, false, false); let mut $wallet = Arc::new(Mutex::new(wallet)); }; diff --git a/api/src/owner_rpc.rs b/api/src/owner_rpc.rs index b7a42568..7b8b4b8f 100644 --- a/api/src/owner_rpc.rs +++ b/api/src/owner_rpc.rs @@ -1413,8 +1413,8 @@ pub fn run_doctest_owner( >, >; let lc = wallet1.lc_provider().unwrap(); - lc.set_wallet_directory(&format!("{}/wallet1", test_dir)); - lc.create_wallet(None, Some(rec_phrase_1), 32, empty_string.clone()) + let _ = lc.set_top_level_directory(&format!("{}/wallet1", test_dir)); + lc.create_wallet(None, Some(rec_phrase_1), 32, empty_string.clone(), false) .unwrap(); let mask1 = lc .open_wallet(None, empty_string.clone(), use_token, true) @@ -1448,8 +1448,8 @@ pub fn run_doctest_owner( >, >; let lc = wallet2.lc_provider().unwrap(); - lc.set_wallet_directory(&format!("{}/wallet2", test_dir)); - lc.create_wallet(None, Some(rec_phrase_2), 32, empty_string.clone()) + let _ = lc.set_top_level_directory(&format!("{}/wallet2", test_dir)); + lc.create_wallet(None, Some(rec_phrase_2), 32, empty_string.clone(), false) .unwrap(); let mask2 = lc .open_wallet(None, empty_string.clone(), use_token, true) diff --git a/api/src/owner_rpc_s.rs b/api/src/owner_rpc_s.rs index 8ee4fb71..a62b3fa5 100644 --- a/api/src/owner_rpc_s.rs +++ b/api/src/owner_rpc_s.rs @@ -15,7 +15,9 @@ //! JSON-RPC Stub generation for the Owner API use uuid::Uuid; +use crate::config::WalletConfig; use crate::core::core::Transaction; +use crate::core::global; use crate::keychain::{Identifier, Keychain}; use crate::libwallet::slate_versions::v2::TransactionV2; use crate::libwallet::{ @@ -24,13 +26,16 @@ use crate::libwallet::{ WalletLCProvider, }; use crate::util::secp::key::{PublicKey, SecretKey}; -use crate::util::static_secp_instance; +use crate::util::{static_secp_instance, LoggingConfig, ZeroingString}; use crate::{ECDHPubkey, Owner, Token}; use easy_jsonrpc_mw; use rand::thread_rng; /// Public definition used to generate Owner jsonrpc api. -/// Secure version, that should be used when running the owner API in 'Secure' Mode +/// Secure version containing wallet lifecycle functions. All calls to this API must be encrypted. +/// See [`init_secure_api`](#tymethod.init_secure_api) for details of secret derivation +/// and encryption. + #[easy_jsonrpc_mw::rpc] pub trait OwnerRpcS { /** @@ -1274,7 +1279,6 @@ pub trait OwnerRpcS { /** Networked version of [Owner::node_height](struct.Owner.html#method.node_height). - ``` # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( # r#" @@ -1306,11 +1310,283 @@ pub trait OwnerRpcS { fn node_height(&self, token: Token) -> Result; /** - Initializes the secure RPC-JSON API - (Documentation TBD) + Initializes the secure JSON-RPC API. This function must be called and a shared key + established before any other OwnerAPI JSON-RPC function can be called. + + The shared key will be derived using ECDH with the provided public key on the secp256k1 curve. This + function will return its public key used in the derivation, which the caller should multiply by its + private key to derive the shared key. + + Once the key is established, all further requests and responses are encrypted and decrypted with the + following parameters: + * AES-256 in GCM mode with 128-bit tags and 96 bit nonces + * 12 byte nonce which must be included in each request/response to use on the decrypting side + * Empty vector for additional data + * Suffix length = AES-256 GCM mode tag length = 16 bytes + * + + Fully-formed JSON-RPC requests (as documented) should be encrypted using these parameters, encoded + into base64 and included with the one-time nonce in a request for the `encrypted_request_v3` method + as follows: + + ``` + # let s = r#" + { + "jsonrpc": "2.0", + "method": "encrypted_request_v3", + "id": "1", + "params": { + "nonce": "ef32...", + "body_enc": "e0bcd..." + } + } + # "#; + ``` + + With a typical response being: + + ``` + # let s = r#"{ + { + "jsonrpc": "2.0", + "method": "encrypted_response_v3", + "id": "1", + "Ok": { + "nonce": "340b...", + "body_enc": "3f09c..." + } + } + # }"#; + ``` + */ fn init_secure_api(&self, ecdh_pubkey: ECDHPubkey) -> Result; + + /** + Networked version of [Owner::get_top_level_directory](struct.Owner.html#method.get_top_level_directory). + + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "get_top_level_directory", + "params": { + }, + "id": 1 + } + # "# + # , + # r#" + { + "id": 1, + "jsonrpc": "2.0", + "result": { + "Ok": "/doctest/dir" + } + } + # "# + # , true, 5, false, false, false); + ``` + */ + + fn get_top_level_directory(&self) -> Result; + + /** + Networked version of [Owner::set_top_level_directory](struct.Owner.html#method.set_top_level_directory). + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "set_top_level_directory", + "params": { + "dir": "/home/wallet_user/my_wallet_dir" + }, + "id": 1 + } + # "# + # , + # r#" + { + "id": 1, + "jsonrpc": "2.0", + "result": { + "Ok": null + } + } + # "# + # , true, 5, false, false, false); + ``` + */ + + fn set_top_level_directory(&self, dir: String) -> Result<(), ErrorKind>; + + /** + Networked version of [Owner::create_config](struct.Owner.html#method.create_config). + + Both the `wallet_config` and `logging_config` parameters can be `null`, the examples + below are for illustration. Note that the values provided for `log_file_path` and `data_file_dir` + will be ignored and replaced with the actual values based on the value of `get_top_level_directory` + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "create_config", + "params": { + "chain_type": "Mainnet", + "wallet_config": { + "chain_type": null, + "api_listen_interface": "127.0.0.1", + "api_listen_port": 3415, + "owner_api_listen_port": 3420, + "api_secret_path": null, + "node_api_secret_path": null, + "check_node_api_http_addr": "http://127.0.0.1:3413", + "owner_api_include_foreign": false, + "data_file_dir": "/path/to/data/file/dir", + "no_commit_cache": null, + "tls_certificate_file": null, + "tls_certificate_key": null, + "dark_background_color_scheme": null, + "keybase_notify_ttl": null + }, + "logging_config": { + "log_to_stdout": false, + "stdout_log_level": "Info", + "log_to_file": true, + "file_log_level": "Debug", + "log_file_path": "/path/to/log/file", + "log_file_append": true, + "log_max_size": null, + "log_max_files": null, + "tui_running": null + } + }, + "id": 1 + } + # "# + # , + # r#" + { + "id": 1, + "jsonrpc": "2.0", + "result": { + "Ok": null + } + } + # "# + # , true, 5, false, false, false); + ``` + */ + fn create_config( + &self, + chain_type: global::ChainTypes, + wallet_config: Option, + logging_config: Option, + ) -> Result<(), ErrorKind>; + + /** + Networked version of [Owner::create_wallet](struct.Owner.html#method.create_wallet). + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "create_wallet", + "params": { + "name": null, + "mnemonic": null, + "mnemonic_length": 0, + "password": "my_secret_password" + }, + "id": 1 + } + # "# + # , + # r#" + { + "id": 1, + "jsonrpc": "2.0", + "result": { + "Ok": null + } + } + # "# + # , true, 0, false, false, false); + ``` + */ + + fn create_wallet( + &self, + name: Option, + mnemonic: Option, + mnemonic_length: u32, + password: String, + ) -> Result<(), ErrorKind>; + + /** + Networked version of [Owner::open_wallet](struct.Owner.html#method.open_wallet). + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "open_wallet", + "params": { + "name": null, + "password": "my_secret_password" + }, + "id": 1 + } + # "# + # , + # r#" + { + "id": 1, + "jsonrpc": "2.0", + "result": { + "Ok": "d096b3cb75986b3b13f80b8f5243a9edf0af4c74ac37578c5a12cfb5b59b1868" + } + } + # "# + # , true, 0, false, false, false); + ``` + */ + + fn open_wallet(&self, name: Option, password: String) -> Result; + + /** + Networked version of [Owner::close_wallet](struct.Owner.html#method.close_wallet). + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "close_wallet", + "params": { + "name": null + }, + "id": 1 + } + # "# + # , + # r#" + { + "id": 1, + "jsonrpc": "2.0", + "result": { + "Ok": null + } + } + # "# + # , true, 0, false, false, false); + ``` + */ + + fn close_wallet(&self, name: Option) -> Result<(), ErrorKind>; } impl<'a, L, C, K> OwnerRpcS for Owner<'a, L, C, K> @@ -1520,4 +1796,51 @@ where ecdh_pubkey: pub_key, }) } + + fn get_top_level_directory(&self) -> Result { + Owner::get_top_level_directory(self).map_err(|e| e.kind()) + } + + fn set_top_level_directory(&self, dir: String) -> Result<(), ErrorKind> { + Owner::set_top_level_directory(self, &dir).map_err(|e| e.kind()) + } + + fn create_config( + &self, + chain_type: global::ChainTypes, + wallet_config: Option, + logging_config: Option, + ) -> Result<(), ErrorKind> { + Owner::create_config(self, &chain_type, wallet_config, logging_config).map_err(|e| e.kind()) + } + + fn create_wallet( + &self, + name: Option, + mnemonic: Option, + mnemonic_length: u32, + password: String, + ) -> Result<(), ErrorKind> { + let n = name.as_ref().map(|s| s.as_str()); + let m = match mnemonic { + Some(s) => Some(ZeroingString::from(s)), + None => None, + }; + Owner::create_wallet(self, n, m, mnemonic_length, ZeroingString::from(password)) + .map_err(|e| e.kind()) + } + + fn open_wallet(&self, name: Option, password: String) -> Result { + let n = name.as_ref().map(|s| s.as_str()); + let sec_key = Owner::open_wallet(self, n, ZeroingString::from(password), true) + .map_err(|e| e.kind())?; + Ok(Token { + keychain_mask: sec_key, + }) + } + + fn close_wallet(&self, name: Option) -> Result<(), ErrorKind> { + let n = name.as_ref().map(|s| s.as_str()); + Owner::close_wallet(self, n).map_err(|e| e.kind()) + } } diff --git a/config/src/lib.rs b/config/src/lib.rs index 22517945..26db6690 100644 --- a/config/src/lib.rs +++ b/config/src/lib.rs @@ -31,4 +31,4 @@ pub mod config; pub mod types; pub use crate::config::{initial_setup_wallet, GRIN_WALLET_DIR, WALLET_CONFIG_FILE_NAME}; -pub use crate::types::{ConfigError, GlobalWalletConfig, WalletConfig}; +pub use crate::types::{ConfigError, GlobalWalletConfig, GlobalWalletConfigMembers, WalletConfig}; diff --git a/controller/src/command.rs b/controller/src/command.rs index 7ea4bd20..97007a83 100644 --- a/controller/src/command.rs +++ b/controller/src/command.rs @@ -74,12 +74,13 @@ where { let mut w_lock = wallet.lock(); let p = w_lock.lc_provider()?; - p.create_config(&g_args.chain_type, WALLET_CONFIG_FILE_NAME)?; + p.create_config(&g_args.chain_type, WALLET_CONFIG_FILE_NAME, None, None)?; p.create_wallet( None, args.recovery_phrase, args.list_length, args.password.clone(), + false, )?; let m = p.get_mnemonic(None, args.password)?; @@ -121,7 +122,7 @@ pub struct ListenArgs { pub fn listen<'a, L, C, K>( wallet: Arc>>>, - keychain_mask: Option, + keychain_mask: Arc>>, config: &WalletConfig, args: &ListenArgs, g_args: &GlobalArgs, @@ -170,9 +171,12 @@ where C: NodeClient + 'static, K: keychain::Keychain + 'static, { + // keychain mask needs to be a sinlge instance, in case the foreign API is + // also being run at the same time + let km = Arc::new(Mutex::new(keychain_mask)); let res = controller::owner_listener( wallet, - keychain_mask, + km, config.owner_api_listen_addr().as_str(), g_args.node_api_secret.clone(), g_args.tls_conf.clone(), diff --git a/controller/src/controller.rs b/controller/src/controller.rs index 8907f66a..b7d2541c 100644 --- a/controller/src/controller.rs +++ b/controller/src/controller.rs @@ -21,7 +21,7 @@ use crate::libwallet::{ CURRENT_SLATE_VERSION, GRIN_BLOCK_HEADER_VERSION, }; use crate::util::secp::key::SecretKey; -use crate::util::{to_base64, Mutex}; +use crate::util::{from_hex, static_secp_instance, to_base64, Mutex}; use failure::ResultExt; use futures::future::{err, ok}; use futures::{Future, Stream}; @@ -29,6 +29,7 @@ use hyper::header::HeaderValue; use hyper::{Body, Request, Response, StatusCode}; use serde::{Deserialize, Serialize}; use serde_json; +use std::collections::HashMap; use std::net::SocketAddr; use std::sync::Arc; @@ -44,10 +45,6 @@ lazy_static! { HeaderValue::from_str("Basic realm=GrinOwnerAPI").unwrap(); } -lazy_static! { - pub static ref OWNER_API_SHARED_KEY: Arc>> = Arc::new(Mutex::new(None)); -} - fn check_middleware( name: ForeignCheckMiddlewareFn, node_version_info: Option, @@ -122,7 +119,7 @@ where /// in the same wallet instance pub fn owner_listener( wallet: Arc + 'static>>>, - keychain_mask: Option, + keychain_mask: Arc>>, addr: &str, api_secret: Option, tls_config: Option, @@ -143,9 +140,14 @@ where )); router.add_middleware(basic_auth_middleware); } + let mut running_foreign = false; + if owner_api_include_foreign.unwrap_or(false) { + running_foreign = true; + } let api_handler_v2 = OwnerAPIHandlerV2::new(wallet.clone()); - let api_handler_v3 = OwnerAPIHandlerV3::new(wallet.clone()); + let api_handler_v3 = + OwnerAPIHandlerV3::new(wallet.clone(), keychain_mask.clone(), running_foreign); router .add_route("/v2/owner", Arc::new(api_handler_v2)) @@ -156,7 +158,7 @@ where .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) { + if running_foreign { warn!("Starting HTTP Foreign API on Owner server at {}.", addr); let foreign_api_handler_v2 = ForeignAPIHandlerV2::new(wallet, keychain_mask); router @@ -182,7 +184,7 @@ where /// port and wrapping the calls pub fn foreign_listener( wallet: Arc + 'static>>>, - keychain_mask: Option, + keychain_mask: Arc>>, addr: &str, tls_config: Option, ) -> Result<(), Error> @@ -299,6 +301,16 @@ where { /// Wallet instance pub wallet: Arc + 'static>>>, + + /// ECDH shared key + pub shared_key: Arc>>, + + /// Keychain mask (to change if also running the foreign API) + pub keychain_mask: Arc>>, + + /// Whether we're running the foreign API on the same port, and therefore + /// have to store the mask in-process + pub running_foreign: bool, } pub struct OwnerV3Helpers; @@ -316,6 +328,18 @@ impl OwnerV3Helpers { } } + /// Checks whether a request is to open the wallet + pub fn is_open_wallet(val: &serde_json::Value) -> bool { + if let Some(m) = val["method"].as_str() { + match m { + "open_wallet" => true, + _ => false, + } + } else { + false + } + } + /// Checks whether a request is an encrypted request pub fn is_encrypted_request(val: &serde_json::Value) -> bool { if let Some(m) = val["method"].as_str() { @@ -329,15 +353,17 @@ impl OwnerV3Helpers { } /// whether encryption is enabled - pub fn encryption_enabled() -> bool { - let share_key_ref = OWNER_API_SHARED_KEY.lock(); + pub fn encryption_enabled(key: Arc>>) -> bool { + let share_key_ref = key.lock(); share_key_ref.is_some() } /// If incoming is an encrypted request, check there is a shared key, /// Otherwise return an error value - pub fn check_encryption_started() -> Result<(), serde_json::Value> { - match OwnerV3Helpers::encryption_enabled() { + pub fn check_encryption_started( + key: Arc>>, + ) -> Result<(), serde_json::Value> { + match OwnerV3Helpers::encryption_enabled(key) { true => Ok(()), false => Err(EncryptionErrorResponse::new( 1, @@ -349,18 +375,42 @@ impl OwnerV3Helpers { } /// Update the statically held owner API shared key - pub fn update_owner_api_shared_key(val: &serde_json::Value, new_key: Option) { + pub fn update_owner_api_shared_key( + key: Arc>>, + val: &serde_json::Value, + new_key: Option, + ) { if let Some(_) = val["result"]["Ok"].as_str() { - let mut share_key_ref = OWNER_API_SHARED_KEY.lock(); + let mut share_key_ref = key.lock(); *share_key_ref = new_key; } } + /// Update the shared mask, in case of foreign API being run + pub fn update_mask(mask: Arc>>, val: &serde_json::Value) { + if let Some(key) = val["result"]["Ok"].as_str() { + let key_bytes = match from_hex(key.to_owned()) { + Ok(k) => k, + Err(_) => return, + }; + let secp_inst = static_secp_instance(); + let secp = secp_inst.lock(); + let sk = match SecretKey::from_slice(&secp, &key_bytes) { + Ok(s) => s, + Err(_) => return, + }; + + let mut shared_mask_ref = mask.lock(); + *shared_mask_ref = Some(sk); + } + } + /// Decrypt an encrypted request pub fn decrypt_request( + key: Arc>>, req: &serde_json::Value, ) -> Result<(u32, serde_json::Value), serde_json::Value> { - let share_key_ref = OWNER_API_SHARED_KEY.lock(); + let share_key_ref = key.lock(); let shared_key = share_key_ref.as_ref().unwrap(); let enc_req: EncryptedRequest = serde_json::from_value(req.clone()).map_err(|e| { EncryptionErrorResponse::new( @@ -380,10 +430,11 @@ impl OwnerV3Helpers { /// Encrypt a response pub fn encrypt_response( + key: Arc>>, id: u32, res: &serde_json::Value, ) -> Result { - let share_key_ref = OWNER_API_SHARED_KEY.lock(); + let share_key_ref = key.lock(); let shared_key = share_key_ref.as_ref().unwrap(); let enc_res = EncryptedResponse::from_json(id, res, &shared_key).map_err(|e| { EncryptionErrorResponse::new(1, -32003, &format!("EncryptionError: {}", e.kind())) @@ -399,6 +450,74 @@ impl OwnerV3Helpers { })?; Ok(res) } + + /// convert an internal error (if exists) as proper JSON-RPC + pub fn check_error_response(val: &serde_json::Value) -> (bool, serde_json::Value) { + // check for string first. This ensures that error messages + // that are just strings aren't given weird formatting + let err_string = if val["result"]["Err"].is_object() { + let mut retval; + let hashed: Result, serde_json::Error> = + serde_json::from_value(val["result"]["Err"].clone()); + retval = match hashed { + Err(e) => { + debug!("Can't cast value to Hashmap {}", e); + None + } + Ok(h) => { + let mut r = "".to_owned(); + for (k, v) in h.iter() { + r = format!("{}: {}", k, v); + } + Some(r) + } + }; + // Otherwise, see if error message is a map that needs + // to be stringified (and accept weird formatting) + if retval.is_none() { + let hashed: Result, serde_json::Error> = + serde_json::from_value(val["result"]["Err"].clone()); + retval = match hashed { + Err(e) => { + debug!("Can't cast value to Hashmap {}", e); + None + } + Ok(h) => { + let mut r = "".to_owned(); + for (k, v) in h.iter() { + r = format!("{}: {}", k, v); + } + Some(r) + } + } + } + retval + } else if val["result"]["Err"].is_string() { + let parsed = serde_json::from_value::(val["result"]["Err"].clone()); + match parsed { + Ok(p) => Some(p), + Err(_) => None, + } + } else { + None + }; + match err_string { + Some(s) => { + return ( + true, + serde_json::json!({ + "jsonrpc": "2.0", + "id": val["id"], + "error": { + "message": s, + "code": -32099 + } + }), + ) + } + None => (false, val.clone()), + } + } } impl OwnerAPIHandlerV3 @@ -410,8 +529,15 @@ where /// Create a new owner API handler for GET methods pub fn new( wallet: Arc + 'static>>>, + keychain_mask: Arc>>, + running_foreign: bool, ) -> OwnerAPIHandlerV3 { - OwnerAPIHandlerV3 { wallet } + OwnerAPIHandlerV3 { + wallet, + shared_key: Arc::new(Mutex::new(None)), + keychain_mask: keychain_mask, + running_foreign, + } } fn call_api( @@ -419,6 +545,9 @@ where req: Request, api: Owner<'static, L, C, K>, ) -> Box + Send> { + let key = self.shared_key.clone(); + let mask = self.keychain_mask.clone(); + let running_foreign = self.running_foreign; Box::new(parse_body(req).and_then(move |val: serde_json::Value| { let mut val = val; let owner_api_s = &api as &dyn OwnerRpcS; @@ -426,10 +555,10 @@ where let mut was_encrypted = false; let mut encrypted_req_id = 0; if !is_init_secure_api { - if let Err(v) = OwnerV3Helpers::check_encryption_started() { + if let Err(v) = OwnerV3Helpers::check_encryption_started(key.clone()) { return ok(v); } - let res = OwnerV3Helpers::decrypt_request(&val); + let res = OwnerV3Helpers::decrypt_request(key.clone(), &val); match res { Err(e) => return ok(e), Ok(v) => { @@ -441,11 +570,21 @@ where } // check again, in case it was an encrypted call to init_secure_api is_init_secure_api = OwnerV3Helpers::is_init_secure_api(&val); + // also need to intercept open/close wallet requests + let is_open_wallet = OwnerV3Helpers::is_open_wallet(&val); match owner_api_s.handle_request(val) { MaybeReply::Reply(mut r) => { - let unencrypted_intercept = r.clone(); + let (_was_error, unencrypted_intercept) = + OwnerV3Helpers::check_error_response(&r.clone()); + if is_open_wallet && running_foreign { + OwnerV3Helpers::update_mask(mask, &r.clone()); + } if was_encrypted { - let res = OwnerV3Helpers::encrypt_response(encrypted_req_id, &r); + let res = OwnerV3Helpers::encrypt_response( + key.clone(), + encrypted_req_id, + &unencrypted_intercept, + ); r = match res { Ok(v) => v, Err(v) => return ok(v), @@ -455,6 +594,7 @@ where // in case it was an encrypted call to 'init_api_secure') if is_init_secure_api { OwnerV3Helpers::update_owner_api_shared_key( + key.clone(), &unencrypted_intercept, api.shared_key.lock().clone(), ); @@ -510,7 +650,7 @@ where /// Wallet instance pub wallet: Arc + 'static>>>, /// Keychain mask - pub keychain_mask: Option, + pub keychain_mask: Arc>>, } impl ForeignAPIHandlerV2 @@ -522,7 +662,7 @@ where /// Create a new foreign API handler for GET methods pub fn new( wallet: Arc + 'static>>>, - keychain_mask: Option, + keychain_mask: Arc>>, ) -> ForeignAPIHandlerV2 { ForeignAPIHandlerV2 { wallet, @@ -549,11 +689,8 @@ where } fn handle_post_request(&self, req: Request) -> WalletResponseFuture { - let api = Foreign::new( - self.wallet.clone(), - self.keychain_mask.clone(), - Some(check_middleware), - ); + let mask = self.keychain_mask.lock(); + let api = Foreign::new(self.wallet.clone(), mask.clone(), Some(check_middleware)); Box::new( self.call_api(req, api) .and_then(|resp| ok(json_response_pretty(&resp))), diff --git a/controller/tests/common/mod.rs b/controller/tests/common/mod.rs index 596f6370..84147faa 100644 --- a/controller/tests/common/mod.rs +++ b/controller/tests/common/mod.rs @@ -121,8 +121,8 @@ pub fn create_local_wallet( >, >; let lc = wallet.lc_provider().unwrap(); - lc.set_wallet_directory(&format!("{}/{}", test_dir, name)); - lc.create_wallet(None, mnemonic, 32, ZeroingString::from("")) + let _ = lc.set_top_level_directory(&format!("{}/{}", test_dir, name)); + lc.create_wallet(None, mnemonic, 32, ZeroingString::from(""), false) .unwrap(); let mask = lc .open_wallet(None, ZeroingString::from(""), create_mask, false) @@ -160,7 +160,7 @@ pub fn open_local_wallet( >, >; let lc = wallet.lc_provider().unwrap(); - lc.set_wallet_directory(&format!("{}/{}", test_dir, name)); + let _ = lc.set_top_level_directory(&format!("{}/{}", test_dir, name)); let mask = lc .open_wallet(None, ZeroingString::from(""), create_mask, false) .unwrap(); diff --git a/impls/src/adapters/keybase.rs b/impls/src/adapters/keybase.rs index d0df5228..f8ac8758 100644 --- a/impls/src/adapters/keybase.rs +++ b/impls/src/adapters/keybase.rs @@ -367,7 +367,7 @@ impl SlateReceiver for KeybaseAllChannels { >, >; let lc = wallet.lc_provider().unwrap(); - lc.set_wallet_directory(&config.data_file_dir); + lc.set_top_level_directory(&config.data_file_dir)?; let mask = lc.open_wallet(None, passphrase, true, false)?; let wallet_inst = lc.wallet_inst()?; wallet_inst.set_parent_key_id_by_name(account)?; diff --git a/impls/src/lifecycle/default.rs b/impls/src/lifecycle/default.rs index 1627091e..bf0a11b8 100644 --- a/impls/src/lifecycle/default.rs +++ b/impls/src/lifecycle/default.rs @@ -14,7 +14,9 @@ //! Default wallet lifecycle provider -use crate::config::{config, GlobalWalletConfig, GRIN_WALLET_DIR}; +use crate::config::{ + config, GlobalWalletConfig, GlobalWalletConfigMembers, WalletConfig, GRIN_WALLET_DIR, +}; use crate::core::global; use crate::keychain::Keychain; use crate::libwallet::{Error, ErrorKind, NodeClient, WalletBackend, WalletLCProvider}; @@ -23,6 +25,8 @@ use crate::util::secp::key::SecretKey; use crate::util::ZeroingString; use crate::LMDBBackend; use failure::ResultExt; +use grin_wallet_util::grin_util::LoggingConfig; +use std::fs; use std::path::PathBuf; pub struct DefaultLCProvider<'a, C, K> @@ -55,15 +59,51 @@ where C: NodeClient + 'a, K: Keychain + 'a, { - fn set_wallet_directory(&mut self, dir: &str) { + fn set_top_level_directory(&mut self, dir: &str) -> Result<(), Error> { self.data_dir = dir.to_owned(); + Ok(()) } - fn create_config(&self, chain_type: &global::ChainTypes, file_name: &str) -> Result<(), Error> { + fn get_top_level_directory(&self) -> Result { + Ok(self.data_dir.to_owned()) + } + + fn create_config( + &self, + chain_type: &global::ChainTypes, + file_name: &str, + wallet_config: Option, + logging_config: Option, + ) -> Result<(), Error> { let mut default_config = GlobalWalletConfig::for_chain(chain_type); + let logging = match logging_config { + Some(l) => Some(l), + None => match default_config.members.as_ref() { + Some(m) => m.clone().logging.clone(), + None => None, + }, + }; + let wallet = match wallet_config { + Some(w) => w, + None => match default_config.members { + Some(m) => m.wallet, + None => WalletConfig::default(), + }, + }; + default_config = GlobalWalletConfig { + members: Some(GlobalWalletConfigMembers { wallet, logging }), + ..default_config + }; let mut config_file_name = PathBuf::from(self.data_dir.clone()); config_file_name.push(file_name); + // create top level dir if it doesn't exist + let dd = PathBuf::from(self.data_dir.clone()); + if !dd.exists() { + // try create + fs::create_dir_all(dd)?; + } + let mut data_dir_name = PathBuf::from(self.data_dir.clone()); data_dir_name.push(GRIN_WALLET_DIR); @@ -81,7 +121,10 @@ where return Ok(()); } - default_config.update_paths(&PathBuf::from(self.data_dir.clone())); + let mut abs_path = std::env::current_dir()?; + abs_path.push(self.data_dir.clone()); + + default_config.update_paths(&PathBuf::from(abs_path)); let res = default_config.write_to_file(config_file_name.to_str().unwrap()); if let Err(e) = res { let msg = format!( @@ -114,10 +157,18 @@ where mnemonic: Option, mnemonic_length: usize, password: ZeroingString, + test_mode: bool, ) -> Result<(), Error> { let mut data_dir_name = PathBuf::from(self.data_dir.clone()); data_dir_name.push(GRIN_WALLET_DIR); let data_dir_name = data_dir_name.to_str().unwrap(); + let exists = WalletSeed::seed_file_exists(&data_dir_name); + if !test_mode { + if let Ok(true) = exists { + let msg = format!("Wallet seed already exists at: {}", data_dir_name); + return Err(ErrorKind::WalletSeedExists(msg))?; + } + } let _ = WalletSeed::init_file(&data_dir_name, mnemonic_length, mnemonic, password); info!("Wallet seed file created"); let _wallet: LMDBBackend<'a, C, K> = diff --git a/impls/src/lifecycle/seed.rs b/impls/src/lifecycle/seed.rs index fc272060..a88f27ae 100644 --- a/impls/src/lifecycle/seed.rs +++ b/impls/src/lifecycle/seed.rs @@ -86,7 +86,7 @@ impl WalletSeed { pub fn seed_file_exists(data_file_dir: &str) -> Result { let seed_file_path = &format!("{}{}{}", data_file_dir, MAIN_SEPARATOR, SEED_FILE,); - println!("Seed file path: {}", seed_file_path); + debug!("Seed file path: {}", seed_file_path); if Path::new(seed_file_path).exists() { Ok(true) } else { @@ -123,9 +123,9 @@ impl WalletSeed { password: util::ZeroingString, ) -> Result<(), Error> { let seed_file_path = &format!("{}{}{}", data_file_dir, MAIN_SEPARATOR, SEED_FILE,); - println!("data file dir: {}", data_file_dir); + debug!("data file dir: {}", data_file_dir); if let Ok(true) = WalletSeed::seed_file_exists(data_file_dir) { - println!("seed file exists"); + debug!("seed file exists"); WalletSeed::backup_seed(data_file_dir)?; } if !Path::new(&data_file_dir).exists() { @@ -157,7 +157,11 @@ impl WalletSeed { let seed_file_path = &format!("{}{}{}", data_file_dir, MAIN_SEPARATOR, SEED_FILE,); warn!("Generating wallet seed file at: {}", seed_file_path); - let _ = WalletSeed::seed_file_exists(data_file_dir)?; + let exists = WalletSeed::seed_file_exists(data_file_dir)?; + if exists { + let msg = format!("Wallet seed already exists at: {}", data_file_dir); + return Err(ErrorKind::WalletSeedExists(msg))?; + } let seed = match recovery_phrase { Some(p) => WalletSeed::from_mnemonic(p)?, diff --git a/libwallet/Cargo.toml b/libwallet/Cargo.toml index 04429311..44a69c4d 100644 --- a/libwallet/Cargo.toml +++ b/libwallet/Cargo.toml @@ -26,6 +26,4 @@ strum = "0.15" strum_macros = "0.15" grin_wallet_util = { path = "../util", version = "2.1.0-beta.1" } - -[dev-dependencies] grin_wallet_config = { path = "../config", version = "2.1.0-beta.1" } diff --git a/libwallet/src/api_impl/owner.rs b/libwallet/src/api_impl/owner.rs index 2b0694bb..f010a7f3 100644 --- a/libwallet/src/api_impl/owner.rs +++ b/libwallet/src/api_impl/owner.rs @@ -274,6 +274,7 @@ where // recieve the transaction back { let mut batch = w.batch(keychain_mask)?; + println!("Saving private context: {:?}", slate.id.as_bytes()); batch.save_private_context(slate.id.as_bytes(), 1, &context)?; batch.commit()?; } diff --git a/libwallet/src/error.rs b/libwallet/src/error.rs index ac13e5e1..31a24710 100644 --- a/libwallet/src/error.rs +++ b/libwallet/src/error.rs @@ -126,8 +126,8 @@ pub enum ErrorKind { DuplicateTransactionId, /// Wallet seed already exists - #[fail(display = "Wallet seed exists error")] - WalletSeedExists, + #[fail(display = "Wallet seed exists error: {}", _0)] + WalletSeedExists(String), /// Wallet seed doesn't exist #[fail(display = "Wallet seed doesn't exist error")] diff --git a/libwallet/src/lib.rs b/libwallet/src/lib.rs index 9e6fb795..2d46f4e8 100644 --- a/libwallet/src/lib.rs +++ b/libwallet/src/lib.rs @@ -22,6 +22,7 @@ #![deny(unused_mut)] #![warn(missing_docs)] +use grin_wallet_config as config; use grin_wallet_util::grin_core; use grin_wallet_util::grin_keychain; use grin_wallet_util::grin_store; diff --git a/libwallet/src/slate_versions/mod.rs b/libwallet/src/slate_versions/mod.rs index c6c33dfc..a5db155a 100644 --- a/libwallet/src/slate_versions/mod.rs +++ b/libwallet/src/slate_versions/mod.rs @@ -37,7 +37,7 @@ pub enum SlateVersion { V2, } -#[derive(Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize)] #[serde(untagged)] /// Versions are ordered newest to oldest so serde attempts to /// deserialize newer versions first, then falls back to older versions. diff --git a/libwallet/src/types.rs b/libwallet/src/types.rs index d05e7150..65729db2 100644 --- a/libwallet/src/types.rs +++ b/libwallet/src/types.rs @@ -15,6 +15,7 @@ //! Types and traits that should be provided by a wallet //! implementation +use crate::config::WalletConfig; use crate::error::{Error, ErrorKind}; use crate::grin_core::core::hash::Hash; use crate::grin_core::core::{Output, Transaction, TxKernel}; @@ -23,7 +24,7 @@ use crate::grin_core::{global, ser}; use crate::grin_keychain::{Identifier, Keychain}; use crate::grin_util::secp::key::{PublicKey, SecretKey}; use crate::grin_util::secp::{self, pedersen, Secp256k1}; -use crate::grin_util::ZeroingString; +use crate::grin_util::{LoggingConfig, ZeroingString}; use crate::slate::ParticipantMessages; use chrono::prelude::*; use failure::ResultExt; @@ -52,10 +53,20 @@ where { /// Sets the top level system wallet directory /// default is assumed to be ~/.grin/main/wallet_data (or floonet equivalent) - fn set_wallet_directory(&mut self, dir: &str); + fn set_top_level_directory(&mut self, dir: &str) -> Result<(), Error>; + + /// Sets the top level system wallet directory + /// default is assumed to be ~/.grin/main/wallet_data (or floonet equivalent) + fn get_top_level_directory(&self) -> Result; /// Output a grin-wallet.toml file into the current top-level system wallet directory - fn create_config(&self, chain_type: &global::ChainTypes, file_name: &str) -> Result<(), Error>; + fn create_config( + &self, + chain_type: &global::ChainTypes, + file_name: &str, + wallet_config: Option, + logging_config: Option, + ) -> Result<(), Error>; /// fn create_wallet( @@ -64,6 +75,7 @@ where mnemonic: Option, mnemonic_length: usize, password: ZeroingString, + test_mode: bool, ) -> Result<(), Error>; /// diff --git a/src/bin/grin-wallet.rs b/src/bin/grin-wallet.rs index 8c3d607f..11bcce5a 100644 --- a/src/bin/grin-wallet.rs +++ b/src/bin/grin-wallet.rs @@ -23,6 +23,7 @@ use crate::core::global; use crate::util::init_logger; use clap::App; use grin_wallet_config as config; +use grin_wallet_impls::HTTPNodeClient; use grin_wallet_util::grin_core as core; use grin_wallet_util::grin_util as util; use std::env; @@ -123,5 +124,8 @@ fn real_main() -> i32 { .clone(), ); - cmd::wallet_command(&args, config) + let wallet_config = config.clone().members.unwrap().wallet; + let node_client = HTTPNodeClient::new(&wallet_config.check_node_api_http_addr, None); + + cmd::wallet_command(&args, config, node_client) } diff --git a/src/bin/grin-wallet.yml b/src/bin/grin-wallet.yml index 2eb7e106..d63fb526 100644 --- a/src/bin/grin-wallet.yml +++ b/src/bin/grin-wallet.yml @@ -76,6 +76,10 @@ subcommands: short: l long: port takes_value: true + - run_foreign: + help: Also run the Foreign API + long: run_foreign + takes_value: false - send: about: Builds a transaction to send coins and sends to the specified listener directly args: @@ -168,7 +172,7 @@ subcommands: short: f long: fluff - invoice: - about: Initialize an invoice transction. + about: Initialize an invoice transaction. args: - amount: help: Number of coins to invoice with optional fraction, e.g. 12.423 @@ -268,7 +272,7 @@ subcommands: short: f long: fluff - cancel: - about: Cancels an previously created transaction, freeing previously locked outputs for use again + about: Cancels a previously created transaction, freeing previously locked outputs for use again args: - id: help: The ID of the transaction to cancel diff --git a/src/cmd/wallet.rs b/src/cmd/wallet.rs index 7ce082a8..50354790 100644 --- a/src/cmd/wallet.rs +++ b/src/cmd/wallet.rs @@ -15,7 +15,6 @@ use crate::cmd::wallet_args; use crate::config::GlobalWalletConfig; use clap::ArgMatches; -use grin_wallet_impls::HTTPNodeClient; use grin_wallet_libwallet::NodeClient; use semver::Version; use std::thread; @@ -23,12 +22,19 @@ use std::time::Duration; const MIN_COMPAT_NODE_VERSION: &str = "2.0.0-beta.1"; -pub fn wallet_command(wallet_args: &ArgMatches<'_>, config: GlobalWalletConfig) -> i32 { +pub fn wallet_command( + wallet_args: &ArgMatches<'_>, + config: GlobalWalletConfig, + mut node_client: C, +) -> i32 +where + C: NodeClient + 'static, +{ // just get defaults from the global config let wallet_config = config.members.unwrap().wallet; // Check the node version info, and exit with report if we're not compatible - let mut node_client = HTTPNodeClient::new(&wallet_config.check_node_api_http_addr, None); + //let mut node_client = HTTPNodeClient::new(&wallet_config.check_node_api_http_addr, None); let global_wallet_args = wallet_args::parse_global_args(&wallet_config, &wallet_args) .expect("Can't read configuration file"); node_client.set_node_api_secret(global_wallet_args.node_api_secret.clone()); @@ -51,7 +57,7 @@ pub fn wallet_command(wallet_args: &ArgMatches<'_>, config: GlobalWalletConfig) } // ... if node isn't available, allow offline functions - let res = wallet_args::wallet_command(wallet_args, wallet_config, node_client, false); + let res = wallet_args::wallet_command(wallet_args, wallet_config, node_client, false, |_| {}); // we need to give log output a chance to catch up before exiting thread::sleep(Duration::from_millis(100)); diff --git a/src/cmd/wallet_args.rs b/src/cmd/wallet_args.rs index 877b90e8..4715be35 100644 --- a/src/cmd/wallet_args.rs +++ b/src/cmd/wallet_args.rs @@ -228,7 +228,7 @@ where let mut wallet = Box::new(DefaultWalletImpl::<'static, C>::new(node_client.clone()).unwrap()) as Box>; let lc = wallet.lc_provider().unwrap(); - lc.set_wallet_directory(&config.data_file_dir); + let _ = lc.set_top_level_directory(&config.data_file_dir); Ok(Arc::new(Mutex::new(wallet))) } @@ -403,6 +403,9 @@ pub fn parse_owner_api_args( if let Some(port) = args.value_of("port") { config.owner_api_listen_port = Some(port.parse().unwrap()); } + if args.is_present("run_foreign") { + config.owner_api_include_foreign = Some(true); + } Ok(()) } @@ -761,14 +764,29 @@ pub fn parse_cancel_args(args: &ArgMatches) -> Result( +pub fn wallet_command( wallet_args: &ArgMatches, mut wallet_config: WalletConfig, mut node_client: C, test_mode: bool, + wallet_inst_cb: F, ) -> Result where C: NodeClient + 'static + Clone, + F: FnOnce( + Arc< + Mutex< + Box< + dyn WalletInst< + 'static, + DefaultLCProvider<'static, C, keychain::ExtKeychain>, + C, + keychain::ExtKeychain, + >, + >, + >, + >, + ), { if let Some(t) = wallet_config.chain_type.clone() { core::global::set_mining_mode(t); @@ -814,17 +832,29 @@ where { let mut wallet_lock = wallet.lock(); let lc = wallet_lock.lc_provider().unwrap(); - lc.set_wallet_directory(&wallet_config.data_file_dir); + let _ = lc.set_top_level_directory(&wallet_config.data_file_dir); } + // provide wallet instance back to the caller (handy for testing with + // local wallet proxy, etc) + wallet_inst_cb(wallet.clone()); + // don't open wallet for certain lifecycle commands - let keychain_mask = match wallet_args.subcommand() { - ("init", Some(_)) => None, - ("recover", _) => None, - // Owner API can be started without a wallet present - // TODO: Not quite yet, next PR will deal with this - //("owner_api", _) => None, - _ => { + let mut open_wallet = true; + match wallet_args.subcommand() { + ("init", Some(_)) => open_wallet = false, + ("recover", _) => open_wallet = false, + ("owner_api", _) => { + // If wallet exists, open it. Otherwise, that's fine too. + let mut wallet_lock = wallet.lock(); + let lc = wallet_lock.lc_provider().unwrap(); + open_wallet = lc.wallet_exists(None)?; + } + _ => {} + } + + let keychain_mask = match open_wallet { + true => { let mut wallet_lock = wallet.lock(); let lc = wallet_lock.lc_provider().unwrap(); let mask = lc.open_wallet( @@ -839,6 +869,7 @@ where } mask } + false => None, }; let km = (&keychain_mask).as_ref(); @@ -864,7 +895,13 @@ where ("listen", Some(args)) => { let mut c = wallet_config.clone(); let a = arg_parse!(parse_listen_args(&mut c, &args)); - command::listen(wallet, keychain_mask, &c, &a, &global_wallet_args.clone()) + command::listen( + wallet, + Arc::new(Mutex::new(keychain_mask)), + &c, + &a, + &global_wallet_args.clone(), + ) } ("owner_api", Some(args)) => { let mut c = wallet_config.clone(); @@ -950,7 +987,7 @@ where command::check_repair(wallet, km, a) } _ => { - let msg = format!("Unknown wallet command, use 'grin help wallet' for details"); + let msg = format!("Unknown wallet command, use 'grin-wallet help' for details"); return Err(ErrorKind::ArgumentError(msg).into()); } }; diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 2830143c..b584ea04 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -27,7 +27,7 @@ use util::{Mutex, ZeroingString}; use grin_wallet_api::{EncryptedRequest, EncryptedResponse}; use grin_wallet_config::{GlobalWalletConfig, WalletConfig, GRIN_WALLET_DIR}; use grin_wallet_impls::{DefaultLCProvider, DefaultWalletImpl}; -use grin_wallet_libwallet::{WalletInfo, WalletInst}; +use grin_wallet_libwallet::{NodeClient, WalletInfo, WalletInst}; use grin_wallet_util::grin_core::global::{self, ChainTypes}; use grin_wallet_util::grin_keychain::ExtKeychain; use grin_wallet_util::grin_util::{from_hex, static_secp_instance}; @@ -149,6 +149,7 @@ pub fn config_command_wallet( } /// Handles setup and detection of paths for wallet +#[allow(dead_code)] 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| { @@ -185,6 +186,7 @@ fn get_wallet_subcommand<'a>( } // // Helper to create an instance of the LMDB wallet +#[allow(dead_code)] pub fn instantiate_wallet( mut wallet_config: WalletConfig, node_client: LocalWalletClient, @@ -226,7 +228,7 @@ pub fn instantiate_wallet( top_level_wallet_dir.pop(); wallet_config.data_file_dir = top_level_wallet_dir.to_str().unwrap().into(); } - lc.set_wallet_directory(&wallet_config.data_file_dir); + let _ = lc.set_top_level_directory(&wallet_config.data_file_dir); let keychain_mask = lc .open_wallet(None, ZeroingString::from(passphrase), true, false) .unwrap(); @@ -235,6 +237,7 @@ pub fn instantiate_wallet( Ok((Arc::new(Mutex::new(wallet)), keychain_mask)) } +#[allow(dead_code)] pub fn execute_command( app: &App, test_dir: &str, @@ -247,18 +250,36 @@ pub fn execute_command( 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(), true) + wallet_args::wallet_command(&args, config.clone(), client.clone(), true, |_| {}) } // as above, but without necessarily setting up the wallet #[allow(dead_code)] -pub fn execute_command_no_setup( +pub fn execute_command_no_setup( app: &App, test_dir: &str, wallet_name: &str, - client: &LocalWalletClient, + client: &C, arg_vec: Vec<&str>, -) -> Result { + f: F, +) -> Result +where + C: NodeClient + 'static + Clone, + F: FnOnce( + Arc< + Mutex< + Box< + dyn WalletInst< + 'static, + DefaultLCProvider<'static, C, ExtKeychain>, + C, + ExtKeychain, + >, + >, + >, + >, + ), +{ let args = app.clone().get_matches_from(arg_vec); let _ = get_wallet_subcommand(test_dir, wallet_name, args.clone()); let config = config::initial_setup_wallet(&ChainTypes::AutomatedTesting, None).unwrap(); @@ -266,7 +287,7 @@ pub fn execute_command_no_setup( wallet_config.chain_type = None; wallet_config.api_secret_path = None; wallet_config.node_api_secret_path = None; - wallet_args::wallet_command(&args, wallet_config, client.clone(), true) + wallet_args::wallet_command(&args, wallet_config, client.clone(), true, f) } pub fn post(url: &Url, api_secret: Option, input: &IN) -> Result @@ -367,6 +388,7 @@ where .clone() .unwrap(); + //println!("RES: {}", res); if res["Err"] != json!(null) { Ok(Err(WalletAPIReturnError { message: res["Err"].as_str().unwrap().to_owned(), @@ -374,8 +396,21 @@ where })) } else { // deserialize result into expected type - let value: OUT = serde_json::from_value(res["Ok"].clone()).unwrap(); - Ok(Ok(value)) + let raw_value = res["Ok"].clone(); + let raw_value_str = serde_json::to_string_pretty(&raw_value).unwrap(); + //println!("Raw value: {}", raw_value_str); + let ok_val = serde_json::from_str(&raw_value_str); + match ok_val { + Ok(v) => { + let value: OUT = v; + Ok(Ok(value)) + } + Err(e) => { + println!("Error deserializing: {:?}", e); + let value: OUT = serde_json::from_value(json!("Null")).unwrap(); + Ok(Ok(value)) + } + } } } diff --git a/tests/data/v2_reqs/init_send_tx.req.json b/tests/data/v2_reqs/init_send_tx.req.json new file mode 100644 index 00000000..1ceb75f8 --- /dev/null +++ b/tests/data/v2_reqs/init_send_tx.req.json @@ -0,0 +1,19 @@ +{ + "jsonrpc": "2.0", + "method": "init_send_tx", + "params": { + "args": { + "src_acct_name": null, + "amount": "600000000000", + "minimum_confirmations": 2, + "max_outputs": 500, + "num_change_outputs": 1, + "selection_strategy_is_use_all": true, + "message": "my message", + "target_slate_version": null, + "send_args": null + } + }, + "id": 1 +} + diff --git a/tests/data/v3_reqs/close_wallet.req.json b/tests/data/v3_reqs/close_wallet.req.json new file mode 100644 index 00000000..fedb965e --- /dev/null +++ b/tests/data/v3_reqs/close_wallet.req.json @@ -0,0 +1,8 @@ +{ + "jsonrpc": "2.0", + "method": "close_wallet", + "params": { + "name": null + }, + "id": 1 +} diff --git a/tests/data/v3_reqs/create_config.req.json b/tests/data/v3_reqs/create_config.req.json new file mode 100644 index 00000000..f88b97af --- /dev/null +++ b/tests/data/v3_reqs/create_config.req.json @@ -0,0 +1,10 @@ +{ + "jsonrpc": "2.0", + "method": "create_config", + "params": { + "chain_type": "AutomatedTesting", + "wallet_config": null, + "logging_config": null + }, + "id": 1 +} diff --git a/tests/data/v3_reqs/create_wallet.req.json b/tests/data/v3_reqs/create_wallet.req.json new file mode 100644 index 00000000..e97e5d8c --- /dev/null +++ b/tests/data/v3_reqs/create_wallet.req.json @@ -0,0 +1,11 @@ +{ + "jsonrpc": "2.0", + "method": "create_wallet", + "params": { + "name": null, + "mnemonic": null, + "mnemonic_length": 0, + "password": "passwoid" + }, + "id": 1 +} diff --git a/tests/data/v3_reqs/get_top_level.req.json b/tests/data/v3_reqs/get_top_level.req.json new file mode 100644 index 00000000..0ebb2eaf --- /dev/null +++ b/tests/data/v3_reqs/get_top_level.req.json @@ -0,0 +1,7 @@ +{ + "jsonrpc": "2.0", + "method": "get_top_level_directory", + "params": { + }, + "id": 1 +} diff --git a/tests/data/v3_reqs/init_send_tx.req.json b/tests/data/v3_reqs/init_send_tx.req.json new file mode 100644 index 00000000..b0f6587a --- /dev/null +++ b/tests/data/v3_reqs/init_send_tx.req.json @@ -0,0 +1,20 @@ +{ + "jsonrpc": "2.0", + "method": "init_send_tx", + "params": { + "token": null, + "args": { + "src_acct_name": null, + "amount": "600000000000", + "minimum_confirmations": 2, + "max_outputs": 500, + "num_change_outputs": 1, + "selection_strategy_is_use_all": true, + "message": "my message", + "target_slate_version": null, + "send_args": null + } + }, + "id": 1 +} + diff --git a/tests/data/v3_reqs/open_wallet.req.json b/tests/data/v3_reqs/open_wallet.req.json index 9e5d096c..5eb4a5a3 100644 --- a/tests/data/v3_reqs/open_wallet.req.json +++ b/tests/data/v3_reqs/open_wallet.req.json @@ -2,9 +2,8 @@ "jsonrpc": "2.0", "method": "open_wallet", "params": { - "token": null, - "refresh_from_node": true, - "minimum_confirmations": 1 + "name": null, + "password": "passwoid" }, "id": 1 } diff --git a/tests/owner_v3_init_secure.rs b/tests/owner_v3_init_secure.rs index 4693c134..69432ff4 100644 --- a/tests/owner_v3_init_secure.rs +++ b/tests/owner_v3_init_secure.rs @@ -197,7 +197,7 @@ fn owner_v3_init_secure() -> Result<(), grin_wallet_controller::Error> { println!("RES 11: {:?}", res); assert!(res.is_ok()); - // 12) A request which triggers and API error (not an encryption error) + // 12) A request which triggers an API error (not an encryption error) let req = serde_json::json!({ "jsonrpc": "2.0", "id": 1, @@ -213,6 +213,14 @@ fn owner_v3_init_secure() -> Result<(), grin_wallet_controller::Error> { assert!(res.is_err()); assert_eq!(res.unwrap_err().code, -32601); + // 13) A request which triggers an internal API error (not enough funds) + let req = include_str!("data/v3_reqs/init_send_tx.req.json"); + let res = + send_request_enc::(13, 1, "http://127.0.0.1:33420/v3/owner", &req, &shared_key)?; + println!("RES 13: {:?}", res); + assert!(res.is_err()); + assert_eq!(res.unwrap_err().code, -32099); + clean_output_dir(test_dir); Ok(()) diff --git a/tests/owner_v3_lifecycle.rs b/tests/owner_v3_lifecycle.rs new file mode 100644 index 00000000..0d5164eb --- /dev/null +++ b/tests/owner_v3_lifecycle.rs @@ -0,0 +1,374 @@ +// 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. + +#[macro_use] +extern crate clap; + +#[macro_use] +extern crate log; + +extern crate grin_wallet; + +use grin_wallet_api::ECDHPubkey; +use grin_wallet_impls::test_framework::{self, LocalWalletClient, WalletProxy}; + +use clap::App; +use std::thread; +use std::time::Duration; + +use grin_wallet_impls::DefaultLCProvider; +use grin_wallet_libwallet::{InitTxArgs, Slate, SlateVersion, VersionedSlate}; +use grin_wallet_util::grin_keychain::ExtKeychain; +use serde_json; + +use grin_wallet_util::grin_util::Mutex; +use std::path::PathBuf; +use std::sync::Arc; + +#[macro_use] +mod common; +use common::{ + clean_output_dir, derive_ecdh_key, execute_command, execute_command_no_setup, + initial_setup_wallet, instantiate_wallet, send_request, send_request_enc, setup, + RetrieveSummaryInfoResp, +}; + +#[test] +fn owner_v3_lifecycle() -> Result<(), grin_wallet_controller::Error> { + let test_dir = "target/test_output/owner_v3_lifecycle"; + setup(test_dir); + + let yml = load_yaml!("../src/bin/grin-wallet.yml"); + let app = App::from_yaml(yml); + + // Create a new proxy to simulate server and wallet responses + let wallet_proxy_a: Arc< + Mutex< + WalletProxy< + DefaultLCProvider<'static, LocalWalletClient, ExtKeychain>, + LocalWalletClient, + ExtKeychain, + >, + >, + > = Arc::new(Mutex::new(WalletProxy::new(test_dir))); + let (chain, wallet2, mask2_i) = { + let mut wallet_proxy = wallet_proxy_a.lock(); + let chain = wallet_proxy.chain.clone(); + + // Create wallet 2 manually, which will mine a bit and insert some + // grins into the equation + let client2 = LocalWalletClient::new("wallet2", wallet_proxy.tx.clone()); + let arg_vec = vec!["grin-wallet", "-p", "password", "init", "-h"]; + execute_command(&app, test_dir, "wallet2", &client2, arg_vec.clone())?; + + let config2 = initial_setup_wallet(test_dir, "wallet2"); + //config2.api_listen_port = 23415; + let (wallet2, mask2_i) = + instantiate_wallet(config2.clone(), client2.clone(), "password", "default")?; + wallet_proxy.add_wallet( + "wallet2", + client2.get_send_instance(), + wallet2.clone(), + mask2_i.clone(), + ); + + // start up the owner api with wallet created + let arg_vec = vec!["grin-wallet", "owner_api", "-l", "43420", "--run_foreign"]; + // should create new wallet file + let client1 = LocalWalletClient::new("wallet1", wallet_proxy.tx.clone()); + + let p = wallet_proxy_a.clone(); + + thread::spawn(move || { + let yml = load_yaml!("../src/bin/grin-wallet.yml"); + let app = App::from_yaml(yml); + execute_command_no_setup( + &app, + test_dir, + "wallet1", + &client1, + arg_vec.clone(), + |wallet_inst| { + let mut wallet_proxy = p.lock(); + wallet_proxy.add_wallet( + "wallet1", + client1.get_send_instance(), + wallet_inst, + None, + ); + }, + ) + .unwrap(); + }); + (chain, wallet2, mask2_i) + }; + // give a bit for wallet to init and populate proxy with wallet via callback in thread above + thread::sleep(Duration::from_millis(500)); + let mask2 = (&mask2_i).as_ref(); + let wallet_proxy = wallet_proxy_a.clone(); + + // Set the wallet proxy listener running + thread::spawn(move || { + let mut p = wallet_proxy.lock(); + if let Err(e) = p.run() { + error!("Wallet Proxy error: {}", e); + } + }); + + // mine into wallet 2 a bit + let bh = 10u64; + let _ = + test_framework::award_blocks_to_wallet(&chain, wallet2.clone(), mask2, bh as usize, false); + + // We have an owner API with no wallet initialized. Init the secure API + let sec_key_str = "e00dcc4a009e3427c6b1e1a550c538179d46f3827a13ed74c759c860761caf1e"; + let req = include_str!("data/v3_reqs/init_secure_api.req.json"); + let res = send_request(1, "http://127.0.0.1:43420/v3/owner", req)?; + println!("RES 1: {:?}", res); + + assert!(res.is_ok()); + let value: ECDHPubkey = res.unwrap(); + let shared_key = derive_ecdh_key(sec_key_str, &value.ecdh_pubkey); + + // 2) get the top level directory, should default to ~/.grin/auto + let req = include_str!("data/v3_reqs/get_top_level.req.json"); + let res = + send_request_enc::(1, 1, "http://127.0.0.1:43420/v3/owner", &req, &shared_key)?; + println!("RES 2: {:?}", res); + assert!(res.is_ok()); + assert!(res.unwrap().contains("auto")); + + // 3) now set the top level directory to our test wallet dir + let req = serde_json::json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "set_top_level_directory", + "params": { + "dir": format!("{}/wallet1", test_dir) + } + }); + let res = send_request_enc::( + 1, + 1, + "http://127.0.0.1:43420/v3/owner", + &req.to_string(), + &shared_key, + )?; + println!("RES 3: {:?}", res); + assert!(res.is_ok()); + + // 4) create a configuration file in top level directory + let req = include_str!("data/v3_reqs/create_config.req.json"); + let res = + send_request_enc::(1, 1, "http://127.0.0.1:43420/v3/owner", &req, &shared_key)?; + println!("RES 4: {:?}", res); + assert!(res.is_ok()); + let pb = PathBuf::from(format!("{}/wallet1/grin-wallet.toml", test_dir)); + assert!(pb.exists()); + + // 5) Try and perform an operation without having a wallet open + let req = include_str!("data/v3_reqs/retrieve_info.req.json"); + let res = + send_request_enc::(1, 1, "http://127.0.0.1:43420/v3/owner", &req, &shared_key)?; + println!("RES 5: {:?}", res); + assert!(res.is_err()); + + // 6) Create a wallet + let req = include_str!("data/v3_reqs/create_wallet.req.json"); + let res = + send_request_enc::(1, 1, "http://127.0.0.1:43420/v3/owner", &req, &shared_key)?; + println!("RES 6: {:?}", res); + assert!(res.is_ok()); + + // 7) Try and create a wallet when one exists + let req = include_str!("data/v3_reqs/create_wallet.req.json"); + let res = + send_request_enc::(1, 1, "http://127.0.0.1:43420/v3/owner", &req, &shared_key)?; + println!("RES 7: {:?}", res); + assert!(res.is_err()); + + // 8) Open the wallet + let req = include_str!("data/v3_reqs/open_wallet.req.json"); + let res = + send_request_enc::(1, 1, "http://127.0.0.1:43420/v3/owner", &req, &shared_key)?; + println!("RES 8: {:?}", res); + assert!(res.is_ok()); + let token = res.unwrap(); + + // 9) Send a request with our new token + let req = serde_json::json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "retrieve_summary_info", + "params": { + "token": token, + "refresh_from_node": true, + "minimum_confirmations": 1 + } + }); + + let res = send_request_enc::( + 1, + 1, + "http://127.0.0.1:43420/v3/owner", + &req.to_string(), + &shared_key, + )?; + println!("RES 9: {:?}", res); + assert!(res.is_ok()); + + // 10) Send same request with no token (even though one is expected) + let req = serde_json::json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "retrieve_summary_info", + "params": { + "token": null, + "refresh_from_node": true, + "minimum_confirmations": 1 + } + }); + + let res = send_request_enc::( + 1, + 1, + "http://127.0.0.1:43420/v3/owner", + &req.to_string(), + &shared_key, + )?; + println!("RES 10: {:?}", res); + assert!(res.is_err()); + + // 11) Close the wallet + let req = include_str!("data/v3_reqs/close_wallet.req.json"); + let res = + send_request_enc::(1, 1, "http://127.0.0.1:43420/v3/owner", &req, &shared_key)?; + println!("RES 11: {:?}", res); + assert!(res.is_ok()); + + // 12) Wallet is closed + let req = serde_json::json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "retrieve_summary_info", + "params": { + "token": token, + "refresh_from_node": true, + "minimum_confirmations": 1 + } + }); + + let res = send_request_enc::( + 1, + 1, + "http://127.0.0.1:43420/v3/owner", + &req.to_string(), + &shared_key, + )?; + println!("RES 12: {:?}", res); + assert!(res.is_err()); + + // 13) Open the wallet again + let req = include_str!("data/v3_reqs/open_wallet.req.json"); + let res = + send_request_enc::(1, 1, "http://127.0.0.1:43420/v3/owner", &req, &shared_key)?; + println!("RES 13: {:?}", res); + assert!(res.is_ok()); + let token = res.unwrap(); + + // 14) Send a request with our new token + let req = serde_json::json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "retrieve_summary_info", + "params": { + "token": token, + "refresh_from_node": true, + "minimum_confirmations": 1 + } + }); + let res = send_request_enc::( + 1, + 1, + "http://127.0.0.1:43420/v3/owner", + &req.to_string(), + &shared_key, + )?; + println!("RES 14: {:?}", res); + assert!(res.is_ok()); + + //15) Ask wallet 2 for some grins + let req = serde_json::json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "issue_invoice_tx", + "params": { + "token": token, + "args": { + "amount": "6000000000", + "message": "geez a block of grins", + "dest_acct_name": null, + "target_slate_version": null + } + } + }); + let res = send_request_enc::( + 1, + 1, + "http://127.0.0.1:43420/v3/owner", + &req.to_string(), + &shared_key, + )?; + println!("RES 15: {:?}", res); + assert!(res.is_ok()); + let mut slate: Slate = res.unwrap().into(); + + // give this slate over to wallet 2 manually + grin_wallet_controller::controller::owner_single_use(wallet2.clone(), mask2, |api, m| { + let args = InitTxArgs { + src_acct_name: None, + amount: slate.amount, + minimum_confirmations: 1, + max_outputs: 500, + num_change_outputs: 1, + selection_strategy_is_use_all: false, + ..Default::default() + }; + let res = api.process_invoice_tx(m, &slate, args); + assert!(res.is_ok()); + slate = res.unwrap(); + api.tx_lock_outputs(m, &slate, 0)?; + + Ok(()) + })?; + + //16) Finalize the invoice tx (to foreign api) + // (Tests that foreign API on same port also has its stored mask updated) + let req = serde_json::json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "finalize_invoice_tx", + "params": { + "slate": VersionedSlate::into_version(slate, SlateVersion::V2), + } + }); + let res = + send_request::(1, "http://127.0.0.1:43420/v2/foreign", &req.to_string())?; + println!("RES 16: {:?}", res); + assert!(res.is_ok()); + + thread::sleep(Duration::from_millis(200)); + clean_output_dir(test_dir); + + Ok(()) +}