mirror of
https://github.com/mimblewimble/grin-wallet.git
synced 2025-01-20 19:11:09 +03:00
Add init_api_secure
function (#206)
* adding initial version of init_secure_api * rustfmt * fix ECDH algo * rustfmt * trying to figure out best way of doing encryption * refactor secure requests and responses into json-rpc responses, with base64 payload for encrypted messages * rustfmt * return proper errors from encrypted api, include tests covering encrypted API error cases * rustfmt * add test for normal error (unencrypted) * rustfmt * change ports for test, add foreign listener to V2 sanity tests, add ability to select owner api port via command line * rustfmt * turn it to 11 * explicit teardown after rpc tests * update tests with explicit teardowns * update tests to perform explicit teardown * fix warnings, ensure all tests teardown * log output to diagnose CI windows build failures * disable owner api doctests on windows * rustfmt
This commit is contained in:
parent
62d976f9ef
commit
a58cae651e
33 changed files with 1224 additions and 294 deletions
32
Cargo.lock
generated
32
Cargo.lock
generated
|
@ -406,6 +406,18 @@ dependencies = [
|
|||
"serde_json 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "easy-jsonrpc-mw"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"easy-jsonrpc-proc-macro-mw 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"jsonrpc-core 10.1.0 (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)",
|
||||
"serde_json 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "easy-jsonrpc-proc-macro"
|
||||
version = "0.5.0"
|
||||
|
@ -417,6 +429,17 @@ dependencies = [
|
|||
"syn 0.15.42 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "easy-jsonrpc-proc-macro-mw"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"syn 0.15.42 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "encode_unicode"
|
||||
version = "0.3.5"
|
||||
|
@ -789,8 +812,9 @@ dependencies = [
|
|||
name = "grin_wallet_api"
|
||||
version = "2.1.0-beta.1"
|
||||
dependencies = [
|
||||
"base64 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"chrono 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"easy-jsonrpc 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"easy-jsonrpc-mw 0.5.3 (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_wallet_config 2.1.0-beta.1",
|
||||
|
@ -798,6 +822,8 @@ dependencies = [
|
|||
"grin_wallet_libwallet 2.1.0-beta.1",
|
||||
"grin_wallet_util 2.1.0-beta.1",
|
||||
"log 0.4.8 (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)",
|
||||
"serde 1.0.98 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde_derive 1.0.98 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde_json 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
|
@ -823,7 +849,7 @@ name = "grin_wallet_controller"
|
|||
version = "2.1.0-beta.1"
|
||||
dependencies = [
|
||||
"chrono 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"easy-jsonrpc 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"easy-jsonrpc-mw 0.5.3 (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.28 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
|
@ -2839,7 +2865,9 @@ dependencies = [
|
|||
"checksum dirs 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "3fd78930633bd1c6e35c4b42b1df7b0cbc6bc191146e512bb3bedf243fcc3901"
|
||||
"checksum dtoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "ea57b42383d091c85abcc2706240b94ab2a8fa1fc81c10ff23c4de06e2a90b5e"
|
||||
"checksum easy-jsonrpc 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d4a851f8e0ed5790b60ded487feb0dc3c7e7da52c4a0adc57c009bfc5af8ca1a"
|
||||
"checksum easy-jsonrpc-mw 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c6f0a8e3a3a2c87620d0d0f1df8e619c2381affd6881558d19d66841b6335844"
|
||||
"checksum easy-jsonrpc-proc-macro 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d9fb33793846951f339a70580375734416898ff8ddbb74401865031e25ba6751"
|
||||
"checksum easy-jsonrpc-proc-macro-mw 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a6368dbd2c6685fb84fc6e6a4749917ddc98905793fd06341c7e11a2504f2724"
|
||||
"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"
|
||||
|
|
|
@ -15,10 +15,13 @@ failure_derive = "0.1"
|
|||
log = "0.4"
|
||||
uuid = { version = "0.7", features = ["serde", "v4"] }
|
||||
serde = "1"
|
||||
rand = "0.5"
|
||||
serde_derive = "1"
|
||||
serde_json = "1"
|
||||
easy-jsonrpc = "0.5.1"
|
||||
easy-jsonrpc-mw = "0.5.3"
|
||||
chrono = { version = "0.4.4", features = ["serde"] }
|
||||
ring = "0.13"
|
||||
base64 = "0.9"
|
||||
|
||||
grin_wallet_libwallet = { path = "../libwallet", version = "2.1.0-beta.1" }
|
||||
grin_wallet_config = { path = "../config", version = "2.1.0-beta.1" }
|
||||
|
|
|
@ -20,13 +20,13 @@ use crate::libwallet::{
|
|||
NodeVersionInfo, Slate, VersionInfo, VersionedSlate, WalletLCProvider,
|
||||
};
|
||||
use crate::{Foreign, ForeignCheckMiddlewareFn};
|
||||
use easy_jsonrpc;
|
||||
use easy_jsonrpc_mw;
|
||||
|
||||
/// Public definition used to generate Foreign jsonrpc api.
|
||||
/// * When running `grin-wallet listen` with defaults, the V2 api is available at
|
||||
/// `localhost:3415/v2/foreign`
|
||||
/// * The endpoint only supports POST operations, with the json-rpc request as the body
|
||||
#[easy_jsonrpc::rpc]
|
||||
#[easy_jsonrpc_mw::rpc]
|
||||
pub trait ForeignRpc {
|
||||
/**
|
||||
Networked version of [Foreign::check_version](struct.Foreign.html#method.check_version).
|
||||
|
@ -577,7 +577,7 @@ pub fn run_doctest_foreign(
|
|||
init_tx: bool,
|
||||
init_invoice_tx: bool,
|
||||
) -> Result<Option<serde_json::Value>, String> {
|
||||
use easy_jsonrpc::Handler;
|
||||
use easy_jsonrpc_mw::Handler;
|
||||
use grin_wallet_impls::test_framework::{self, LocalWalletClient, WalletProxy};
|
||||
use grin_wallet_impls::{DefaultLCProvider, DefaultWalletImpl};
|
||||
use grin_wallet_libwallet::{api_impl, WalletInst};
|
||||
|
@ -613,7 +613,7 @@ pub fn run_doctest_foreign(
|
|||
let mut wallet1 =
|
||||
Box::new(DefaultWalletImpl::<LocalWalletClient>::new(client1.clone()).unwrap())
|
||||
as Box<
|
||||
WalletInst<
|
||||
dyn WalletInst<
|
||||
'static,
|
||||
DefaultLCProvider<LocalWalletClient, ExtKeychain>,
|
||||
LocalWalletClient,
|
||||
|
@ -648,7 +648,7 @@ pub fn run_doctest_foreign(
|
|||
let mut wallet2 =
|
||||
Box::new(DefaultWalletImpl::<LocalWalletClient>::new(client2.clone()).unwrap())
|
||||
as Box<
|
||||
WalletInst<
|
||||
dyn WalletInst<
|
||||
'static,
|
||||
DefaultLCProvider<LocalWalletClient, ExtKeychain>,
|
||||
LocalWalletClient,
|
||||
|
@ -751,7 +751,9 @@ pub fn run_doctest_foreign(
|
|||
};
|
||||
api_foreign.doctest_mode = true;
|
||||
let foreign_api = &api_foreign as &dyn ForeignRpc;
|
||||
Ok(foreign_api.handle_request(request).as_option())
|
||||
let res = foreign_api.handle_request(request).as_option();
|
||||
let _ = fs::remove_dir_all(test_dir);
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
|
|
|
@ -43,6 +43,8 @@ mod owner;
|
|||
mod owner_rpc;
|
||||
mod owner_rpc_s;
|
||||
|
||||
mod types;
|
||||
|
||||
pub use crate::foreign::{Foreign, ForeignCheckMiddleware, ForeignCheckMiddlewareFn};
|
||||
pub use crate::foreign_rpc::ForeignRpc;
|
||||
pub use crate::owner::Owner;
|
||||
|
@ -53,13 +55,4 @@ pub use crate::foreign_rpc::foreign_rpc as foreign_rpc_client;
|
|||
pub use crate::foreign_rpc::run_doctest_foreign;
|
||||
pub use crate::owner_rpc::run_doctest_owner;
|
||||
|
||||
use grin_wallet_util::grin_core::libtx::secp_ser;
|
||||
use util::secp::key::SecretKey;
|
||||
|
||||
/// Wrapper for API Tokens
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(transparent)]
|
||||
pub struct Token {
|
||||
#[serde(with = "secp_ser::option_seckey_serde")]
|
||||
keychain_mask: Option<SecretKey>,
|
||||
}
|
||||
pub use types::{ECDHPubkey, EncryptedRequest, EncryptedResponse, EncryptionErrorResponse, Token};
|
||||
|
|
|
@ -54,6 +54,8 @@ where
|
|||
pub wallet_inst: Arc<Mutex<Box<dyn WalletInst<'a, L, C, K>>>>,
|
||||
/// Flag to normalize some output during testing. Can mostly be ignored.
|
||||
pub doctest_mode: bool,
|
||||
/// Share ECDH key
|
||||
pub shared_key: Arc<Mutex<Option<SecretKey>>>,
|
||||
}
|
||||
|
||||
impl<'a, L, C, K> Owner<'a, L, C, K>
|
||||
|
@ -141,6 +143,7 @@ where
|
|||
Owner {
|
||||
wallet_inst,
|
||||
doctest_mode: false,
|
||||
shared_key: Arc::new(Mutex::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -24,15 +24,15 @@ use crate::libwallet::{
|
|||
};
|
||||
use crate::util::Mutex;
|
||||
use crate::{Owner, OwnerRpcS};
|
||||
use easy_jsonrpc;
|
||||
use easy_jsonrpc_mw;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Public definition used to generate Owner jsonrpc api.
|
||||
/// * When running `grin-wallet owner_api` with defaults, the V2 api is available at
|
||||
/// `localhost:3420/v2/owner`
|
||||
/// * The endpoint only supports POST operations, with the json-rpc request as the body
|
||||
#[easy_jsonrpc::rpc]
|
||||
pub trait OwnerRpc {
|
||||
#[easy_jsonrpc_mw::rpc]
|
||||
pub trait OwnerRpc: Sync + Send {
|
||||
/**
|
||||
Networked version of [Owner::accounts](struct.Owner.html#method.accounts).
|
||||
|
||||
|
@ -1148,7 +1148,7 @@ pub trait OwnerRpc {
|
|||
}
|
||||
}
|
||||
# "#
|
||||
# ,false, 5 ,true, false, false);
|
||||
# ,false, 0 ,false, false, false);
|
||||
```
|
||||
*/
|
||||
fn verify_slate_messages(&self, slate: VersionedSlate) -> Result<(), ErrorKind>;
|
||||
|
@ -1370,7 +1370,7 @@ pub fn run_doctest_owner(
|
|||
lock_tx: bool,
|
||||
finalize_tx: bool,
|
||||
) -> Result<Option<serde_json::Value>, String> {
|
||||
use easy_jsonrpc::Handler;
|
||||
use easy_jsonrpc_mw::Handler;
|
||||
use grin_wallet_impls::test_framework::{self, LocalWalletClient, WalletProxy};
|
||||
use grin_wallet_impls::{DefaultLCProvider, DefaultWalletImpl};
|
||||
use grin_wallet_libwallet::{api_impl, WalletInst};
|
||||
|
@ -1404,7 +1404,7 @@ pub fn run_doctest_owner(
|
|||
let mut wallet1 =
|
||||
Box::new(DefaultWalletImpl::<LocalWalletClient>::new(client1.clone()).unwrap())
|
||||
as Box<
|
||||
WalletInst<
|
||||
dyn WalletInst<
|
||||
'static,
|
||||
DefaultLCProvider<LocalWalletClient, ExtKeychain>,
|
||||
LocalWalletClient,
|
||||
|
@ -1439,7 +1439,7 @@ pub fn run_doctest_owner(
|
|||
let mut wallet2 =
|
||||
Box::new(DefaultWalletImpl::<LocalWalletClient>::new(client2.clone()).unwrap())
|
||||
as Box<
|
||||
WalletInst<
|
||||
dyn WalletInst<
|
||||
'static,
|
||||
DefaultLCProvider<LocalWalletClient, ExtKeychain>,
|
||||
LocalWalletClient,
|
||||
|
@ -1547,13 +1547,15 @@ pub fn run_doctest_owner(
|
|||
|
||||
let mut api_owner = Owner::new(wallet1);
|
||||
api_owner.doctest_mode = true;
|
||||
if use_token {
|
||||
let res = if use_token {
|
||||
let owner_api = &api_owner as &dyn OwnerRpcS;
|
||||
Ok(owner_api.handle_request(request).as_option())
|
||||
owner_api.handle_request(request).as_option()
|
||||
} else {
|
||||
let owner_api = &api_owner as &dyn OwnerRpc;
|
||||
Ok(owner_api.handle_request(request).as_option())
|
||||
}
|
||||
owner_api.handle_request(request).as_option()
|
||||
};
|
||||
let _ = fs::remove_dir_all(test_dir);
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
|
@ -1563,39 +1565,46 @@ macro_rules! doctest_helper_json_rpc_owner_assert_response {
|
|||
// create temporary wallet, run jsonrpc request on owner api of wallet, delete wallet, return
|
||||
// json response.
|
||||
// In order to prevent leaking tempdirs, This function should not panic.
|
||||
use grin_wallet_api::run_doctest_owner;
|
||||
use serde_json;
|
||||
use serde_json::Value;
|
||||
use tempfile::tempdir;
|
||||
|
||||
let dir = tempdir().map_err(|e| format!("{:#?}", e)).unwrap();
|
||||
let dir = dir
|
||||
.path()
|
||||
.to_str()
|
||||
.ok_or("Failed to convert tmpdir path to string.".to_owned())
|
||||
// These cause LMDB to run out of disk space on CircleCI
|
||||
// disable for now on windows
|
||||
// TODO: Fix properly
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
use grin_wallet_api::run_doctest_owner;
|
||||
use serde_json;
|
||||
use serde_json::Value;
|
||||
use tempfile::tempdir;
|
||||
|
||||
let dir = tempdir().map_err(|e| format!("{:#?}", e)).unwrap();
|
||||
let dir = dir
|
||||
.path()
|
||||
.to_str()
|
||||
.ok_or("Failed to convert tmpdir path to string.".to_owned())
|
||||
.unwrap();
|
||||
|
||||
let request_val: Value = serde_json::from_str($request).unwrap();
|
||||
let expected_response: Value = serde_json::from_str($expected_response).unwrap();
|
||||
|
||||
let response = run_doctest_owner(
|
||||
request_val,
|
||||
dir,
|
||||
$use_token,
|
||||
$blocks_to_mine,
|
||||
$perform_tx,
|
||||
$lock_tx,
|
||||
$finalize_tx,
|
||||
)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
let request_val: Value = serde_json::from_str($request).unwrap();
|
||||
let expected_response: Value = serde_json::from_str($expected_response).unwrap();
|
||||
|
||||
let response = run_doctest_owner(
|
||||
request_val,
|
||||
dir,
|
||||
$use_token,
|
||||
$blocks_to_mine,
|
||||
$perform_tx,
|
||||
$lock_tx,
|
||||
$finalize_tx,
|
||||
)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
if response != expected_response {
|
||||
panic!(
|
||||
"(left != right) \nleft: {}\nright: {}",
|
||||
serde_json::to_string_pretty(&response).unwrap(),
|
||||
serde_json::to_string_pretty(&expected_response).unwrap()
|
||||
if response != expected_response {
|
||||
panic!(
|
||||
"(left != right) \nleft: {}\nright: {}",
|
||||
serde_json::to_string_pretty(&response).unwrap(),
|
||||
serde_json::to_string_pretty(&expected_response).unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -22,12 +22,15 @@ use crate::libwallet::{
|
|||
OutputCommitMapping, Slate, SlateVersion, TxLogEntry, VersionedSlate, WalletInfo,
|
||||
WalletLCProvider,
|
||||
};
|
||||
use crate::{Owner, Token};
|
||||
use easy_jsonrpc;
|
||||
use crate::util::secp::key::{PublicKey, SecretKey};
|
||||
use crate::util::static_secp_instance;
|
||||
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
|
||||
#[easy_jsonrpc::rpc]
|
||||
#[easy_jsonrpc_mw::rpc]
|
||||
pub trait OwnerRpcS {
|
||||
/**
|
||||
Networked version of [Owner::accounts](struct.Owner.html#method.accounts).
|
||||
|
@ -1199,7 +1202,7 @@ pub trait OwnerRpcS {
|
|||
}
|
||||
}
|
||||
# "#
|
||||
# ,true, 5 ,true, false, false);
|
||||
# ,true, 0 ,false, false, false);
|
||||
```
|
||||
*/
|
||||
fn verify_slate_messages(&self, token: Token, slate: VersionedSlate) -> Result<(), ErrorKind>;
|
||||
|
@ -1300,6 +1303,13 @@ pub trait OwnerRpcS {
|
|||
```
|
||||
*/
|
||||
fn node_height(&self, token: Token) -> Result<NodeHeightResult, ErrorKind>;
|
||||
|
||||
/**
|
||||
Initializes the secure RPC-JSON API
|
||||
(Documentation TBD)
|
||||
*/
|
||||
|
||||
fn init_secure_api(&self, ecdh_pubkey: ECDHPubkey) -> Result<ECDHPubkey, ErrorKind>;
|
||||
}
|
||||
|
||||
impl<'a, L, C, K> OwnerRpcS for Owner<'a, L, C, K>
|
||||
|
@ -1471,4 +1481,30 @@ where
|
|||
fn node_height(&self, token: Token) -> Result<NodeHeightResult, ErrorKind> {
|
||||
Owner::node_height(self, (&token.keychain_mask).as_ref()).map_err(|e| e.kind())
|
||||
}
|
||||
|
||||
fn init_secure_api(&self, ecdh_pubkey: ECDHPubkey) -> Result<ECDHPubkey, ErrorKind> {
|
||||
let secp_inst = static_secp_instance();
|
||||
let secp = secp_inst.lock();
|
||||
let sec_key = SecretKey::new(&secp, &mut thread_rng());
|
||||
|
||||
let mut shared_pubkey = ecdh_pubkey.ecdh_pubkey.clone();
|
||||
shared_pubkey
|
||||
.mul_assign(&secp, &sec_key)
|
||||
.map_err(|e| ErrorKind::Secp(e))?;
|
||||
|
||||
let x_coord = shared_pubkey.serialize_vec(&secp, true);
|
||||
let shared_key =
|
||||
SecretKey::from_slice(&secp, &x_coord[1..]).map_err(|e| ErrorKind::Secp(e))?;
|
||||
{
|
||||
let mut s = self.shared_key.lock();
|
||||
*s = Some(shared_key);
|
||||
}
|
||||
|
||||
let pub_key =
|
||||
PublicKey::from_secret_key(&secp, &sec_key).map_err(|e| ErrorKind::Secp(e))?;
|
||||
|
||||
Ok(ECDHPubkey {
|
||||
ecdh_pubkey: pub_key,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
308
api/src/types.rs
Normal file
308
api/src/types.rs
Normal file
|
@ -0,0 +1,308 @@
|
|||
// 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.
|
||||
|
||||
use crate::core::libtx::secp_ser;
|
||||
use crate::libwallet::{Error, ErrorKind};
|
||||
use crate::util::secp::key::{PublicKey, SecretKey};
|
||||
use crate::util::{from_hex, to_hex};
|
||||
use failure::ResultExt;
|
||||
|
||||
use base64;
|
||||
use rand::{thread_rng, Rng};
|
||||
use ring::aead;
|
||||
use serde_json::{self, Value};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Wrapper for API Tokens
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(transparent)]
|
||||
pub struct Token {
|
||||
#[serde(with = "secp_ser::option_seckey_serde")]
|
||||
/// Token to XOR mask against the stored wallet seed
|
||||
pub keychain_mask: Option<SecretKey>,
|
||||
}
|
||||
|
||||
/// Wrapper for ECDH Public keys
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(transparent)]
|
||||
pub struct ECDHPubkey {
|
||||
/// public key, flattened
|
||||
#[serde(with = "secp_ser::pubkey_serde")]
|
||||
pub ecdh_pubkey: PublicKey,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct EncryptedBody {
|
||||
/// nonce used for encryption
|
||||
pub nonce: String,
|
||||
/// Encrypted base64 body request
|
||||
pub body_enc: String,
|
||||
}
|
||||
|
||||
impl EncryptedBody {
|
||||
/// Encrypts and encodes json as base 64
|
||||
pub fn from_json(json_in: &Value, enc_key: &SecretKey) -> Result<Self, Error> {
|
||||
let mut to_encrypt = serde_json::to_string(&json_in)
|
||||
.context(ErrorKind::APIEncryption(
|
||||
"EncryptedBody Enc: Unable to encode JSON".to_owned(),
|
||||
))?
|
||||
.as_bytes()
|
||||
.to_vec();
|
||||
let sealing_key = aead::SealingKey::new(&aead::AES_256_GCM, &enc_key.0).context(
|
||||
ErrorKind::APIEncryption("EncryptedBody Enc: Unable to create key".to_owned()),
|
||||
)?;
|
||||
let nonce: [u8; 12] = thread_rng().gen();
|
||||
let suffix_len = aead::AES_256_GCM.tag_len();
|
||||
for _ in 0..suffix_len {
|
||||
to_encrypt.push(0);
|
||||
}
|
||||
aead::seal_in_place(&sealing_key, &nonce, &[], &mut to_encrypt, suffix_len).context(
|
||||
ErrorKind::APIEncryption("EncryptedBody: Encryption Failed".to_owned()),
|
||||
)?;
|
||||
|
||||
Ok(EncryptedBody {
|
||||
nonce: to_hex(nonce.to_vec()),
|
||||
body_enc: base64::encode(&to_encrypt),
|
||||
})
|
||||
}
|
||||
|
||||
/// return serialize JSON self
|
||||
pub fn as_json_value(&self) -> Result<Value, Error> {
|
||||
let res = serde_json::to_value(self).context(ErrorKind::APIEncryption(
|
||||
"EncryptedBody: JSON serialization failed".to_owned(),
|
||||
))?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// return serialized JSON self as string
|
||||
pub fn as_json_str(&self) -> Result<String, Error> {
|
||||
let res = self.as_json_value()?;
|
||||
let res = serde_json::to_string(&res).context(ErrorKind::APIEncryption(
|
||||
"EncryptedBody: JSON String serialization failed".to_owned(),
|
||||
))?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Return original request
|
||||
pub fn decrypt(&self, dec_key: &SecretKey) -> Result<Value, Error> {
|
||||
let mut to_decrypt = base64::decode(&self.body_enc).context(ErrorKind::APIEncryption(
|
||||
"EncryptedBody Dec: Encrypted request contains invalid Base64".to_string(),
|
||||
))?;
|
||||
let opening_key = aead::OpeningKey::new(&aead::AES_256_GCM, &dec_key.0).context(
|
||||
ErrorKind::APIEncryption("EncryptedBody Dec: Unable to create key".to_owned()),
|
||||
)?;
|
||||
let nonce = from_hex(self.nonce.clone()).context(ErrorKind::APIEncryption(
|
||||
"EncryptedBody Dec: Invalid Nonce".to_string(),
|
||||
))?;
|
||||
aead::open_in_place(&opening_key, &nonce, &[], 0, &mut to_decrypt).context(
|
||||
ErrorKind::APIEncryption(
|
||||
"EncryptedBody Dec: Decryption Failed (is key correct?)".to_string(),
|
||||
),
|
||||
)?;
|
||||
for _ in 0..aead::AES_256_GCM.tag_len() {
|
||||
to_decrypt.pop();
|
||||
}
|
||||
let decrypted = String::from_utf8(to_decrypt).context(ErrorKind::APIEncryption(
|
||||
"EncryptedBody Dec: Invalid UTF-8".to_string(),
|
||||
))?;
|
||||
Ok(
|
||||
serde_json::from_str(&decrypted).context(ErrorKind::APIEncryption(
|
||||
"EncryptedBody Dec: Invalid JSON".to_string(),
|
||||
))?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper for secure JSON requests
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct EncryptedRequest {
|
||||
/// JSON RPC response
|
||||
pub jsonrpc: String,
|
||||
/// method
|
||||
pub method: String,
|
||||
/// id
|
||||
pub id: u32,
|
||||
/// Body params, which includes nonce and encrypted request
|
||||
pub params: EncryptedBody,
|
||||
}
|
||||
|
||||
impl EncryptedRequest {
|
||||
/// from json
|
||||
pub fn from_json(id: u32, json_in: &Value, enc_key: &SecretKey) -> Result<Self, Error> {
|
||||
Ok(EncryptedRequest {
|
||||
jsonrpc: "2.0".to_owned(),
|
||||
method: "encrypted_request_v3".to_owned(),
|
||||
id: id,
|
||||
params: EncryptedBody::from_json(json_in, enc_key)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// return serialize JSON self
|
||||
pub fn as_json_value(&self) -> Result<Value, Error> {
|
||||
let res = serde_json::to_value(self).context(ErrorKind::APIEncryption(
|
||||
"EncryptedRequest: JSON serialization failed".to_owned(),
|
||||
))?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// return serialized JSON self as string
|
||||
pub fn as_json_str(&self) -> Result<String, Error> {
|
||||
let res = self.as_json_value()?;
|
||||
let res = serde_json::to_string(&res).context(ErrorKind::APIEncryption(
|
||||
"EncryptedRequest: JSON String serialization failed".to_owned(),
|
||||
))?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Return decrypted body
|
||||
pub fn decrypt(&self, dec_key: &SecretKey) -> Result<Value, Error> {
|
||||
self.params.decrypt(dec_key)
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper for secure JSON requests
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct EncryptedResponse {
|
||||
/// JSON RPC response
|
||||
pub jsonrpc: String,
|
||||
/// id
|
||||
pub id: u32,
|
||||
/// result
|
||||
pub result: HashMap<String, EncryptedBody>,
|
||||
}
|
||||
|
||||
impl EncryptedResponse {
|
||||
/// from json
|
||||
pub fn from_json(id: u32, json_in: &Value, enc_key: &SecretKey) -> Result<Self, Error> {
|
||||
let mut result_set = HashMap::new();
|
||||
result_set.insert(
|
||||
"Ok".to_string(),
|
||||
EncryptedBody::from_json(json_in, enc_key)?,
|
||||
);
|
||||
Ok(EncryptedResponse {
|
||||
jsonrpc: "2.0".to_owned(),
|
||||
id: id,
|
||||
result: result_set,
|
||||
})
|
||||
}
|
||||
|
||||
/// return serialize JSON self
|
||||
pub fn as_json_value(&self) -> Result<Value, Error> {
|
||||
let res = serde_json::to_value(self).context(ErrorKind::APIEncryption(
|
||||
"EncryptedResponse: JSON serialization failed".to_owned(),
|
||||
))?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// return serialized JSON self as string
|
||||
pub fn as_json_str(&self) -> Result<String, Error> {
|
||||
let res = self.as_json_value()?;
|
||||
let res = serde_json::to_string(&res).context(ErrorKind::APIEncryption(
|
||||
"EncryptedResponse: JSON String serialization failed".to_owned(),
|
||||
))?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Return decrypted body
|
||||
pub fn decrypt(&self, dec_key: &SecretKey) -> Result<Value, Error> {
|
||||
self.result.get("Ok").unwrap().decrypt(dec_key)
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper for encryption error responses
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct EncryptionError {
|
||||
/// code
|
||||
pub code: i32,
|
||||
/// message
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
/// Wrapper for encryption error responses
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct EncryptionErrorResponse {
|
||||
/// JSON RPC response
|
||||
pub jsonrpc: String,
|
||||
/// id
|
||||
pub id: u32,
|
||||
/// error
|
||||
pub error: EncryptionError,
|
||||
}
|
||||
|
||||
impl EncryptionErrorResponse {
|
||||
/// Create new response
|
||||
pub fn new(id: u32, code: i32, message: &str) -> Self {
|
||||
EncryptionErrorResponse {
|
||||
jsonrpc: "2.0".to_owned(),
|
||||
id: id,
|
||||
error: EncryptionError {
|
||||
code: code,
|
||||
message: message.to_owned(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// return serialized JSON self
|
||||
pub fn as_json_value(&self) -> Value {
|
||||
let res = serde_json::to_value(self).context(ErrorKind::APIEncryption(
|
||||
"EncryptedResponse: JSON serialization failed".to_owned(),
|
||||
));
|
||||
match res {
|
||||
Ok(r) => r,
|
||||
// proverbial "should never happen"
|
||||
Err(r) => serde_json::json!({
|
||||
"json_rpc" : "2.0",
|
||||
"id" : "1",
|
||||
"error" : {
|
||||
"message": format!("internal error serialising json error response {}", r),
|
||||
"code": -32000
|
||||
}
|
||||
}
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encrypted_request() -> Result<(), Error> {
|
||||
use crate::util::{from_hex, static_secp_instance};
|
||||
|
||||
let sec_key_str = "e00dcc4a009e3427c6b1e1a550c538179d46f3827a13ed74c759c860761caf1e";
|
||||
let shared_key = {
|
||||
let secp_inst = static_secp_instance();
|
||||
let secp = secp_inst.lock();
|
||||
|
||||
let sec_key_bytes = from_hex(sec_key_str.to_owned()).unwrap();
|
||||
SecretKey::from_slice(&secp, &sec_key_bytes)?
|
||||
};
|
||||
let req = serde_json::json!({
|
||||
"jsonrpc": "2.0",
|
||||
"method": "accounts",
|
||||
"id": 1,
|
||||
"params": {
|
||||
"token": "d202964900000000d302964900000000d402964900000000d502964900000000"
|
||||
}
|
||||
});
|
||||
let enc_req = EncryptedRequest::from_json(1, &req, &shared_key)?;
|
||||
println!("{:?}", enc_req);
|
||||
let dec_req = enc_req.decrypt(&shared_key)?;
|
||||
println!("{:?}", dec_req);
|
||||
assert_eq!(req, dec_req);
|
||||
let enc_res = EncryptedResponse::from_json(1, &req, &shared_key)?;
|
||||
println!("{:?}", enc_res);
|
||||
println!("{:?}", enc_res.as_json_str()?);
|
||||
let dec_res = enc_res.decrypt(&shared_key)?;
|
||||
println!("{:?}", dec_res);
|
||||
assert_eq!(req, dec_res);
|
||||
Ok(())
|
||||
}
|
|
@ -29,7 +29,7 @@ tokio-retry = "0.1"
|
|||
uuid = { version = "0.7", features = ["serde", "v4"] }
|
||||
url = "1.7.0"
|
||||
chrono = { version = "0.4.4", features = ["serde"] }
|
||||
easy-jsonrpc = "0.5.1"
|
||||
easy-jsonrpc-mw = "0.5.3"
|
||||
lazy_static = "1"
|
||||
|
||||
grin_wallet_util = { path = "../util", version = "2.1.0-beta.1" }
|
||||
|
|
|
@ -32,15 +32,22 @@ use serde_json;
|
|||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::apiwallet::{Foreign, ForeignCheckMiddlewareFn, ForeignRpc, Owner, OwnerRpc, OwnerRpcS};
|
||||
use easy_jsonrpc;
|
||||
use easy_jsonrpc::{Handler, MaybeReply};
|
||||
use crate::apiwallet::{
|
||||
EncryptedRequest, EncryptedResponse, EncryptionErrorResponse, Foreign,
|
||||
ForeignCheckMiddlewareFn, ForeignRpc, Owner, OwnerRpc, OwnerRpcS,
|
||||
};
|
||||
use easy_jsonrpc_mw;
|
||||
use easy_jsonrpc_mw::{Handler, MaybeReply};
|
||||
|
||||
lazy_static! {
|
||||
pub static ref GRIN_OWNER_BASIC_REALM: HeaderValue =
|
||||
HeaderValue::from_str("Basic realm=GrinOwnerAPI").unwrap();
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref OWNER_API_SHARED_KEY: Arc<Mutex<Option<SecretKey>>> = Arc::new(Mutex::new(None));
|
||||
}
|
||||
|
||||
fn check_middleware(
|
||||
name: ForeignCheckMiddlewareFn,
|
||||
node_version_info: Option<NodeVersionInfo>,
|
||||
|
@ -138,7 +145,6 @@ where
|
|||
}
|
||||
|
||||
let api_handler_v2 = OwnerAPIHandlerV2::new(wallet.clone());
|
||||
|
||||
let api_handler_v3 = OwnerAPIHandlerV3::new(wallet.clone());
|
||||
|
||||
router
|
||||
|
@ -295,6 +301,106 @@ where
|
|||
pub wallet: Arc<Mutex<Box<dyn WalletInst<'static, L, C, K> + 'static>>>,
|
||||
}
|
||||
|
||||
pub struct OwnerV3Helpers;
|
||||
|
||||
impl OwnerV3Helpers {
|
||||
/// Checks whether a request is to init the secure API
|
||||
pub fn is_init_secure_api(val: &serde_json::Value) -> bool {
|
||||
if let Some(m) = val["method"].as_str() {
|
||||
match m {
|
||||
"init_secure_api" => 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() {
|
||||
match m {
|
||||
"encrypted_request_v3" => true,
|
||||
_ => false,
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// whether encryption is enabled
|
||||
pub fn encryption_enabled() -> bool {
|
||||
let share_key_ref = OWNER_API_SHARED_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() {
|
||||
true => Ok(()),
|
||||
false => Err(EncryptionErrorResponse::new(
|
||||
1,
|
||||
-32001,
|
||||
"Encryption must be enabled. Please call 'init_secure_api` first",
|
||||
)
|
||||
.as_json_value()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the statically held owner API shared key
|
||||
pub fn update_owner_api_shared_key(val: &serde_json::Value, new_key: Option<SecretKey>) {
|
||||
if let Some(_) = val["result"]["Ok"].as_str() {
|
||||
let mut share_key_ref = OWNER_API_SHARED_KEY.lock();
|
||||
*share_key_ref = new_key;
|
||||
}
|
||||
}
|
||||
|
||||
/// Decrypt an encrypted request
|
||||
pub fn decrypt_request(
|
||||
req: &serde_json::Value,
|
||||
) -> Result<(u32, serde_json::Value), serde_json::Value> {
|
||||
let share_key_ref = OWNER_API_SHARED_KEY.lock();
|
||||
let shared_key = share_key_ref.as_ref().unwrap();
|
||||
let enc_req: EncryptedRequest = serde_json::from_value(req.clone()).map_err(|e| {
|
||||
EncryptionErrorResponse::new(
|
||||
1,
|
||||
-32002,
|
||||
&format!("Encrypted request format error: {}", e),
|
||||
)
|
||||
.as_json_value()
|
||||
})?;
|
||||
let id = enc_req.id;
|
||||
let res = enc_req.decrypt(&shared_key).map_err(|e| {
|
||||
EncryptionErrorResponse::new(1, -32002, &format!("Decryption error: {}", e.kind()))
|
||||
.as_json_value()
|
||||
})?;
|
||||
Ok((id, res))
|
||||
}
|
||||
|
||||
/// Encrypt a response
|
||||
pub fn encrypt_response(
|
||||
id: u32,
|
||||
res: &serde_json::Value,
|
||||
) -> Result<serde_json::Value, serde_json::Value> {
|
||||
let share_key_ref = OWNER_API_SHARED_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()))
|
||||
.as_json_value()
|
||||
})?;
|
||||
let res = enc_res.as_json_value().map_err(|e| {
|
||||
EncryptionErrorResponse::new(
|
||||
1,
|
||||
-32002,
|
||||
&format!("Encrypted response format error: {}", e),
|
||||
)
|
||||
.as_json_value()
|
||||
})?;
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl<L, C, K> OwnerAPIHandlerV3<L, C, K>
|
||||
where
|
||||
L: WalletLCProvider<'static, C, K>,
|
||||
|
@ -314,9 +420,47 @@ where
|
|||
api: Owner<'static, L, C, K>,
|
||||
) -> Box<dyn Future<Item = serde_json::Value, Error = Error> + Send> {
|
||||
Box::new(parse_body(req).and_then(move |val: serde_json::Value| {
|
||||
let mut val = val;
|
||||
let owner_api_s = &api as &dyn OwnerRpcS;
|
||||
let mut is_init_secure_api = OwnerV3Helpers::is_init_secure_api(&val);
|
||||
let mut was_encrypted = false;
|
||||
let mut encrypted_req_id = 0;
|
||||
if !is_init_secure_api {
|
||||
if let Err(v) = OwnerV3Helpers::check_encryption_started() {
|
||||
return ok(v);
|
||||
}
|
||||
let res = OwnerV3Helpers::decrypt_request(&val);
|
||||
match res {
|
||||
Err(e) => return ok(e),
|
||||
Ok(v) => {
|
||||
encrypted_req_id = v.0;
|
||||
val = v.1;
|
||||
}
|
||||
}
|
||||
was_encrypted = true;
|
||||
}
|
||||
// check again, in case it was an encrypted call to init_secure_api
|
||||
is_init_secure_api = OwnerV3Helpers::is_init_secure_api(&val);
|
||||
match owner_api_s.handle_request(val) {
|
||||
MaybeReply::Reply(r) => ok(r),
|
||||
MaybeReply::Reply(mut r) => {
|
||||
let unencrypted_intercept = r.clone();
|
||||
if was_encrypted {
|
||||
let res = OwnerV3Helpers::encrypt_response(encrypted_req_id, &r);
|
||||
r = match res {
|
||||
Ok(v) => v,
|
||||
Err(v) => return ok(v),
|
||||
}
|
||||
}
|
||||
// intercept init_secure_api response (after encryption,
|
||||
// in case it was an encrypted call to 'init_api_secure')
|
||||
if is_init_secure_api {
|
||||
OwnerV3Helpers::update_owner_api_shared_key(
|
||||
&unencrypted_intercept,
|
||||
api.shared_key.lock().clone(),
|
||||
);
|
||||
}
|
||||
ok(r)
|
||||
}
|
||||
MaybeReply::DontReply => {
|
||||
// Since it's http, we need to return something. We return [] because jsonrpc
|
||||
// clients will parse it as an empty batch response.
|
||||
|
|
|
@ -30,12 +30,10 @@ use std::time::Duration;
|
|||
|
||||
#[macro_use]
|
||||
mod common;
|
||||
use common::{create_wallet_proxy, setup};
|
||||
use common::{clean_output_dir, create_wallet_proxy, setup};
|
||||
|
||||
/// Various tests on accounts within the same wallet
|
||||
fn accounts_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> {
|
||||
setup(test_dir);
|
||||
|
||||
// Create a new proxy to simulate server and wallet responses
|
||||
let mut wallet_proxy = create_wallet_proxy(test_dir);
|
||||
let chain = wallet_proxy.chain.clone();
|
||||
|
@ -265,7 +263,9 @@ fn accounts_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> {
|
|||
#[test]
|
||||
fn accounts() {
|
||||
let test_dir = "test_output/accounts";
|
||||
setup(test_dir);
|
||||
if let Err(e) = accounts_test_impl(test_dir) {
|
||||
panic!("Libwallet Error: {} - {}", e, e.backtrace().unwrap());
|
||||
}
|
||||
clean_output_dir(test_dir);
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@ use util::ZeroingString;
|
|||
|
||||
#[macro_use]
|
||||
mod common;
|
||||
use common::{create_wallet_proxy, setup};
|
||||
use common::{clean_output_dir, create_wallet_proxy, setup};
|
||||
|
||||
macro_rules! send_to_dest {
|
||||
($a:expr, $m: expr, $b:expr, $c:expr, $d:expr) => {
|
||||
|
@ -48,8 +48,6 @@ macro_rules! wallet_info {
|
|||
|
||||
/// Various tests on checking functionality
|
||||
fn check_repair_impl(test_dir: &'static str) -> Result<(), libwallet::Error> {
|
||||
setup(test_dir);
|
||||
|
||||
// Create a new proxy to simulate server and wallet responses
|
||||
let mut wallet_proxy = create_wallet_proxy(test_dir);
|
||||
let chain = wallet_proxy.chain.clone();
|
||||
|
@ -753,12 +751,15 @@ fn check_repair() {
|
|||
if let Err(e) = check_repair_impl(test_dir) {
|
||||
panic!("Libwallet Error: {} - {}", e, e.backtrace().unwrap());
|
||||
}
|
||||
clean_output_dir(test_dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_wallets_one_seed() {
|
||||
let test_dir = "test_output/two_wallets_one_seed";
|
||||
setup(test_dir);
|
||||
if let Err(e) = two_wallets_one_seed_impl(test_dir) {
|
||||
panic!("Libwallet Error: {} - {}", e, e.backtrace().unwrap());
|
||||
}
|
||||
clean_output_dir(test_dir);
|
||||
}
|
||||
|
|
|
@ -101,7 +101,7 @@ pub fn create_local_wallet(
|
|||
Arc<
|
||||
Mutex<
|
||||
Box<
|
||||
WalletInst<
|
||||
dyn WalletInst<
|
||||
'static,
|
||||
DefaultLCProvider<'static, LocalWalletClient, ExtKeychain>,
|
||||
LocalWalletClient,
|
||||
|
@ -114,7 +114,7 @@ pub fn create_local_wallet(
|
|||
) {
|
||||
let mut wallet = Box::new(DefaultWalletImpl::<LocalWalletClient>::new(client).unwrap())
|
||||
as Box<
|
||||
WalletInst<
|
||||
dyn WalletInst<
|
||||
DefaultLCProvider<'static, LocalWalletClient, ExtKeychain>,
|
||||
LocalWalletClient,
|
||||
ExtKeychain,
|
||||
|
@ -130,6 +130,7 @@ pub fn create_local_wallet(
|
|||
(Arc::new(Mutex::new(wallet)), mask)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn open_local_wallet(
|
||||
test_dir: &str,
|
||||
name: &str,
|
||||
|
@ -139,7 +140,7 @@ pub fn open_local_wallet(
|
|||
Arc<
|
||||
Mutex<
|
||||
Box<
|
||||
WalletInst<
|
||||
dyn WalletInst<
|
||||
'static,
|
||||
DefaultLCProvider<'static, LocalWalletClient, ExtKeychain>,
|
||||
LocalWalletClient,
|
||||
|
@ -152,7 +153,7 @@ pub fn open_local_wallet(
|
|||
) {
|
||||
let mut wallet = Box::new(DefaultWalletImpl::<LocalWalletClient>::new(client).unwrap())
|
||||
as Box<
|
||||
WalletInst<
|
||||
dyn WalletInst<
|
||||
DefaultLCProvider<'static, LocalWalletClient, ExtKeychain>,
|
||||
LocalWalletClient,
|
||||
ExtKeychain,
|
||||
|
|
|
@ -31,12 +31,10 @@ use serde_json;
|
|||
|
||||
#[macro_use]
|
||||
mod common;
|
||||
use common::{create_wallet_proxy, setup};
|
||||
use common::{clean_output_dir, create_wallet_proxy, setup};
|
||||
|
||||
/// self send impl
|
||||
fn file_exchange_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> {
|
||||
setup(test_dir);
|
||||
|
||||
// Create a new proxy to simulate server and wallet responses
|
||||
let mut wallet_proxy = create_wallet_proxy(test_dir);
|
||||
let chain = wallet_proxy.chain.clone();
|
||||
|
@ -224,7 +222,9 @@ fn file_exchange_test_impl(test_dir: &'static str) -> Result<(), libwallet::Erro
|
|||
#[test]
|
||||
fn wallet_file_exchange() {
|
||||
let test_dir = "test_output/file_exchange";
|
||||
setup(test_dir);
|
||||
if let Err(e) = file_exchange_test_impl(test_dir) {
|
||||
panic!("Libwallet Error: {} - {}", e, e.backtrace().unwrap());
|
||||
}
|
||||
clean_output_dir(test_dir);
|
||||
}
|
||||
|
|
|
@ -31,193 +31,186 @@ use common::{clean_output_dir, create_wallet_proxy, setup};
|
|||
|
||||
/// self send impl
|
||||
fn invoice_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> {
|
||||
{
|
||||
setup(test_dir);
|
||||
// Create a new proxy to simulate server and wallet responses
|
||||
let mut wallet_proxy = create_wallet_proxy(test_dir);
|
||||
let chain = wallet_proxy.chain.clone();
|
||||
|
||||
// Create a new proxy to simulate server and wallet responses
|
||||
let mut wallet_proxy = create_wallet_proxy(test_dir);
|
||||
let chain = wallet_proxy.chain.clone();
|
||||
create_wallet_and_add!(
|
||||
client1,
|
||||
wallet1,
|
||||
mask1_i,
|
||||
test_dir,
|
||||
"wallet1",
|
||||
None,
|
||||
&mut wallet_proxy,
|
||||
true
|
||||
);
|
||||
let mask1 = (&mask1_i).as_ref();
|
||||
create_wallet_and_add!(
|
||||
client2,
|
||||
wallet2,
|
||||
mask2_i,
|
||||
test_dir,
|
||||
"wallet2",
|
||||
None,
|
||||
&mut wallet_proxy,
|
||||
true
|
||||
);
|
||||
let mask2 = (&mask2_i).as_ref();
|
||||
|
||||
create_wallet_and_add!(
|
||||
client1,
|
||||
wallet1,
|
||||
mask1_i,
|
||||
test_dir,
|
||||
"wallet1",
|
||||
None,
|
||||
&mut wallet_proxy,
|
||||
true
|
||||
);
|
||||
let mask1 = (&mask1_i).as_ref();
|
||||
create_wallet_and_add!(
|
||||
client2,
|
||||
wallet2,
|
||||
mask2_i,
|
||||
test_dir,
|
||||
"wallet2",
|
||||
None,
|
||||
&mut wallet_proxy,
|
||||
true
|
||||
);
|
||||
let mask2 = (&mask2_i).as_ref();
|
||||
|
||||
// Set the wallet proxy listener running
|
||||
thread::spawn(move || {
|
||||
if let Err(e) = wallet_proxy.run() {
|
||||
error!("Wallet Proxy error: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
// few values to keep things shorter
|
||||
let reward = core::consensus::REWARD;
|
||||
|
||||
// add some accounts
|
||||
wallet::controller::owner_single_use(wallet1.clone(), mask1, |api, m| {
|
||||
api.create_account_path(m, "mining")?;
|
||||
api.create_account_path(m, "listener")?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// Get some mining done
|
||||
{
|
||||
wallet_inst!(wallet1, w);
|
||||
w.set_parent_key_id_by_name("mining")?;
|
||||
// Set the wallet proxy listener running
|
||||
thread::spawn(move || {
|
||||
if let Err(e) = wallet_proxy.run() {
|
||||
error!("Wallet Proxy error: {}", e);
|
||||
}
|
||||
let mut bh = 10u64;
|
||||
let _ = test_framework::award_blocks_to_wallet(
|
||||
&chain,
|
||||
wallet1.clone(),
|
||||
mask1,
|
||||
bh as usize,
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
// Sanity check wallet 1 contents
|
||||
wallet::controller::owner_single_use(wallet1.clone(), mask1, |api, m| {
|
||||
let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?;
|
||||
assert!(wallet1_refreshed);
|
||||
assert_eq!(wallet1_info.last_confirmed_height, bh);
|
||||
assert_eq!(wallet1_info.total, bh * reward);
|
||||
Ok(())
|
||||
})?;
|
||||
// few values to keep things shorter
|
||||
let reward = core::consensus::REWARD;
|
||||
|
||||
let mut slate = Slate::blank(2);
|
||||
// add some accounts
|
||||
wallet::controller::owner_single_use(wallet1.clone(), mask1, |api, m| {
|
||||
api.create_account_path(m, "mining")?;
|
||||
api.create_account_path(m, "listener")?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
wallet::controller::owner_single_use(wallet2.clone(), mask2, |api, m| {
|
||||
// Wallet 2 inititates an invoice transaction, requesting payment
|
||||
let args = IssueInvoiceTxArgs {
|
||||
amount: reward * 2,
|
||||
..Default::default()
|
||||
};
|
||||
slate = api.issue_invoice_tx(m, args)?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
wallet::controller::owner_single_use(wallet1.clone(), mask1, |api, m| {
|
||||
// Wallet 1 receives the invoice transaction
|
||||
let args = InitTxArgs {
|
||||
src_acct_name: None,
|
||||
amount: slate.amount,
|
||||
minimum_confirmations: 2,
|
||||
max_outputs: 500,
|
||||
num_change_outputs: 1,
|
||||
selection_strategy_is_use_all: true,
|
||||
..Default::default()
|
||||
};
|
||||
slate = api.process_invoice_tx(m, &slate, args)?;
|
||||
api.tx_lock_outputs(m, &slate, 0)?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// wallet 2 finalizes and posts
|
||||
wallet::controller::foreign_single_use(wallet2.clone(), mask2_i.clone(), |api| {
|
||||
// Wallet 2 receives the invoice transaction
|
||||
slate = api.finalize_invoice_tx(&slate)?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// wallet 1 posts so wallet 2 doesn't get the mined amount
|
||||
wallet::controller::owner_single_use(wallet1.clone(), mask1, |api, m| {
|
||||
api.post_tx(m, &slate.tx, false)?;
|
||||
Ok(())
|
||||
})?;
|
||||
bh += 1;
|
||||
|
||||
let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 3, false);
|
||||
bh += 3;
|
||||
|
||||
// Check transaction log for wallet 2
|
||||
wallet::controller::owner_single_use(wallet2.clone(), mask2, |api, m| {
|
||||
let (_, wallet2_info) = api.retrieve_summary_info(m, true, 1)?;
|
||||
let (refreshed, txs) = api.retrieve_txs(m, true, None, None)?;
|
||||
assert!(refreshed);
|
||||
assert!(txs.len() == 1);
|
||||
println!(
|
||||
"last confirmed height: {}, bh: {}",
|
||||
wallet2_info.last_confirmed_height, bh
|
||||
);
|
||||
assert!(refreshed);
|
||||
assert_eq!(wallet2_info.amount_currently_spendable, slate.amount);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// Check transaction log for wallet 1, ensure only 1 entry
|
||||
// exists
|
||||
wallet::controller::owner_single_use(wallet1.clone(), mask1, |api, m| {
|
||||
let (_, wallet1_info) = api.retrieve_summary_info(m, true, 1)?;
|
||||
let (refreshed, txs) = api.retrieve_txs(m, true, None, None)?;
|
||||
assert!(refreshed);
|
||||
assert_eq!(txs.len() as u64, bh + 1);
|
||||
println!(
|
||||
"Wallet 1: last confirmed height: {}, bh: {}",
|
||||
wallet1_info.last_confirmed_height, bh
|
||||
);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// Test self-sending
|
||||
wallet::controller::owner_single_use(wallet1.clone(), mask1, |api, m| {
|
||||
// Wallet 1 inititates an invoice transaction, requesting payment
|
||||
let args = IssueInvoiceTxArgs {
|
||||
amount: reward * 2,
|
||||
..Default::default()
|
||||
};
|
||||
slate = api.issue_invoice_tx(m, args)?;
|
||||
// Wallet 1 receives the invoice transaction
|
||||
let args = InitTxArgs {
|
||||
src_acct_name: None,
|
||||
amount: slate.amount,
|
||||
minimum_confirmations: 2,
|
||||
max_outputs: 500,
|
||||
num_change_outputs: 1,
|
||||
selection_strategy_is_use_all: true,
|
||||
..Default::default()
|
||||
};
|
||||
slate = api.process_invoice_tx(m, &slate, args)?;
|
||||
api.tx_lock_outputs(m, &slate, 0)?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// wallet 1 finalizes and posts
|
||||
wallet::controller::foreign_single_use(wallet1.clone(), mask1_i.clone(), |api| {
|
||||
// Wallet 2 receives the invoice transaction
|
||||
slate = api.finalize_invoice_tx(&slate)?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 3, false);
|
||||
//bh += 3;
|
||||
|
||||
// let logging finish
|
||||
thread::sleep(Duration::from_millis(200));
|
||||
// Get some mining done
|
||||
{
|
||||
wallet_inst!(wallet1, w);
|
||||
w.set_parent_key_id_by_name("mining")?;
|
||||
}
|
||||
let mut bh = 10u64;
|
||||
let _ =
|
||||
test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, bh as usize, false);
|
||||
|
||||
// Sanity check wallet 1 contents
|
||||
wallet::controller::owner_single_use(wallet1.clone(), mask1, |api, m| {
|
||||
let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?;
|
||||
assert!(wallet1_refreshed);
|
||||
assert_eq!(wallet1_info.last_confirmed_height, bh);
|
||||
assert_eq!(wallet1_info.total, bh * reward);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
let mut slate = Slate::blank(2);
|
||||
|
||||
wallet::controller::owner_single_use(wallet2.clone(), mask2, |api, m| {
|
||||
// Wallet 2 inititates an invoice transaction, requesting payment
|
||||
let args = IssueInvoiceTxArgs {
|
||||
amount: reward * 2,
|
||||
..Default::default()
|
||||
};
|
||||
slate = api.issue_invoice_tx(m, args)?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
wallet::controller::owner_single_use(wallet1.clone(), mask1, |api, m| {
|
||||
// Wallet 1 receives the invoice transaction
|
||||
let args = InitTxArgs {
|
||||
src_acct_name: None,
|
||||
amount: slate.amount,
|
||||
minimum_confirmations: 2,
|
||||
max_outputs: 500,
|
||||
num_change_outputs: 1,
|
||||
selection_strategy_is_use_all: true,
|
||||
..Default::default()
|
||||
};
|
||||
slate = api.process_invoice_tx(m, &slate, args)?;
|
||||
api.tx_lock_outputs(m, &slate, 0)?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// wallet 2 finalizes and posts
|
||||
wallet::controller::foreign_single_use(wallet2.clone(), mask2_i.clone(), |api| {
|
||||
// Wallet 2 receives the invoice transaction
|
||||
slate = api.finalize_invoice_tx(&slate)?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// wallet 1 posts so wallet 2 doesn't get the mined amount
|
||||
wallet::controller::owner_single_use(wallet1.clone(), mask1, |api, m| {
|
||||
api.post_tx(m, &slate.tx, false)?;
|
||||
Ok(())
|
||||
})?;
|
||||
bh += 1;
|
||||
|
||||
let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 3, false);
|
||||
bh += 3;
|
||||
|
||||
// Check transaction log for wallet 2
|
||||
wallet::controller::owner_single_use(wallet2.clone(), mask2, |api, m| {
|
||||
let (_, wallet2_info) = api.retrieve_summary_info(m, true, 1)?;
|
||||
let (refreshed, txs) = api.retrieve_txs(m, true, None, None)?;
|
||||
assert!(refreshed);
|
||||
assert!(txs.len() == 1);
|
||||
println!(
|
||||
"last confirmed height: {}, bh: {}",
|
||||
wallet2_info.last_confirmed_height, bh
|
||||
);
|
||||
assert!(refreshed);
|
||||
assert_eq!(wallet2_info.amount_currently_spendable, slate.amount);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// Check transaction log for wallet 1, ensure only 1 entry
|
||||
// exists
|
||||
wallet::controller::owner_single_use(wallet1.clone(), mask1, |api, m| {
|
||||
let (_, wallet1_info) = api.retrieve_summary_info(m, true, 1)?;
|
||||
let (refreshed, txs) = api.retrieve_txs(m, true, None, None)?;
|
||||
assert!(refreshed);
|
||||
assert_eq!(txs.len() as u64, bh + 1);
|
||||
println!(
|
||||
"Wallet 1: last confirmed height: {}, bh: {}",
|
||||
wallet1_info.last_confirmed_height, bh
|
||||
);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// Test self-sending
|
||||
wallet::controller::owner_single_use(wallet1.clone(), mask1, |api, m| {
|
||||
// Wallet 1 inititates an invoice transaction, requesting payment
|
||||
let args = IssueInvoiceTxArgs {
|
||||
amount: reward * 2,
|
||||
..Default::default()
|
||||
};
|
||||
slate = api.issue_invoice_tx(m, args)?;
|
||||
// Wallet 1 receives the invoice transaction
|
||||
let args = InitTxArgs {
|
||||
src_acct_name: None,
|
||||
amount: slate.amount,
|
||||
minimum_confirmations: 2,
|
||||
max_outputs: 500,
|
||||
num_change_outputs: 1,
|
||||
selection_strategy_is_use_all: true,
|
||||
..Default::default()
|
||||
};
|
||||
slate = api.process_invoice_tx(m, &slate, args)?;
|
||||
api.tx_lock_outputs(m, &slate, 0)?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// wallet 1 finalizes and posts
|
||||
wallet::controller::foreign_single_use(wallet1.clone(), mask1_i.clone(), |api| {
|
||||
// Wallet 2 receives the invoice transaction
|
||||
slate = api.finalize_invoice_tx(&slate)?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 3, false);
|
||||
//bh += 3;
|
||||
|
||||
// let logging finish
|
||||
thread::sleep(Duration::from_millis(200));
|
||||
|
||||
clean_output_dir(test_dir);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wallet_invoice_tx() -> Result<(), libwallet::Error> {
|
||||
let test_dir = "test_output/invoice_tx";
|
||||
invoice_tx_impl(test_dir)
|
||||
setup(test_dir);
|
||||
invoice_tx_impl(test_dir)?;
|
||||
clean_output_dir(test_dir);
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -28,12 +28,10 @@ use std::time::Duration;
|
|||
|
||||
#[macro_use]
|
||||
mod common;
|
||||
use common::{create_wallet_proxy, setup};
|
||||
use common::{clean_output_dir, create_wallet_proxy, setup};
|
||||
|
||||
/// self send impl
|
||||
fn file_repost_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> {
|
||||
setup(test_dir);
|
||||
|
||||
// Create a new proxy to simulate server and wallet responses
|
||||
let mut wallet_proxy = create_wallet_proxy(test_dir);
|
||||
let chain = wallet_proxy.chain.clone();
|
||||
|
@ -257,7 +255,9 @@ fn file_repost_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error>
|
|||
#[test]
|
||||
fn wallet_file_repost() {
|
||||
let test_dir = "test_output/file_repost";
|
||||
setup(test_dir);
|
||||
if let Err(e) = file_repost_test_impl(test_dir) {
|
||||
panic!("Libwallet Error: {} - {}", e, e.backtrace().unwrap());
|
||||
}
|
||||
clean_output_dir(test_dir);
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ use std::time::Duration;
|
|||
|
||||
#[macro_use]
|
||||
mod common;
|
||||
use common::{create_wallet_proxy, setup};
|
||||
use common::{clean_output_dir, create_wallet_proxy, setup};
|
||||
|
||||
fn restore_wallet(base_dir: &'static str, wallet_dir: &str) -> Result<(), libwallet::Error> {
|
||||
let source_seed = format!("{}/{}/wallet_data/wallet.seed", base_dir, wallet_dir);
|
||||
|
@ -196,8 +196,6 @@ fn compare_wallet_restore(
|
|||
/// 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: &'static str) -> Result<(), libwallet::Error> {
|
||||
setup(test_dir);
|
||||
|
||||
// Create a new proxy to simulate server and wallet responses
|
||||
let mut wallet_proxy = create_wallet_proxy(test_dir);
|
||||
let chain = wallet_proxy.chain.clone();
|
||||
|
@ -417,6 +415,7 @@ fn perform_restore(test_dir: &'static str) -> Result<(), libwallet::Error> {
|
|||
#[test]
|
||||
fn wallet_restore() {
|
||||
let test_dir = "test_output/wallet_restore";
|
||||
setup(test_dir);
|
||||
if let Err(e) = setup_restore(test_dir) {
|
||||
panic!("Libwallet Error: {} - {}", e, e.backtrace().unwrap());
|
||||
}
|
||||
|
@ -425,4 +424,5 @@ fn wallet_restore() {
|
|||
}
|
||||
// let logging finish
|
||||
thread::sleep(Duration::from_millis(200));
|
||||
clean_output_dir(test_dir);
|
||||
}
|
||||
|
|
|
@ -27,11 +27,10 @@ use std::time::Duration;
|
|||
|
||||
#[macro_use]
|
||||
mod common;
|
||||
use common::{create_wallet_proxy, setup};
|
||||
use common::{clean_output_dir, create_wallet_proxy, setup};
|
||||
|
||||
/// self send impl
|
||||
fn self_send_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> {
|
||||
setup(test_dir);
|
||||
// Create a new proxy to simulate server and wallet responses
|
||||
let mut wallet_proxy = create_wallet_proxy(test_dir);
|
||||
let chain = wallet_proxy.chain.clone();
|
||||
|
@ -138,7 +137,9 @@ fn self_send_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> {
|
|||
#[test]
|
||||
fn wallet_self_send() {
|
||||
let test_dir = "test_output/self_send";
|
||||
setup(test_dir);
|
||||
if let Err(e) = self_send_test_impl(test_dir) {
|
||||
panic!("Libwallet Error: {} - {}", e, e.backtrace().unwrap());
|
||||
}
|
||||
clean_output_dir(test_dir);
|
||||
}
|
||||
|
|
|
@ -28,13 +28,12 @@ use std::thread;
|
|||
use std::time::Duration;
|
||||
|
||||
mod common;
|
||||
use common::{create_wallet_proxy, setup};
|
||||
use common::{clean_output_dir, create_wallet_proxy, setup};
|
||||
|
||||
/// Exercises the Transaction API fully with a test NodeClient operating
|
||||
/// directly on a chain instance
|
||||
/// Callable with any type of wallet
|
||||
fn basic_transaction_api(test_dir: &'static str) -> Result<(), libwallet::Error> {
|
||||
setup(test_dir);
|
||||
// Create a new proxy to simulate server and wallet responses
|
||||
let mut wallet_proxy = create_wallet_proxy(test_dir);
|
||||
let chain = wallet_proxy.chain.clone();
|
||||
|
@ -350,7 +349,6 @@ fn basic_transaction_api(test_dir: &'static str) -> Result<(), libwallet::Error>
|
|||
/// Test rolling back transactions and outputs when a transaction is never
|
||||
/// posted to a chain
|
||||
fn tx_rollback(test_dir: &'static str) -> Result<(), libwallet::Error> {
|
||||
setup(test_dir);
|
||||
// Create a new proxy to simulate server and wallet responses
|
||||
let mut wallet_proxy = create_wallet_proxy(test_dir);
|
||||
let chain = wallet_proxy.chain.clone();
|
||||
|
@ -528,15 +526,19 @@ fn tx_rollback(test_dir: &'static str) -> Result<(), libwallet::Error> {
|
|||
#[test]
|
||||
fn db_wallet_basic_transaction_api() {
|
||||
let test_dir = "test_output/basic_transaction_api";
|
||||
setup(test_dir);
|
||||
if let Err(e) = basic_transaction_api(test_dir) {
|
||||
panic!("Libwallet Error: {} - {}", e, e.backtrace().unwrap());
|
||||
}
|
||||
clean_output_dir(test_dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn db_wallet_tx_rollback() {
|
||||
let test_dir = "test_output/tx_rollback";
|
||||
setup(test_dir);
|
||||
if let Err(e) = tx_rollback(test_dir) {
|
||||
panic!("Libwallet Error: {} - {}", e, e.backtrace().unwrap());
|
||||
}
|
||||
clean_output_dir(test_dir);
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ impl HttpSlateSender {
|
|||
}
|
||||
|
||||
/// Check version of the listening wallet
|
||||
fn check_other_version(&self) -> Result<(), Error> {
|
||||
fn check_other_version(&self, url: &Url) -> Result<(), Error> {
|
||||
let req = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"method": "check_version",
|
||||
|
@ -44,7 +44,7 @@ impl HttpSlateSender {
|
|||
"params": []
|
||||
});
|
||||
|
||||
let res: String = post(&self.base_url, None, &req).map_err(|e| {
|
||||
let res: String = post(url, None, &req).map_err(|e| {
|
||||
let mut report = format!("Performing version check (is recipient listening?): {}", e);
|
||||
let err_string = format!("{}", e);
|
||||
if err_string.contains("404") {
|
||||
|
@ -101,7 +101,7 @@ impl SlateSender for HttpSlateSender {
|
|||
.expect("/v2/foreign is an invalid url path");
|
||||
debug!("Posting transaction slate to {}", url);
|
||||
|
||||
self.check_other_version()?;
|
||||
self.check_other_version(&url)?;
|
||||
|
||||
// Note: not using easy-jsonrpc as don't want the dependencies in this crate
|
||||
let req = json!({
|
||||
|
|
|
@ -359,7 +359,7 @@ impl SlateReceiver for KeybaseAllChannels {
|
|||
DefaultWalletImpl::<'static, HTTPNodeClient>::new(node_client.clone()).unwrap(),
|
||||
)
|
||||
as Box<
|
||||
WalletInst<
|
||||
dyn WalletInst<
|
||||
'static,
|
||||
DefaultLCProvider<HTTPNodeClient, ExtKeychain>,
|
||||
HTTPNodeClient,
|
||||
|
|
|
@ -124,6 +124,7 @@ where
|
|||
match LMDBBackend::new(&data_dir_name, self.node_client.clone()) {
|
||||
Err(e) => {
|
||||
let msg = format!("Error creating wallet: {}, Data Dir: {}", e, &data_dir_name);
|
||||
error!("{}", msg);
|
||||
return Err(ErrorKind::Lifecycle(msg).into());
|
||||
}
|
||||
Ok(d) => d,
|
||||
|
|
|
@ -117,6 +117,10 @@ pub enum ErrorKind {
|
|||
#[fail(display = "Signature error: {}", _0)]
|
||||
Signature(String),
|
||||
|
||||
/// OwnerAPIEncryption
|
||||
#[fail(display = "{}", _0)]
|
||||
APIEncryption(String),
|
||||
|
||||
/// Attempt to use duplicate transaction id in separate transactions
|
||||
#[fail(display = "Duplicate transaction ID error")]
|
||||
DuplicateTransactionId,
|
||||
|
|
|
@ -99,7 +99,7 @@ fn real_main() -> i32 {
|
|||
panic!("Error loading wallet configuration: {}", e);
|
||||
});
|
||||
|
||||
config.members.as_mut().unwrap().wallet.chain_type = Some(chain_type);
|
||||
//config.members.as_mut().unwrap().wallet.chain_type = Some(chain_type);
|
||||
|
||||
// Load logging config
|
||||
let l = config.members.as_mut().unwrap().logging.clone().unwrap();
|
||||
|
|
|
@ -70,6 +70,12 @@ subcommands:
|
|||
takes_value: true
|
||||
- owner_api:
|
||||
about: Runs the wallet's local web API
|
||||
args:
|
||||
- port:
|
||||
help: Port on which to run the wallet owner listener
|
||||
short: l
|
||||
long: port
|
||||
takes_value: true
|
||||
- send:
|
||||
about: Builds a transaction to send coins and sends to the specified listener directly
|
||||
args:
|
||||
|
|
|
@ -218,7 +218,7 @@ fn prompt_pay_invoice(slate: &Slate, method: &str, dest: &str) -> Result<bool, P
|
|||
pub fn inst_wallet<L, C, K>(
|
||||
config: WalletConfig,
|
||||
node_client: C,
|
||||
) -> Result<Arc<Mutex<Box<WalletInst<'static, L, C, K>>>>, ParseError>
|
||||
) -> Result<Arc<Mutex<Box<dyn WalletInst<'static, L, C, K>>>>, ParseError>
|
||||
where
|
||||
DefaultWalletImpl<'static, C>: WalletInst<'static, L, C, K>,
|
||||
L: WalletLCProvider<'static, C, K>,
|
||||
|
@ -226,7 +226,7 @@ where
|
|||
K: keychain::Keychain + 'static,
|
||||
{
|
||||
let mut wallet = Box::new(DefaultWalletImpl::<'static, C>::new(node_client.clone()).unwrap())
|
||||
as Box<WalletInst<'static, L, C, K>>;
|
||||
as Box<dyn WalletInst<'static, L, C, K>>;
|
||||
let lc = wallet.lc_provider().unwrap();
|
||||
lc.set_wallet_directory(&config.data_file_dir);
|
||||
Ok(Arc::new(Mutex::new(wallet)))
|
||||
|
@ -396,6 +396,16 @@ pub fn parse_listen_args(
|
|||
})
|
||||
}
|
||||
|
||||
pub fn parse_owner_api_args(
|
||||
config: &mut WalletConfig,
|
||||
args: &ArgMatches,
|
||||
) -> Result<(), ParseError> {
|
||||
if let Some(port) = args.value_of("port") {
|
||||
config.owner_api_listen_port = Some(port.parse().unwrap());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn parse_account_args(account_args: &ArgMatches) -> Result<command::AccountArgs, ParseError> {
|
||||
let create = match account_args.value_of("create") {
|
||||
None => None,
|
||||
|
@ -811,6 +821,9 @@ where
|
|||
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 wallet_lock = wallet.lock();
|
||||
let lc = wallet_lock.lc_provider().unwrap();
|
||||
|
@ -853,11 +866,12 @@ where
|
|||
let a = arg_parse!(parse_listen_args(&mut c, &args));
|
||||
command::listen(wallet, keychain_mask, &c, &a, &global_wallet_args.clone())
|
||||
}
|
||||
("owner_api", Some(_)) => {
|
||||
("owner_api", Some(args)) => {
|
||||
let mut c = wallet_config.clone();
|
||||
let mut g = global_wallet_args.clone();
|
||||
g.tls_conf = None;
|
||||
print!("mask: {:?}", keychain_mask);
|
||||
command::owner_api(wallet, keychain_mask, &wallet_config, &g)
|
||||
arg_parse!(parse_owner_api_args(&mut c, &args));
|
||||
command::owner_api(wallet, keychain_mask, &c, &g)
|
||||
}
|
||||
("web", Some(_)) => {
|
||||
command::owner_api(wallet, keychain_mask, &wallet_config, &global_wallet_args)
|
||||
|
|
|
@ -30,7 +30,7 @@ use grin_wallet_impls::DefaultLCProvider;
|
|||
use grin_wallet_util::grin_keychain::ExtKeychain;
|
||||
|
||||
mod common;
|
||||
use common::{execute_command, initial_setup_wallet, instantiate_wallet, setup};
|
||||
use common::{clean_output_dir, execute_command, initial_setup_wallet, instantiate_wallet, setup};
|
||||
|
||||
/// command line tests
|
||||
fn command_line_test_impl(test_dir: &str) -> Result<(), grin_wallet_controller::Error> {
|
||||
|
@ -482,6 +482,7 @@ fn command_line_test_impl(test_dir: &str) -> Result<(), grin_wallet_controller::
|
|||
|
||||
// let logging finish
|
||||
thread::sleep(Duration::from_millis(200));
|
||||
clean_output_dir(test_dir);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
//! Common functions for wallet integration tests
|
||||
extern crate grin_wallet;
|
||||
|
||||
use grin_wallet_config as config;
|
||||
use grin_wallet_impls::test_framework::LocalWalletClient;
|
||||
use grin_wallet_util::grin_util as util;
|
||||
|
||||
|
@ -23,12 +24,14 @@ use std::sync::Arc;
|
|||
use std::{env, fs};
|
||||
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_util::grin_core::global::{self, ChainTypes};
|
||||
use grin_wallet_util::grin_keychain::ExtKeychain;
|
||||
use util::secp::key::SecretKey;
|
||||
use grin_wallet_util::grin_util::{from_hex, static_secp_instance};
|
||||
use util::secp::key::{PublicKey, SecretKey};
|
||||
|
||||
use grin_wallet::cmd::wallet_args;
|
||||
use grin_wallet_util::grin_api as api;
|
||||
|
@ -100,7 +103,7 @@ macro_rules! setup_proxy {
|
|||
};
|
||||
}
|
||||
|
||||
fn clean_output_dir(test_dir: &str) {
|
||||
pub fn clean_output_dir(test_dir: &str) {
|
||||
let _ = fs::remove_dir_all(test_dir);
|
||||
}
|
||||
|
||||
|
@ -192,7 +195,7 @@ pub fn instantiate_wallet(
|
|||
Arc<
|
||||
Mutex<
|
||||
Box<
|
||||
WalletInst<
|
||||
dyn WalletInst<
|
||||
'static,
|
||||
DefaultLCProvider<'static, LocalWalletClient, ExtKeychain>,
|
||||
LocalWalletClient,
|
||||
|
@ -208,7 +211,7 @@ pub fn instantiate_wallet(
|
|||
wallet_config.chain_type = None;
|
||||
let mut wallet = Box::new(DefaultWalletImpl::<LocalWalletClient>::new(node_client).unwrap())
|
||||
as Box<
|
||||
WalletInst<
|
||||
dyn WalletInst<
|
||||
DefaultLCProvider<'static, LocalWalletClient, ExtKeychain>,
|
||||
LocalWalletClient,
|
||||
ExtKeychain,
|
||||
|
@ -247,6 +250,25 @@ pub fn execute_command(
|
|||
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(
|
||||
app: &App,
|
||||
test_dir: &str,
|
||||
wallet_name: &str,
|
||||
client: &LocalWalletClient,
|
||||
arg_vec: Vec<&str>,
|
||||
) -> Result<String, grin_wallet_controller::Error> {
|
||||
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();
|
||||
let mut wallet_config = config.members.unwrap().wallet.clone();
|
||||
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)
|
||||
}
|
||||
|
||||
pub fn post<IN>(url: &Url, api_secret: Option<String>, input: &IN) -> Result<String, api::Error>
|
||||
where
|
||||
IN: Serialize,
|
||||
|
@ -257,6 +279,7 @@ where
|
|||
Ok(res)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn send_request<OUT>(
|
||||
id: u64,
|
||||
dest: &str,
|
||||
|
@ -266,19 +289,30 @@ where
|
|||
OUT: DeserializeOwned,
|
||||
{
|
||||
let url = Url::parse(dest).unwrap();
|
||||
let req: Value = serde_json::from_str(req).unwrap();
|
||||
let res: String = post(&url, None, &req).map_err(|e| {
|
||||
let req_val: Value = serde_json::from_str(req).unwrap();
|
||||
let res = post(&url, None, &req_val).map_err(|e| {
|
||||
let err_string = format!("{}", e);
|
||||
println!("{}", err_string);
|
||||
thread::sleep(Duration::from_millis(200));
|
||||
e
|
||||
})?;
|
||||
|
||||
let res_val: Value = serde_json::from_str(&res).unwrap();
|
||||
// encryption error, just return the string
|
||||
if res_val["error"] != json!(null) {
|
||||
return Ok(Err(WalletAPIReturnError {
|
||||
message: res_val["error"]["message"].as_str().unwrap().to_owned(),
|
||||
code: res_val["error"]["code"].as_i64().unwrap() as i32,
|
||||
}));
|
||||
}
|
||||
|
||||
let res = serde_json::from_str(&res).unwrap();
|
||||
let res = easy_jsonrpc::Response::from_json_response(res).unwrap();
|
||||
let res = res.outputs.get(&id).unwrap().clone().unwrap();
|
||||
if res["Err"] != json!(null) {
|
||||
Ok(Err(WalletAPIReturnError {
|
||||
message: res["Err"].as_str().unwrap().to_owned(),
|
||||
code: res["error"]["code"].as_i64().unwrap() as i32,
|
||||
}))
|
||||
} else {
|
||||
// deserialize result into expected type
|
||||
|
@ -287,10 +321,88 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn send_request_enc<OUT>(
|
||||
sec_req_id: u32,
|
||||
internal_request_id: u32,
|
||||
dest: &str,
|
||||
req: &str,
|
||||
shared_key: &SecretKey,
|
||||
) -> Result<Result<OUT, WalletAPIReturnError>, api::Error>
|
||||
where
|
||||
OUT: DeserializeOwned,
|
||||
{
|
||||
let url = Url::parse(dest).unwrap();
|
||||
let req_val: Value = serde_json::from_str(req).unwrap();
|
||||
let req = EncryptedRequest::from_json(sec_req_id, &req_val, &shared_key).unwrap();
|
||||
let res = post(&url, None, &req).map_err(|e| {
|
||||
let err_string = format!("{}", e);
|
||||
println!("{}", err_string);
|
||||
thread::sleep(Duration::from_millis(200));
|
||||
e
|
||||
})?;
|
||||
|
||||
let res_val: Value = serde_json::from_str(&res).unwrap();
|
||||
// encryption error, just return the string
|
||||
if res_val["error"] != json!(null) {
|
||||
return Ok(Err(WalletAPIReturnError {
|
||||
message: res_val["error"]["message"].as_str().unwrap().to_owned(),
|
||||
code: res_val["error"]["code"].as_i64().unwrap() as i32,
|
||||
}));
|
||||
}
|
||||
|
||||
let enc_resp: EncryptedResponse = serde_json::from_str(&res).unwrap();
|
||||
let res = enc_resp.decrypt(shared_key).unwrap();
|
||||
if res["error"] != json!(null) {
|
||||
return Ok(Err(WalletAPIReturnError {
|
||||
message: res["error"]["message"].as_str().unwrap().to_owned(),
|
||||
code: res["error"]["code"].as_i64().unwrap() as i32,
|
||||
}));
|
||||
}
|
||||
let res = easy_jsonrpc::Response::from_json_response(res).unwrap();
|
||||
let res = res
|
||||
.outputs
|
||||
.get(&(internal_request_id as u64))
|
||||
.unwrap()
|
||||
.clone()
|
||||
.unwrap();
|
||||
|
||||
if res["Err"] != json!(null) {
|
||||
Ok(Err(WalletAPIReturnError {
|
||||
message: res["Err"].as_str().unwrap().to_owned(),
|
||||
code: res_val["error"]["code"].as_i64().unwrap() as i32,
|
||||
}))
|
||||
} else {
|
||||
// deserialize result into expected type
|
||||
let value: OUT = serde_json::from_value(res["Ok"].clone()).unwrap();
|
||||
Ok(Ok(value))
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn derive_ecdh_key(sec_key_str: &str, other_pubkey: &PublicKey) -> SecretKey {
|
||||
let sec_key_bytes = from_hex(sec_key_str.to_owned()).unwrap();
|
||||
let sec_key = {
|
||||
let secp_inst = static_secp_instance();
|
||||
let secp = secp_inst.lock();
|
||||
SecretKey::from_slice(&secp, &sec_key_bytes).unwrap()
|
||||
};
|
||||
|
||||
let secp_inst = static_secp_instance();
|
||||
let secp = secp_inst.lock();
|
||||
|
||||
let mut shared_pubkey = other_pubkey.clone();
|
||||
shared_pubkey.mul_assign(&secp, &sec_key).unwrap();
|
||||
|
||||
let x_coord = shared_pubkey.serialize_vec(&secp, true);
|
||||
SecretKey::from_slice(&secp, &x_coord[1..]).unwrap()
|
||||
}
|
||||
|
||||
// Types to make working with json responses easier
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct WalletAPIReturnError {
|
||||
message: String,
|
||||
pub message: String,
|
||||
pub code: i32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
|
|
9
tests/data/v2_reqs/retrieve_info.req.json
Normal file
9
tests/data/v2_reqs/retrieve_info.req.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "retrieve_summary_info",
|
||||
"params": [
|
||||
true,
|
||||
1
|
||||
],
|
||||
"id": 1
|
||||
}
|
8
tests/data/v3_reqs/init_secure_api.req.json
Normal file
8
tests/data/v3_reqs/init_secure_api.req.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "init_secure_api",
|
||||
"params": {
|
||||
"ecdh_pubkey": "03b3c18c9a38783d105e238953b1638b021ba7456d87a5c085b3bdb75777b4c490"
|
||||
},
|
||||
"id": 1
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "open_wallet",
|
||||
"params": {
|
||||
"token": null,
|
||||
"refresh_from_node": true,
|
||||
"minimum_confirmations": 1
|
||||
},
|
||||
"id": 1
|
||||
}
|
|
@ -31,11 +31,14 @@ use grin_wallet_util::grin_keychain::ExtKeychain;
|
|||
#[macro_use]
|
||||
mod common;
|
||||
use common::RetrieveSummaryInfoResp;
|
||||
use common::{execute_command, initial_setup_wallet, instantiate_wallet, send_request, setup};
|
||||
use common::{
|
||||
clean_output_dir, execute_command, initial_setup_wallet, instantiate_wallet, send_request,
|
||||
setup,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn owner_v3() -> Result<(), grin_wallet_controller::Error> {
|
||||
let test_dir = "target/test_output/owner_v3";
|
||||
fn owner_v2_sanity() -> Result<(), grin_wallet_controller::Error> {
|
||||
let test_dir = "target/test_output/owner_v2_sanity";
|
||||
setup(test_dir);
|
||||
|
||||
setup_proxy!(test_dir, chain, wallet1, client1, mask1, wallet2, client2, _mask2);
|
||||
|
@ -44,6 +47,7 @@ fn owner_v3() -> Result<(), grin_wallet_controller::Error> {
|
|||
let bh = 10u64;
|
||||
let _ =
|
||||
test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, bh as usize, false);
|
||||
let client1_2 = client1.clone();
|
||||
|
||||
// run the owner listener on wallet 1
|
||||
let arg_vec = vec!["grin-wallet", "-p", "password", "owner_api"];
|
||||
|
@ -55,7 +59,7 @@ fn owner_v3() -> Result<(), grin_wallet_controller::Error> {
|
|||
});
|
||||
|
||||
// run the foreign listener for wallet 2
|
||||
let arg_vec = vec!["grin-wallet", "-p", "password", "listen"];
|
||||
let arg_vec = vec!["grin-wallet", "-p", "password", "listen", "-l", "23415"];
|
||||
// Set owner listener running
|
||||
thread::spawn(move || {
|
||||
let yml = load_yaml!("../src/bin/grin-wallet.yml");
|
||||
|
@ -65,12 +69,30 @@ fn owner_v3() -> Result<(), grin_wallet_controller::Error> {
|
|||
|
||||
thread::sleep(Duration::from_millis(200));
|
||||
|
||||
// Send simple retrieve_info request to owner listener
|
||||
let req = include_str!("data/v3_reqs/retrieve_info.req.json");
|
||||
let res = send_request(1, "http://127.0.0.1:3420/v3/owner", req)?;
|
||||
// 1) Send simple retrieve_info request to owner listener
|
||||
let req = include_str!("data/v2_reqs/retrieve_info.req.json");
|
||||
let res = send_request(1, "http://127.0.0.1:3420/v2/owner", req)?;
|
||||
assert!(res.is_ok());
|
||||
let value: RetrieveSummaryInfoResp = res.unwrap();
|
||||
assert_eq!(value.1.amount_currently_spendable, 420000000000);
|
||||
println!("Response: {:?}", value);
|
||||
println!("Response 1: {:?}", value);
|
||||
|
||||
// 2) Send to wallet 2 foreign listener
|
||||
let arg_vec = vec![
|
||||
"grin-wallet",
|
||||
"-p",
|
||||
"password",
|
||||
"send",
|
||||
"-d",
|
||||
"http://127.0.0.1:23415",
|
||||
"10",
|
||||
];
|
||||
let yml = load_yaml!("../src/bin/grin-wallet.yml");
|
||||
let app = App::from_yaml(yml);
|
||||
let res = execute_command(&app, test_dir, "wallet1", &client1_2, arg_vec.clone());
|
||||
println!("Response 2: {:?}", res);
|
||||
assert!(res.is_ok());
|
||||
|
||||
clean_output_dir(test_dir);
|
||||
Ok(())
|
||||
}
|
219
tests/owner_v3_init_secure.rs
Normal file
219
tests/owner_v3_init_secure.rs
Normal file
|
@ -0,0 +1,219 @@
|
|||
// 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_util::grin_keychain::ExtKeychain;
|
||||
use grin_wallet_util::grin_util::secp::key::SecretKey;
|
||||
use grin_wallet_util::grin_util::{from_hex, static_secp_instance};
|
||||
use serde_json;
|
||||
|
||||
#[macro_use]
|
||||
mod common;
|
||||
use common::{
|
||||
clean_output_dir, derive_ecdh_key, execute_command, initial_setup_wallet, instantiate_wallet,
|
||||
send_request, send_request_enc, setup, RetrieveSummaryInfoResp,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn owner_v3_init_secure() -> Result<(), grin_wallet_controller::Error> {
|
||||
let test_dir = "target/test_output/owner_v3_init_secure";
|
||||
setup(test_dir);
|
||||
|
||||
// Create a new proxy to simulate server and wallet responses
|
||||
setup_proxy!(test_dir, chain, wallet1, client1, mask1, wallet2, client2, _mask2);
|
||||
|
||||
// add some blocks manually
|
||||
let bh = 2u64;
|
||||
let _ =
|
||||
test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, bh as usize, false);
|
||||
|
||||
// run a wallet owner listener
|
||||
let arg_vec = vec!["grin-wallet", "-p", "password", "owner_api", "-l", "33420"];
|
||||
thread::spawn(move || {
|
||||
let yml = load_yaml!("../src/bin/grin-wallet.yml");
|
||||
let app = App::from_yaml(yml);
|
||||
execute_command(&app, test_dir, "wallet1", &client1, arg_vec.clone()).unwrap();
|
||||
});
|
||||
thread::sleep(Duration::from_millis(200));
|
||||
|
||||
// use in all tests
|
||||
let sec_key_str = "e00dcc4a009e3427c6b1e1a550c538179d46f3827a13ed74c759c860761caf1e";
|
||||
let _pub_key_str = "03b3c18c9a38783d105e238953b1638b021ba7456d87a5c085b3bdb75777b4c490";
|
||||
|
||||
let sec_key_bytes = from_hex(sec_key_str.to_owned()).unwrap();
|
||||
let sec_key = {
|
||||
let secp_inst = static_secp_instance();
|
||||
let secp = secp_inst.lock();
|
||||
SecretKey::from_slice(&secp, &sec_key_bytes).unwrap()
|
||||
};
|
||||
|
||||
// 1) Attempt to send an encrypted request before calling `init_secure_api`
|
||||
let req = include_str!("data/v3_reqs/retrieve_info.req.json");
|
||||
let res = send_request_enc::<String>(1, 1, "http://127.0.0.1:33420/v3/owner", &req, &sec_key)?;
|
||||
println!("RES 1: {:?}", res);
|
||||
assert!(res.is_err());
|
||||
assert_eq!(res.unwrap_err().code, -32001);
|
||||
|
||||
// 2) Call any function on the V3 api without calling 'init_secure_api` first
|
||||
let res = send_request::<String>(1, "http://127.0.0.1:33420/v3/owner", &req)?;
|
||||
println!("RES 2: {:?}", res);
|
||||
assert!(res.is_err());
|
||||
assert_eq!(res.unwrap_err().code, -32001);
|
||||
|
||||
// 3) Call 'init_secure_api' and negotiate shared key
|
||||
let req = include_str!("data/v3_reqs/init_secure_api.req.json");
|
||||
let res = send_request(1, "http://127.0.0.1:33420/v3/owner", req)?;
|
||||
println!("RES 3: {:?}", res);
|
||||
|
||||
assert!(res.is_ok());
|
||||
let value: ECDHPubkey = res.unwrap();
|
||||
let shared_key = derive_ecdh_key(sec_key_str, &value.ecdh_pubkey);
|
||||
|
||||
// 4) A normal request, correct key
|
||||
let req = include_str!("data/v3_reqs/retrieve_info.req.json");
|
||||
let res = send_request_enc::<RetrieveSummaryInfoResp>(
|
||||
1,
|
||||
1,
|
||||
"http://127.0.0.1:33420/v3/owner",
|
||||
&req,
|
||||
&shared_key,
|
||||
)?;
|
||||
println!("RES 4: {:?}", res);
|
||||
assert!(res.is_ok());
|
||||
|
||||
// 5) A normal request, incorrect key
|
||||
let mut bad_key = shared_key.clone();
|
||||
bad_key.0[0] = 0;
|
||||
let req = include_str!("data/v3_reqs/retrieve_info.req.json");
|
||||
let res = send_request_enc::<RetrieveSummaryInfoResp>(
|
||||
1,
|
||||
1,
|
||||
"http://127.0.0.1:33420/v3/owner",
|
||||
&req,
|
||||
&bad_key,
|
||||
)?;
|
||||
println!("RES 5: {:?}", res);
|
||||
assert!(res.is_err());
|
||||
assert_eq!(res.unwrap_err().code, -32002);
|
||||
|
||||
// 6) A malformed encrypted json request (missing nonce)
|
||||
let req = serde_json::json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "encrypted_request_v3",
|
||||
"params": {
|
||||
"body_enc:": "thisiswrong",
|
||||
}
|
||||
});
|
||||
let res = send_request::<String>(1, "http://127.0.0.1:33420/v3/owner", &req.to_string())?;
|
||||
println!("RES 6: {:?}", res);
|
||||
assert!(res.is_err());
|
||||
assert_eq!(res.unwrap_err().code, -32002);
|
||||
|
||||
// 7) A malformed encrypted json request (garbage encrypted content)
|
||||
let req = serde_json::json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "encrypted_request_v3",
|
||||
"params": {
|
||||
"nonce": "32",
|
||||
"body_enc": "thisiswrong",
|
||||
}
|
||||
});
|
||||
let res = send_request::<String>(1, "http://127.0.0.1:33420/v3/owner", &req.to_string())?;
|
||||
println!("RES 7: {:?}", res);
|
||||
assert!(res.is_err());
|
||||
assert_eq!(res.unwrap_err().code, -32002);
|
||||
|
||||
// 8) Encrypted call to `init_secure_api`, followed by re-deriving key
|
||||
let req = include_str!("data/v3_reqs/init_secure_api.req.json");
|
||||
let res = send_request_enc(
|
||||
1,
|
||||
1,
|
||||
"http://127.0.0.1:33420/v3/owner",
|
||||
&req.to_string(),
|
||||
&shared_key,
|
||||
)?;
|
||||
println!("RES 8: {:?}", res);
|
||||
assert!(res.is_ok());
|
||||
let value: ECDHPubkey = res.unwrap();
|
||||
let shared_key = derive_ecdh_key(sec_key_str, &value.ecdh_pubkey);
|
||||
|
||||
// 9) A normal request, with new correct key
|
||||
let req = include_str!("data/v3_reqs/retrieve_info.req.json");
|
||||
let res = send_request_enc::<RetrieveSummaryInfoResp>(
|
||||
9,
|
||||
1,
|
||||
"http://127.0.0.1:33420/v3/owner",
|
||||
&req,
|
||||
&shared_key,
|
||||
)?;
|
||||
println!("RES 9: {:?}", res);
|
||||
assert!(res.is_ok());
|
||||
|
||||
// 10) Call 'init_secure_api' unencrypted (which we can do) and negotiate new shared key
|
||||
let req = include_str!("data/v3_reqs/init_secure_api.req.json");
|
||||
let res = send_request(1, "http://127.0.0.1:33420/v3/owner", req)?;
|
||||
println!("RES 10: {:?}", res);
|
||||
|
||||
assert!(res.is_ok());
|
||||
let value: ECDHPubkey = res.unwrap();
|
||||
let shared_key = derive_ecdh_key(sec_key_str, &value.ecdh_pubkey);
|
||||
|
||||
// 11) A normal request, correct key
|
||||
let req = include_str!("data/v3_reqs/retrieve_info.req.json");
|
||||
let res = send_request_enc::<RetrieveSummaryInfoResp>(
|
||||
11,
|
||||
1,
|
||||
"http://127.0.0.1:33420/v3/owner",
|
||||
&req,
|
||||
&shared_key,
|
||||
)?;
|
||||
println!("RES 11: {:?}", res);
|
||||
assert!(res.is_ok());
|
||||
|
||||
// 12) A request which triggers and API error (not an encryption error)
|
||||
let req = serde_json::json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "method_dun_exist",
|
||||
"params": {
|
||||
"nope": "nope",
|
||||
}
|
||||
})
|
||||
.to_string();
|
||||
let res =
|
||||
send_request_enc::<String>(12, 1, "http://127.0.0.1:33420/v3/owner", &req, &shared_key)?;
|
||||
println!("RES 12: {:?}", res);
|
||||
assert!(res.is_err());
|
||||
assert_eq!(res.unwrap_err().code, -32601);
|
||||
|
||||
clean_output_dir(test_dir);
|
||||
|
||||
Ok(())
|
||||
}
|
Loading…
Reference in a new issue